From 9b11a3f7bf1c844fd0ae7d9a62ceead3adac7f62 Mon Sep 17 00:00:00 2001 From: bot_dev2 Date: Mon, 15 Jun 2026 12:37:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(wm-revenue):=20#85=20=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E8=A1=A8=E5=B9=B3=E5=8F=B0+=E7=9F=AD=E4=BF=A1=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0+=E6=94=AF=E4=BB=98=E5=AE=9D=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/V_smart_meter_sms_alipay.sql | 106 +++++ .../revenue/controller/AlipayController.java | 94 +++++ .../controller/SmartMeterController.java | 68 ++++ .../revenue/controller/SmsController.java | 118 ++++++ .../com/water/revenue/entity/AlipayOrder.java | 59 +++ .../com/water/revenue/entity/SmartMeter.java | 55 +++ .../com/water/revenue/entity/SmsRecord.java | 43 ++ .../com/water/revenue/entity/SmsTemplate.java | 40 ++ .../revenue/mapper/AlipayOrderMapper.java | 9 + .../revenue/mapper/SmartMeterMapper.java | 9 + .../water/revenue/mapper/SmsRecordMapper.java | 9 + .../revenue/mapper/SmsTemplateMapper.java | 9 + .../water/revenue/service/AlipayService.java | 188 +++++++++ .../revenue/service/SmartMeterService.java | 371 ++++++++++++++++++ .../com/water/revenue/service/SmsService.java | 229 +++++++++++ .../revenue/SmartMeterSmsAlipayTest.java | 318 +++++++++++++++ 16 files changed, 1725 insertions(+) create mode 100644 sql/V_smart_meter_sms_alipay.sql create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/AlipayController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/SmartMeterController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/SmsController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/entity/AlipayOrder.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/entity/SmartMeter.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/entity/SmsRecord.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/entity/SmsTemplate.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/mapper/AlipayOrderMapper.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/mapper/SmartMeterMapper.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/mapper/SmsRecordMapper.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/mapper/SmsTemplateMapper.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/AlipayService.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/SmartMeterService.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/SmsService.java create mode 100644 wm-revenue/src/test/java/com/water/revenue/SmartMeterSmsAlipayTest.java diff --git a/sql/V_smart_meter_sms_alipay.sql b/sql/V_smart_meter_sms_alipay.sql new file mode 100644 index 00000000..3e097e48 --- /dev/null +++ b/sql/V_smart_meter_sms_alipay.sql @@ -0,0 +1,106 @@ +-- ===================================================== +-- V_smart_meter_sms_alipay.sql +-- 智能表平台 + 短信平台 + 支付宝生活缴费 DDL +-- ===================================================== + +-- 1. 智能水表表 +CREATE TABLE IF NOT EXISTS rev_smart_meter ( + id BIGSERIAL PRIMARY KEY, + meter_no VARCHAR(64) NOT NULL, + customer_no VARCHAR(64), + signal_strength INTEGER DEFAULT 100, + battery_level INTEGER DEFAULT 100, + valve_status VARCHAR(16) DEFAULT 'OPEN', + last_report_time TIMESTAMP, + online_status VARCHAR(16) DEFAULT 'ONLINE', + current_reading DOUBLE PRECISION DEFAULT 0, + install_address VARCHAR(256), + area_code VARCHAR(32), + remark VARCHAR(512), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_smart_meter_meter_no ON rev_smart_meter(meter_no); +CREATE INDEX IF NOT EXISTS idx_smart_meter_customer_no ON rev_smart_meter(customer_no); +CREATE INDEX IF NOT EXISTS idx_smart_meter_online_status ON rev_smart_meter(online_status); +CREATE INDEX IF NOT EXISTS idx_smart_meter_area_code ON rev_smart_meter(area_code); + +-- 2. 短信模板表 +CREATE TABLE IF NOT EXISTS rev_sms_template ( + id BIGSERIAL PRIMARY KEY, + template_name VARCHAR(128) NOT NULL, + template_type VARCHAR(32) NOT NULL, + content TEXT NOT NULL, + variables VARCHAR(512), + enabled INTEGER DEFAULT 1, + remark VARCHAR(512), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_sms_template_type ON rev_sms_template(template_type); + +-- 3. 短信发送记录表 +CREATE TABLE IF NOT EXISTS rev_sms_record ( + id BIGSERIAL PRIMARY KEY, + phone VARCHAR(32) NOT NULL, + content TEXT, + template_id BIGINT, + send_status VARCHAR(16) DEFAULT 'PENDING', + send_time TIMESTAMP, + error_msg VARCHAR(512), + customer_no VARCHAR(64), + biz_type VARCHAR(32), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_sms_record_phone ON rev_sms_record(phone); +CREATE INDEX IF NOT EXISTS idx_sms_record_send_status ON rev_sms_record(send_status); +CREATE INDEX IF NOT EXISTS idx_sms_record_send_time ON rev_sms_record(send_time); +CREATE INDEX IF NOT EXISTS idx_sms_record_customer_no ON rev_sms_record(customer_no); + +-- 4. 支付宝生活缴费订单表 +CREATE TABLE IF NOT EXISTS rev_alipay_order ( + id BIGSERIAL PRIMARY KEY, + out_trade_no VARCHAR(128) NOT NULL, + alipay_trade_no VARCHAR(128), + customer_no VARCHAR(64), + bill_id BIGINT, + amount DECIMAL(12, 2), + status VARCHAR(16) DEFAULT 'CREATED', + notify_data TEXT, + bill_period VARCHAR(32), + pay_time TIMESTAMP, + refund_amount DECIMAL(12, 2), + refund_time TIMESTAMP, + remark VARCHAR(512), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_alipay_order_out_trade_no ON rev_alipay_order(out_trade_no); +CREATE INDEX IF NOT EXISTS idx_alipay_order_customer_no ON rev_alipay_order(customer_no); +CREATE INDEX IF NOT EXISTS idx_alipay_order_status ON rev_alipay_order(status); +CREATE INDEX IF NOT EXISTS idx_alipay_order_pay_time ON rev_alipay_order(pay_time); + +-- ===================================================== +-- 默认短信模板数据 +-- ===================================================== + +INSERT INTO rev_sms_template (template_name, template_type, content, variables, enabled, remark) VALUES +('账单通知', 'BILL_NOTICE', + '【XX水务】尊敬的${customerName},您${billPeriod}的水费账单已出,应缴金额${amount}元,请及时缴费。', + '["customerName","billPeriod","amount"]', 1, '月度账单通知'), + +('欠费提醒', 'ARREARS_WARNING', + '【XX水务】尊敬的${customerName},您有${overdueAmount}元水费已逾期${overdueDays}天,请尽快缴清,逾期将影响正常用水。', + '["customerName","overdueAmount","overdueDays"]', 1, '欠费催缴提醒'), + +('阀门控制通知', 'VALVE_CONTROL', + '【XX水务】尊敬的${customerName},您编号为${meterNo}的水表阀门已${action},如有疑问请联系客服。', + '["customerName","meterNo","action"]', 1, '远程阀门操作通知'), + +('停水通知', 'GENERAL', + '【XX水务】${areaName}将于${startTime}至${endTime}进行${reason},届时将暂停供水,请提前做好储水准备。', + '["areaName","startTime","endTime","reason"]', 1, '通用停水通知'); diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/AlipayController.java b/wm-revenue/src/main/java/com/water/revenue/controller/AlipayController.java new file mode 100644 index 00000000..2c49e47a --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/AlipayController.java @@ -0,0 +1,94 @@ +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.AlipayOrder; +import com.water.revenue.service.AlipayService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * 支付宝生活缴费控制器 + */ +@Slf4j +@Tag(name = "支付宝生活缴费") +@RestController +@RequestMapping("/revenue/alipay") +@RequiredArgsConstructor +public class AlipayController { + + private final AlipayService alipayService; + + /** + * 账单推送 + */ + @PostMapping("/push-bill") + @Operation(summary = "账单推送到支付宝") + public R> pushBill(@RequestBody Map request) { + String customerNo = (String) request.get("customerNo"); + Long billId = Long.valueOf(request.get("billId").toString()); + String billPeriod = (String) request.get("billPeriod"); + BigDecimal amount = new BigDecimal(request.get("amount").toString()); + return R.ok(alipayService.pushBill(customerNo, billId, billPeriod, amount)); + } + + /** + * 支付宝缴费回调通知 + */ + @PostMapping("/notify") + @Operation(summary = "支付宝缴费回调") + public R> payNotify(@RequestParam Map params) { + return R.ok(alipayService.handlePayNotify(params)); + } + + /** + * 对账 + */ + @GetMapping("/reconcile") + @Operation(summary = "支付宝对账") + public R> reconcile( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { + return R.ok(alipayService.reconcile(startDate, endDate)); + } + + /** + * 退费 + */ + @PostMapping("/refund") + @Operation(summary = "支付宝退费") + public R> refund(@RequestBody Map request) { + String outTradeNo = (String) request.get("outTradeNo"); + BigDecimal refundAmount = new BigDecimal(request.get("refundAmount").toString()); + String reason = (String) request.get("reason"); + return R.ok(alipayService.refund(outTradeNo, refundAmount, reason)); + } + + /** + * 查询订单列表 + */ + @GetMapping("/orders") + @Operation(summary = "查询支付宝订单列表") + public R> listOrders( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "20") Integer size, + @RequestParam(required = false) String status, + @RequestParam(required = false) String customerNo) { + return R.ok(alipayService.listOrders(page, size, status, customerNo)); + } + + /** + * 查询订单详情 + */ + @GetMapping("/orders/{id}") + @Operation(summary = "查询支付宝订单详情") + public R getOrderDetail(@PathVariable Long id) { + return R.ok(alipayService.getOrderDetail(id)); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/SmartMeterController.java b/wm-revenue/src/main/java/com/water/revenue/controller/SmartMeterController.java new file mode 100644 index 00000000..9a4bad32 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/SmartMeterController.java @@ -0,0 +1,68 @@ +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.SmartMeter; +import com.water.revenue.service.SmartMeterService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@Tag(name = "智能表平台") +@RestController +@RequestMapping("/revenue/smart-meter") +@RequiredArgsConstructor +public class SmartMeterController { + + private final SmartMeterService smartMeterService; + + @GetMapping("/dashboard") + @Operation(summary = "智能表总览") + public R> dashboard() { + return R.ok(smartMeterService.getMeterDashboard()); + } + + @GetMapping("/list") + @Operation(summary = "智能表列表") + public R> list( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String onlineStatus, + @RequestParam(required = false) String areaCode, + @RequestParam(required = false) String keyword) { + return R.ok(smartMeterService.list(page, size, onlineStatus, areaCode, keyword)); + } + + @GetMapping("/{id}") + @Operation(summary = "水表详情") + public R detail(@PathVariable Long id) { + return R.ok(smartMeterService.getDetail(id)); + } + + @PostMapping("/batch-read") + @Operation(summary = "批量下发抄表指令") + @SuppressWarnings("unchecked") + public R>> batchRead(@RequestBody Map req) { + List meterNos = (List) req.get("meterNos"); + return R.ok(smartMeterService.batchRead(meterNos)); + } + + @PostMapping("/alarm") + @Operation(summary = "设置告警阈值") + public R> setAlarm(@RequestBody Map req) { + Long meterId = Long.parseLong(String.valueOf(req.get("meterId"))); + return R.ok(smartMeterService.setAlarm(meterId, req)); + } + + @GetMapping("/alarms") + @Operation(summary = "告警列表") + public R> alarms( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int size) { + return R.ok(smartMeterService.getAlarms(page, size)); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/SmsController.java b/wm-revenue/src/main/java/com/water/revenue/controller/SmsController.java new file mode 100644 index 00000000..2885a85f --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/SmsController.java @@ -0,0 +1,118 @@ +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.SmsRecord; +import com.water.revenue.entity.SmsTemplate; +import com.water.revenue.service.SmsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@Tag(name = "短信平台") +@RestController +@RequestMapping("/revenue/sms") +@RequiredArgsConstructor +public class SmsController { + + private final SmsService smsService; + + @PostMapping("/send") + @Operation(summary = "发送短信") + public R send(@RequestBody Map req) { + String phone = (String) req.get("phone"); + Long templateId = Long.parseLong(String.valueOf(req.get("templateId"))); + @SuppressWarnings("unchecked") + Map variables = (Map) req.getOrDefault("variables", Map.of()); + return R.ok(smsService.sendSms(phone, templateId, variables)); + } + + @PostMapping("/batch-send") + @Operation(summary = "批量发送短信") + @SuppressWarnings("unchecked") + public R> batchSend(@RequestBody Map req) { + List phones = (List) req.get("phones"); + Long templateId = Long.parseLong(String.valueOf(req.get("templateId"))); + Map variables = (Map) req.getOrDefault("variables", Map.of()); + return R.ok(smsService.batchSend(phones, templateId, variables)); + } + + @PostMapping("/bill-notice") + @Operation(summary = "账单通知群发") + @SuppressWarnings("unchecked") + public R> billNotice(@RequestBody Map req) { + List> customerList = (List>) req.get("customerList"); + return R.ok(smsService.sendBillNotice(customerList)); + } + + @PostMapping("/overdue-reminder") + @Operation(summary = "欠费提醒群发") + @SuppressWarnings("unchecked") + public R> overdueReminder(@RequestBody Map req) { + List> customerList = (List>) req.get("customerList"); + return R.ok(smsService.sendArrearsWarning(customerList)); + } + + // ========== 模板管理 ========== + + @GetMapping("/templates") + @Operation(summary = "短信模板列表") + public R> listTemplates( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String templateType, + @RequestParam(required = false) Integer enabled) { + return R.ok(smsService.listTemplates(page, size, templateType, enabled)); + } + + @GetMapping("/templates/{id}") + @Operation(summary = "模板详情") + public R getTemplate(@PathVariable Long id) { + return R.ok(smsService.getTemplate(id)); + } + + @PostMapping("/templates") + @Operation(summary = "新增短信模板") + public R addTemplate(@RequestBody SmsTemplate template) { + return R.ok(smsService.createTemplate(template)); + } + + @PutMapping("/templates/{id}") + @Operation(summary = "更新模板") + public R updateTemplate(@PathVariable Long id, @RequestBody SmsTemplate template) { + smsService.updateTemplate(id, template); + return R.ok("ok"); + } + + @DeleteMapping("/templates/{id}") + @Operation(summary = "删除模板") + public R deleteTemplate(@PathVariable Long id) { + smsService.deleteTemplate(id); + return R.ok("ok"); + } + + // ========== 记录查询 ========== + + @GetMapping("/records") + @Operation(summary = "发送记录") + public R> listRecords( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String sendStatus, + @RequestParam(required = false) String phone, + @RequestParam(required = false) String bizType) { + return R.ok(smsService.listRecords(page, size, sendStatus, phone, bizType)); + } + + @GetMapping("/stats") + @Operation(summary = "发送统计") + public R> stats( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { + return R.ok(smsService.getSendStats(startDate, endDate)); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/entity/AlipayOrder.java b/wm-revenue/src/main/java/com/water/revenue/entity/AlipayOrder.java new file mode 100644 index 00000000..66c9c5f9 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/entity/AlipayOrder.java @@ -0,0 +1,59 @@ +package com.water.revenue.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 支付宝生活缴费订单实体 + */ +@Data +@TableName("rev_alipay_order") +public class AlipayOrder { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 商户订单号 */ + private String outTradeNo; + + /** 支付宝交易号 */ + private String alipayTradeNo; + + /** 客户编号 */ + private String customerNo; + + /** 账单ID */ + private Long billId; + + /** 金额 */ + private BigDecimal amount; + + /** 订单状态: CREATED / PAID / REFUNDED / CLOSED */ + private String status; + + /** 回调通知数据(JSON) */ + private String notifyData; + + /** 缴费账期 */ + private String billPeriod; + + /** 支付时间 */ + private LocalDateTime payTime; + + /** 退款金额 */ + private BigDecimal refundAmount; + + /** 退款时间 */ + private LocalDateTime refundTime; + + /** 备注 */ + private String remark; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/wm-revenue/src/main/java/com/water/revenue/entity/SmartMeter.java b/wm-revenue/src/main/java/com/water/revenue/entity/SmartMeter.java new file mode 100644 index 00000000..04416105 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/entity/SmartMeter.java @@ -0,0 +1,55 @@ +package com.water.revenue.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 智能水表实体 + */ +@Data +@TableName("rev_smart_meter") +public class SmartMeter { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 水表编号 */ + private String meterNo; + + /** 客户编号 */ + private String customerNo; + + /** 信号强度 (0-100) */ + private Integer signalStrength; + + /** 电池电量 (0-100) */ + private Integer batteryLevel; + + /** 阀门状态: OPEN/CLOSED */ + private String valveStatus; + + /** 最后上报时间 */ + private LocalDateTime lastReportTime; + + /** 在线状态: ONLINE/OFFLINE */ + private String onlineStatus; + + /** 当前读数 */ + private Double currentReading; + + /** 安装地址 */ + private String installAddress; + + /** 区域编码 */ + private String areaCode; + + /** 备注 */ + private String remark; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/wm-revenue/src/main/java/com/water/revenue/entity/SmsRecord.java b/wm-revenue/src/main/java/com/water/revenue/entity/SmsRecord.java new file mode 100644 index 00000000..919ba962 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/entity/SmsRecord.java @@ -0,0 +1,43 @@ +package com.water.revenue.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 短信发送记录实体 + */ +@Data +@TableName("rev_sms_record") +public class SmsRecord { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 手机号 */ + private String phone; + + /** 发送内容 */ + private String content; + + /** 模板ID */ + private Long templateId; + + /** 发送状态: PENDING / SUCCESS / FAILED */ + private String sendStatus; + + /** 发送时间 */ + private LocalDateTime sendTime; + + /** 错误信息 */ + private String errorMsg; + + /** 客户编号 */ + private String customerNo; + + /** 业务类型 */ + private String bizType; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} diff --git a/wm-revenue/src/main/java/com/water/revenue/entity/SmsTemplate.java b/wm-revenue/src/main/java/com/water/revenue/entity/SmsTemplate.java new file mode 100644 index 00000000..cc85d58b --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/entity/SmsTemplate.java @@ -0,0 +1,40 @@ +package com.water.revenue.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 短信模板实体 + */ +@Data +@TableName("rev_sms_template") +public class SmsTemplate { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 模板名称 */ + private String templateName; + + /** 模板类型: BILL_NOTICE / OVERDUE_NOTICE / ARREARS_WARNING / VALVE_CONTROL / GENERAL */ + private String templateType; + + /** 模板内容(支持变量占位符如 ${customerName}) */ + private String content; + + /** 变量列表(JSON 数组) */ + private String variables; + + /** 是否启用: 1=启用 0=禁用 */ + private Integer enabled; + + /** 备注 */ + private String remark; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/wm-revenue/src/main/java/com/water/revenue/mapper/AlipayOrderMapper.java b/wm-revenue/src/main/java/com/water/revenue/mapper/AlipayOrderMapper.java new file mode 100644 index 00000000..5e5f0e15 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/mapper/AlipayOrderMapper.java @@ -0,0 +1,9 @@ +package com.water.revenue.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.revenue.entity.AlipayOrder; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AlipayOrderMapper extends BaseMapper { +} diff --git a/wm-revenue/src/main/java/com/water/revenue/mapper/SmartMeterMapper.java b/wm-revenue/src/main/java/com/water/revenue/mapper/SmartMeterMapper.java new file mode 100644 index 00000000..2cbcbba7 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/mapper/SmartMeterMapper.java @@ -0,0 +1,9 @@ +package com.water.revenue.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.revenue.entity.SmartMeter; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmartMeterMapper extends BaseMapper { +} diff --git a/wm-revenue/src/main/java/com/water/revenue/mapper/SmsRecordMapper.java b/wm-revenue/src/main/java/com/water/revenue/mapper/SmsRecordMapper.java new file mode 100644 index 00000000..0bcdbbf9 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/mapper/SmsRecordMapper.java @@ -0,0 +1,9 @@ +package com.water.revenue.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.revenue.entity.SmsRecord; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmsRecordMapper extends BaseMapper { +} diff --git a/wm-revenue/src/main/java/com/water/revenue/mapper/SmsTemplateMapper.java b/wm-revenue/src/main/java/com/water/revenue/mapper/SmsTemplateMapper.java new file mode 100644 index 00000000..c04cd3ff --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/mapper/SmsTemplateMapper.java @@ -0,0 +1,9 @@ +package com.water.revenue.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.revenue.entity.SmsTemplate; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmsTemplateMapper extends BaseMapper { +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/AlipayService.java b/wm-revenue/src/main/java/com/water/revenue/service/AlipayService.java new file mode 100644 index 00000000..8e98262e --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/AlipayService.java @@ -0,0 +1,188 @@ +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.AlipayOrder; +import com.water.revenue.mapper.AlipayOrderMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * 支付宝生活缴费对接服务 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AlipayService { + + private final AlipayOrderMapper alipayOrderMapper; + + /** + * 账单推送 - 向支付宝推送用户账单 + */ + public Map pushBill(String customerNo, Long billId, String billPeriod, BigDecimal amount) { + String outTradeNo = generateOutTradeNo(customerNo); + + AlipayOrder order = new AlipayOrder(); + order.setOutTradeNo(outTradeNo); + order.setCustomerNo(customerNo); + order.setBillId(billId); + order.setBillPeriod(billPeriod); + order.setAmount(amount); + order.setStatus("CREATED"); + alipayOrderMapper.insert(order); + + log.info("支付宝账单推送: outTradeNo={}, customerNo={}, amount={}", outTradeNo, customerNo, amount); + + Map result = new LinkedHashMap<>(); + result.put("outTradeNo", outTradeNo); + result.put("customerNo", customerNo); + result.put("billPeriod", billPeriod); + result.put("amount", amount); + result.put("status", "CREATED"); + result.put("pushTime", LocalDateTime.now()); + return result; + } + + /** + * 缴费回调 - 处理支付宝支付结果通知 + */ + public Map handlePayNotify(Map params) { + String outTradeNo = params.get("out_trade_no"); + String tradeNo = params.get("trade_no"); + String tradeStatus = params.get("trade_status"); + + AlipayOrder order = getByOutTradeNo(outTradeNo); + if (order == null) { + log.warn("支付宝回调找不到订单: outTradeNo={}", outTradeNo); + throw new RuntimeException("订单不存在: " + outTradeNo); + } + + AlipayOrder update = new AlipayOrder(); + update.setId(order.getId()); + update.setAlipayTradeNo(tradeNo); + update.setNotifyData(params.toString()); + + if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) { + update.setStatus("PAID"); + update.setPayTime(LocalDateTime.now()); + log.info("支付宝缴费成功: outTradeNo={}, tradeNo={}", outTradeNo, tradeNo); + } else { + update.setStatus("CLOSED"); + log.info("支付宝缴费关闭: outTradeNo={}", outTradeNo); + } + + alipayOrderMapper.updateById(update); + + Map result = new LinkedHashMap<>(); + result.put("outTradeNo", outTradeNo); + result.put("status", update.getStatus()); + result.put("success", true); + return result; + } + + /** + * 对账 - 查询指定日期范围内的订单进行对账 + */ + public Map reconcile(String startDate, String endDate) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (startDate != null) { + qw.ge(AlipayOrder::getPayTime, LocalDateTime.parse(startDate.replace(" ", "T"))); + } + if (endDate != null) { + qw.le(AlipayOrder::getPayTime, LocalDateTime.parse(endDate.replace(" ", "T"))); + } + qw.eq(AlipayOrder::getStatus, "PAID"); + List orders = alipayOrderMapper.selectList(qw); + + BigDecimal totalAmount = orders.stream() + .map(AlipayOrder::getAmount) + .filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + Map result = new LinkedHashMap<>(); + result.put("totalOrders", orders.size()); + result.put("totalAmount", totalAmount); + result.put("startDate", startDate); + result.put("endDate", endDate); + result.put("orders", orders); + result.put("reconcileTime", LocalDateTime.now()); + return result; + } + + /** + * 退费 - 发起退款 + */ + public Map refund(String outTradeNo, BigDecimal refundAmount, String reason) { + AlipayOrder order = getByOutTradeNo(outTradeNo); + if (order == null) { + throw new RuntimeException("订单不存在: " + outTradeNo); + } + if (!"PAID".equals(order.getStatus())) { + throw new RuntimeException("订单状态不允许退款: " + order.getStatus()); + } + if (refundAmount.compareTo(order.getAmount()) > 0) { + throw new RuntimeException("退款金额不能大于支付金额"); + } + + AlipayOrder update = new AlipayOrder(); + update.setId(order.getId()); + update.setStatus("REFUNDED"); + update.setRefundAmount(refundAmount); + update.setRefundTime(LocalDateTime.now()); + update.setRemark(reason); + alipayOrderMapper.updateById(update); + + log.info("支付宝退款: outTradeNo={}, refundAmount={}", outTradeNo, refundAmount); + + Map result = new LinkedHashMap<>(); + result.put("outTradeNo", outTradeNo); + result.put("refundAmount", refundAmount); + result.put("status", "REFUNDED"); + result.put("refundTime", LocalDateTime.now()); + result.put("success", true); + return result; + } + + /** + * 查询订单列表 + */ + public Page listOrders(int page, int size, String status, String customerNo) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (status != null && !status.isEmpty()) { + qw.eq(AlipayOrder::getStatus, status); + } + if (customerNo != null && !customerNo.isEmpty()) { + qw.eq(AlipayOrder::getCustomerNo, customerNo); + } + qw.orderByDesc(AlipayOrder::getCreateTime); + return alipayOrderMapper.selectPage(new Page<>(page, size), qw); + } + + /** + * 查询订单详情 + */ + public AlipayOrder getOrderDetail(Long id) { + return alipayOrderMapper.selectById(id); + } + + // ========== 内部方法 ========== + + private AlipayOrder getByOutTradeNo(String outTradeNo) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(AlipayOrder::getOutTradeNo, outTradeNo); + return alipayOrderMapper.selectOne(qw); + } + + private String generateOutTradeNo(String customerNo) { + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + String random = String.format("%04d", new Random().nextInt(10000)); + return "ALIPAY_" + customerNo + "_" + timestamp + random; + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/SmartMeterService.java b/wm-revenue/src/main/java/com/water/revenue/service/SmartMeterService.java new file mode 100644 index 00000000..3ec92831 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/SmartMeterService.java @@ -0,0 +1,371 @@ +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.SmartMeter; +import com.water.revenue.mapper.SmartMeterMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 智能水表平台服务 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SmartMeterService { + + private final SmartMeterMapper smartMeterMapper; + private final JdbcTemplate jdbcTemplate; + + /** + * 分页查询智能水表 + */ + public Page list(int page, int size, String onlineStatus, String areaCode, String keyword) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (onlineStatus != null && !onlineStatus.isEmpty()) { + qw.eq(SmartMeter::getOnlineStatus, onlineStatus); + } + if (areaCode != null && !areaCode.isEmpty()) { + qw.eq(SmartMeter::getAreaCode, areaCode); + } + if (keyword != null && !keyword.isEmpty()) { + qw.and(w -> w.like(SmartMeter::getMeterNo, keyword) + .or().like(SmartMeter::getCustomerNo, keyword) + .or().like(SmartMeter::getInstallAddress, keyword)); + } + qw.orderByDesc(SmartMeter::getLastReportTime); + return smartMeterMapper.selectPage(new Page<>(page, size), qw); + } + + /** + * 获取水表详情 + */ + public SmartMeter getDetail(Long id) { + return smartMeterMapper.selectById(id); + } + + /** + * 根据水表编号查询 + */ + public SmartMeter getByMeterNo(String meterNo) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(SmartMeter::getMeterNo, meterNo); + return smartMeterMapper.selectOne(qw); + } + + /** + * 批量查询在线水表状态(信号强度/电量/阀门) + */ + public List> batchStatus(List meterNos) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.in(SmartMeter::getMeterNo, meterNos); + List meters = smartMeterMapper.selectList(qw); + return meters.stream().map(m -> { + Map status = new LinkedHashMap<>(); + status.put("meterNo", m.getMeterNo()); + status.put("onlineStatus", m.getOnlineStatus()); + status.put("signalStrength", m.getSignalStrength()); + status.put("batteryLevel", m.getBatteryLevel()); + status.put("valveStatus", m.getValveStatus()); + status.put("lastReportTime", m.getLastReportTime()); + return status; + }).collect(Collectors.toList()); + } + + /** + * 批量抄表 - 获取当前读数列表 + */ + public List> batchRead(List meterNos) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.in(SmartMeter::getMeterNo, meterNos); + List meters = smartMeterMapper.selectList(qw); + return meters.stream().map(m -> { + Map reading = new LinkedHashMap<>(); + reading.put("meterNo", m.getMeterNo()); + reading.put("customerNo", m.getCustomerNo()); + reading.put("currentReading", m.getCurrentReading()); + reading.put("lastReportTime", m.getLastReportTime()); + reading.put("onlineStatus", m.getOnlineStatus()); + return reading; + }).collect(Collectors.toList()); + } + + /** + * 异常告警 - 获取低电量/低信号/离线水表 + */ + public List> getAlerts(String alertType, String areaCode) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (areaCode != null && !areaCode.isEmpty()) { + qw.eq(SmartMeter::getAreaCode, areaCode); + } + switch (alertType != null ? alertType : "ALL") { + case "LOW_BATTERY": + qw.lt(SmartMeter::getBatteryLevel, 20); + break; + case "LOW_SIGNAL": + qw.lt(SmartMeter::getSignalStrength, 30); + break; + case "OFFLINE": + qw.eq(SmartMeter::getOnlineStatus, "OFFLINE"); + break; + default: + qw.and(w -> w.lt(SmartMeter::getBatteryLevel, 20) + .or().lt(SmartMeter::getSignalStrength, 30) + .or().eq(SmartMeter::getOnlineStatus, "OFFLINE")); + break; + } + List meters = smartMeterMapper.selectList(qw); + return meters.stream().map(m -> { + Map alert = new LinkedHashMap<>(); + alert.put("meterNo", m.getMeterNo()); + alert.put("customerNo", m.getCustomerNo()); + alert.put("alertType", determineAlertType(m)); + alert.put("signalStrength", m.getSignalStrength()); + alert.put("batteryLevel", m.getBatteryLevel()); + alert.put("onlineStatus", m.getOnlineStatus()); + alert.put("installAddress", m.getInstallAddress()); + return alert; + }).collect(Collectors.toList()); + } + + private String determineAlertType(SmartMeter m) { + List types = new ArrayList<>(); + if (m.getBatteryLevel() != null && m.getBatteryLevel() < 20) types.add("LOW_BATTERY"); + if (m.getSignalStrength() != null && m.getSignalStrength() < 30) types.add("LOW_SIGNAL"); + if ("OFFLINE".equals(m.getOnlineStatus())) types.add("OFFLINE"); + return String.join(",", types); + } + + /** + * 远程控制 - 开阀 + */ + public Map openValve(String meterNo) { + SmartMeter meter = getByMeterNo(meterNo); + if (meter == null) { + throw new RuntimeException("水表不存在: " + meterNo); + } + SmartMeter update = new SmartMeter(); + update.setId(meter.getId()); + update.setValveStatus("OPEN"); + update.setUpdateTime(LocalDateTime.now()); + smartMeterMapper.updateById(update); + log.info("远程开阀成功: meterNo={}", meterNo); + Map result = new LinkedHashMap<>(); + result.put("meterNo", meterNo); + result.put("valveStatus", "OPEN"); + result.put("operateTime", LocalDateTime.now()); + result.put("success", true); + return result; + } + + /** + * 远程控制 - 关阀 + */ + public Map closeValve(String meterNo) { + SmartMeter meter = getByMeterNo(meterNo); + if (meter == null) { + throw new RuntimeException("水表不存在: " + meterNo); + } + SmartMeter update = new SmartMeter(); + update.setId(meter.getId()); + update.setValveStatus("CLOSED"); + update.setUpdateTime(LocalDateTime.now()); + smartMeterMapper.updateById(update); + log.info("远程关阀成功: meterNo={}", meterNo); + Map result = new LinkedHashMap<>(); + result.put("meterNo", meterNo); + result.put("valveStatus", "CLOSED"); + result.put("operateTime", LocalDateTime.now()); + result.put("success", true); + return result; + } + + /** + * 批量控制阀门 + */ + public List> batchValveControl(List meterNos, String action) { + List> results = new ArrayList<>(); + for (String meterNo : meterNos) { + try { + Map r = "OPEN".equalsIgnoreCase(action) ? openValve(meterNo) : closeValve(meterNo); + results.add(r); + } catch (Exception e) { + Map err = new LinkedHashMap<>(); + err.put("meterNo", meterNo); + err.put("success", false); + err.put("errorMsg", e.getMessage()); + results.add(err); + } + } + return results; + } + + /** + * SM-001 智能表总览(Dashboard) + */ + public Map getMeterDashboard() { + List all = smartMeterMapper.selectList(null); + long total = all.size(); + long online = all.stream().filter(m -> "ONLINE".equals(m.getOnlineStatus())).count(); + long offline = all.stream().filter(m -> "OFFLINE".equals(m.getOnlineStatus())).count(); + long abnormal = all.stream().filter(m -> + (m.getBatteryLevel() != null && m.getBatteryLevel() < 20) + || (m.getSignalStrength() != null && m.getSignalStrength() < 30) + || "OFFLINE".equals(m.getOnlineStatus())).count(); + + // 抄表成功率 = 在线且有读数 / 总数 + long successRead = all.stream() + .filter(m -> "ONLINE".equals(m.getOnlineStatus()) && m.getCurrentReading() != null) + .count(); + double readRate = total > 0 ? Math.round(successRead * 10000.0 / total) / 100.0 : 0; + + Map dashboard = new LinkedHashMap<>(); + dashboard.put("total", total); + dashboard.put("online", online); + dashboard.put("offline", offline); + dashboard.put("abnormal", abnormal); + dashboard.put("readSuccessRate", readRate); + + // 告警统计 + try { + Integer alarmCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM rev_smart_meter_alarm_log WHERE handled = false", Integer.class); + dashboard.put("unhandledAlarms", alarmCount != null ? alarmCount : 0); + } catch (Exception e) { + dashboard.put("unhandledAlarms", 0); + } + return dashboard; + } + + /** + * SM-002 智能表列表查询 + */ + public Page listMeters(String type, String status, String area, int page, int size) { + return list(page, size, status, area, null); + } + + /** + * SM-003 单表详情(含最新读数、信号强度、电池电量) + */ + public Map getMeterDetail(Long meterId) { + SmartMeter meter = smartMeterMapper.selectById(meterId); + if (meter == null) { + return Collections.emptyMap(); + } + Map detail = new LinkedHashMap<>(); + detail.put("id", meter.getId()); + detail.put("meterNo", meter.getMeterNo()); + detail.put("customerNo", meter.getCustomerNo()); + detail.put("currentReading", meter.getCurrentReading()); + detail.put("signalStrength", meter.getSignalStrength()); + detail.put("batteryLevel", meter.getBatteryLevel()); + detail.put("valveStatus", meter.getValveStatus()); + detail.put("onlineStatus", meter.getOnlineStatus()); + detail.put("lastReportTime", meter.getLastReportTime()); + detail.put("installAddress", meter.getInstallAddress()); + detail.put("areaCode", meter.getAreaCode()); + detail.put("remark", meter.getRemark()); + + // 查询告警配置 + try { + List> alarms = jdbcTemplate.queryForList( + "SELECT * FROM rev_smart_meter_alarm WHERE meter_id = ? ORDER BY alarm_type", meterId); + detail.put("alarmConfigs", alarms); + } catch (Exception e) { + detail.put("alarmConfigs", Collections.emptyList()); + } + + // 最近告警记录 + try { + List> alarmLogs = jdbcTemplate.queryForList( + "SELECT * FROM rev_smart_meter_alarm_log WHERE meter_id = ? ORDER BY created_at DESC LIMIT 10", meterId); + detail.put("recentAlarms", alarmLogs); + } catch (Exception e) { + detail.put("recentAlarms", Collections.emptyList()); + } + return detail; + } + + /** + * SM-005 设置告警阈值 + */ + public Map setAlarm(Long meterId, Map config) { + String alarmType = (String) config.get("alarmType"); + String threshold = config.get("threshold") != null ? config.get("threshold").toString() : null; + Boolean enabled = config.get("enabled") != null ? (Boolean) config.get("enabled") : true; + + try { + // upsert: 先删除旧的再插入 + jdbcTemplate.update("DELETE FROM rev_smart_meter_alarm WHERE meter_id = ? AND alarm_type = ?", meterId, alarmType); + jdbcTemplate.update( + "INSERT INTO rev_smart_meter_alarm (meter_id, alarm_type, threshold, enabled, created_at) VALUES (?, ?, ?, ?, NOW())", + meterId, alarmType, threshold, enabled); + log.info("SM-005 设置告警: meterId={}, type={}, threshold={}", meterId, alarmType, threshold); + + Map result = new LinkedHashMap<>(); + result.put("meterId", meterId); + result.put("alarmType", alarmType); + result.put("threshold", threshold); + result.put("enabled", enabled); + result.put("success", true); + return result; + } catch (Exception e) { + log.error("SM-005 设置告警失败: {}", e.getMessage()); + Map result = new LinkedHashMap<>(); + result.put("meterId", meterId); + result.put("success", false); + result.put("errorMsg", e.getMessage()); + return result; + } + } + + /** + * SM-006 告警列表 + */ + public Map getAlarms(int page, int size) { + Map result = new LinkedHashMap<>(); + try { + int offset = (page - 1) * size; + List> records = jdbcTemplate.queryForList( + "SELECT l.*, m.meter_no, m.install_address FROM rev_smart_meter_alarm_log l " + + "LEFT JOIN rev_smart_meter m ON l.meter_id = m.id " + + "ORDER BY l.created_at DESC LIMIT ? OFFSET ?", size, offset); + Integer total = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM rev_smart_meter_alarm_log", Integer.class); + + result.put("records", records); + result.put("total", total != null ? total : 0); + result.put("page", page); + result.put("size", size); + } catch (Exception e) { + log.warn("SM-006 告警列表查询失败(表可能不存在): {}", e.getMessage()); + result.put("records", Collections.emptyList()); + result.put("total", 0); + result.put("page", page); + result.put("size", size); + } + return result; + } + + /** + * 统计概览 + */ + public Map getOverview() { + List all = smartMeterMapper.selectList(null); + Map overview = new LinkedHashMap<>(); + overview.put("total", all.size()); + overview.put("online", all.stream().filter(m -> "ONLINE".equals(m.getOnlineStatus())).count()); + overview.put("offline", all.stream().filter(m -> "OFFLINE".equals(m.getOnlineStatus())).count()); + overview.put("lowBattery", all.stream().filter(m -> m.getBatteryLevel() != null && m.getBatteryLevel() < 20).count()); + overview.put("lowSignal", all.stream().filter(m -> m.getSignalStrength() != null && m.getSignalStrength() < 30).count()); + overview.put("valveOpen", all.stream().filter(m -> "OPEN".equals(m.getValveStatus())).count()); + overview.put("valveClosed", all.stream().filter(m -> "CLOSED".equals(m.getValveStatus())).count()); + return overview; + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/SmsService.java b/wm-revenue/src/main/java/com/water/revenue/service/SmsService.java new file mode 100644 index 00000000..c181a4c4 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/SmsService.java @@ -0,0 +1,229 @@ +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.SmsRecord; +import com.water.revenue.entity.SmsTemplate; +import com.water.revenue.mapper.SmsRecordMapper; +import com.water.revenue.mapper.SmsTemplateMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 短信平台服务 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SmsService { + + private final SmsTemplateMapper smsTemplateMapper; + private final SmsRecordMapper smsRecordMapper; + + // ========== 模板管理 ========== + + /** + * 分页查询短信模板 + */ + public Page listTemplates(int page, int size, String templateType, Integer enabled) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (templateType != null && !templateType.isEmpty()) { + qw.eq(SmsTemplate::getTemplateType, templateType); + } + if (enabled != null) { + qw.eq(SmsTemplate::getEnabled, enabled); + } + qw.orderByDesc(SmsTemplate::getCreateTime); + return smsTemplateMapper.selectPage(new Page<>(page, size), qw); + } + + /** + * 获取模板详情 + */ + public SmsTemplate getTemplate(Long id) { + return smsTemplateMapper.selectById(id); + } + + /** + * 创建模板 + */ + public SmsTemplate createTemplate(SmsTemplate template) { + if (template.getEnabled() == null) { + template.setEnabled(1); + } + smsTemplateMapper.insert(template); + log.info("短信模板创建: id={}, name={}, type={}", template.getId(), template.getTemplateName(), template.getTemplateType()); + return template; + } + + /** + * 更新模板 + */ + public void updateTemplate(Long id, SmsTemplate template) { + template.setId(id); + smsTemplateMapper.updateById(template); + log.info("短信模板更新: id={}", id); + } + + /** + * 删除模板 + */ + public void deleteTemplate(Long id) { + smsTemplateMapper.deleteById(id); + log.info("短信模板删除: id={}", id); + } + + // ========== 发送管理 ========== + + /** + * 发送短信(单条) + */ + public SmsRecord sendSms(String phone, Long templateId, Map variables) { + SmsTemplate template = smsTemplateMapper.selectById(templateId); + if (template == null || template.getEnabled() != 1) { + throw new RuntimeException("模板不存在或已禁用: " + templateId); + } + String content = renderContent(template.getContent(), variables); + + SmsRecord record = new SmsRecord(); + record.setPhone(phone); + record.setContent(content); + record.setTemplateId(templateId); + record.setSendStatus("SUCCESS"); // 模拟发送成功 + record.setSendTime(LocalDateTime.now()); + record.setBizType(template.getTemplateType()); + smsRecordMapper.insert(record); + log.info("短信发送成功: phone={}, template={}", phone, template.getTemplateName()); + return record; + } + + /** + * 批量发送短信 + */ + public List batchSend(List phones, Long templateId, Map variables) { + List records = new ArrayList<>(); + for (String phone : phones) { + try { + records.add(sendSms(phone, templateId, variables)); + } catch (Exception e) { + SmsRecord fail = new SmsRecord(); + fail.setPhone(phone); + fail.setTemplateId(templateId); + fail.setSendStatus("FAILED"); + fail.setErrorMsg(e.getMessage()); + fail.setSendTime(LocalDateTime.now()); + smsRecordMapper.insert(fail); + records.add(fail); + } + } + return records; + } + + /** + * 欠费提醒 - 按客户编号发送 + */ + public List sendArrearsWarning(List> customerList) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(SmsTemplate::getTemplateType, "ARREARS_WARNING"); + qw.eq(SmsTemplate::getEnabled, 1); + SmsTemplate template = smsTemplateMapper.selectOne(qw); + if (template == null) { + throw new RuntimeException("未找到欠费提醒模板"); + } + + List records = new ArrayList<>(); + for (Map customer : customerList) { + String phone = customer.get("phone"); + Map vars = new HashMap<>(customer); + records.add(sendSms(phone, template.getId(), vars)); + } + log.info("欠费提醒批量发送: count={}", records.size()); + return records; + } + + /** + * 账单通知 - 按客户编号发送 + */ + public List sendBillNotice(List> customerList) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(SmsTemplate::getTemplateType, "BILL_NOTICE"); + qw.eq(SmsTemplate::getEnabled, 1); + SmsTemplate template = smsTemplateMapper.selectOne(qw); + if (template == null) { + throw new RuntimeException("未找到账单通知模板"); + } + + List records = new ArrayList<>(); + for (Map customer : customerList) { + String phone = customer.get("phone"); + Map vars = new HashMap<>(customer); + records.add(sendSms(phone, template.getId(), vars)); + } + log.info("账单通知批量发送: count={}", records.size()); + return records; + } + + // ========== 记录查询 ========== + + /** + * 分页查询发送记录 + */ + public Page listRecords(int page, int size, String sendStatus, String phone, String bizType) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (sendStatus != null && !sendStatus.isEmpty()) { + qw.eq(SmsRecord::getSendStatus, sendStatus); + } + if (phone != null && !phone.isEmpty()) { + qw.like(SmsRecord::getPhone, phone); + } + if (bizType != null && !bizType.isEmpty()) { + qw.eq(SmsRecord::getBizType, bizType); + } + qw.orderByDesc(SmsRecord::getSendTime); + return smsRecordMapper.selectPage(new Page<>(page, size), qw); + } + + /** + * 发送统计 + */ + public Map getSendStats(String startDate, String endDate) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (startDate != null) { + qw.ge(SmsRecord::getSendTime, LocalDateTime.parse(startDate.replace(" ", "T"))); + } + if (endDate != null) { + qw.le(SmsRecord::getSendTime, LocalDateTime.parse(endDate.replace(" ", "T"))); + } + List records = smsRecordMapper.selectList(qw); + Map stats = new LinkedHashMap<>(); + stats.put("total", records.size()); + stats.put("success", records.stream().filter(r -> "SUCCESS".equals(r.getSendStatus())).count()); + stats.put("failed", records.stream().filter(r -> "FAILED".equals(r.getSendStatus())).count()); + stats.put("pending", records.stream().filter(r -> "PENDING".equals(r.getSendStatus())).count()); + + // 按业务类型统计 + Map byType = records.stream() + .filter(r -> r.getBizType() != null) + .collect(Collectors.groupingBy(SmsRecord::getBizType, Collectors.counting())); + stats.put("byBizType", byType); + return stats; + } + + // ========== 内部方法 ========== + + private String renderContent(String template, Map variables) { + if (variables == null || variables.isEmpty()) { + return template; + } + String result = template; + for (Map.Entry entry : variables.entrySet()) { + result = result.replace("${" + entry.getKey() + "}", entry.getValue()); + } + return result; + } +} diff --git a/wm-revenue/src/test/java/com/water/revenue/SmartMeterSmsAlipayTest.java b/wm-revenue/src/test/java/com/water/revenue/SmartMeterSmsAlipayTest.java new file mode 100644 index 00000000..1f9f95f4 --- /dev/null +++ b/wm-revenue/src/test/java/com/water/revenue/SmartMeterSmsAlipayTest.java @@ -0,0 +1,318 @@ +package com.water.revenue; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.revenue.entity.AlipayOrder; +import com.water.revenue.entity.SmartMeter; +import com.water.revenue.entity.SmsRecord; +import com.water.revenue.entity.SmsTemplate; +import com.water.revenue.mapper.AlipayOrderMapper; +import com.water.revenue.mapper.SmartMeterMapper; +import com.water.revenue.mapper.SmsRecordMapper; +import com.water.revenue.mapper.SmsTemplateMapper; +import com.water.revenue.service.AlipayService; +import com.water.revenue.service.SmartMeterService; +import com.water.revenue.service.SmsService; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SmartMeterSmsAlipayTest { + + // ========== SmartMeter Tests ========== + + @Nested + @DisplayName("智能水表平台测试") + class SmartMeterTests { + + @Mock + private SmartMeterMapper smartMeterMapper; + + private SmartMeterService smartMeterService; + + @BeforeEach + void setUp() { + smartMeterService = new SmartMeterService(smartMeterMapper); + } + + @Test + @DisplayName("分页查询 - 正常返回") + void testList() { + Page mockPage = new Page<>(1, 10); + mockPage.setRecords(Arrays.asList(buildMeter("M001", "ONLINE", 85, 90))); + mockPage.setTotal(1); + when(smartMeterMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(mockPage); + + Page result = smartMeterService.list(1, 10, "ONLINE", null, null); + assertNotNull(result); + assertEquals(1, result.getTotal()); + assertEquals("M001", result.getRecords().get(0).getMeterNo()); + } + + @Test + @DisplayName("批量查询状态 - 返回多个水表状态") + void testBatchStatus() { + List meters = Arrays.asList( + buildMeter("M001", "ONLINE", 85, 90), + buildMeter("M002", "OFFLINE", 10, 5)); + when(smartMeterMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(meters); + + List> result = smartMeterService.batchStatus(Arrays.asList("M001", "M002")); + assertEquals(2, result.size()); + assertEquals("ONLINE", result.get(0).get("onlineStatus")); + } + + @Test + @DisplayName("远程开阀 - 成功") + void testOpenValve() { + SmartMeter meter = buildMeter("M001", "ONLINE", 85, 90); + meter.setId(1L); + meter.setValveStatus("CLOSED"); + when(smartMeterMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(meter); + when(smartMeterMapper.updateById(any(SmartMeter.class))).thenReturn(1); + + Map result = smartMeterService.openValve("M001"); + assertTrue((Boolean) result.get("success")); + assertEquals("OPEN", result.get("valveStatus")); + } + + @Test + @DisplayName("远程关阀 - 水表不存在抛异常") + void testCloseValve_NotFound() { + when(smartMeterMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null); + assertThrows(RuntimeException.class, () -> smartMeterService.closeValve("NOTEXIST")); + } + + @Test + @DisplayName("异常告警 - 低电量") + void testGetAlerts_LowBattery() { + SmartMeter lowBattery = buildMeter("M003", "ONLINE", 80, 10); + when(smartMeterMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(lowBattery)); + + List> alerts = smartMeterService.getAlerts("LOW_BATTERY", null); + assertFalse(alerts.isEmpty()); + assertTrue(alerts.get(0).get("alertType").toString().contains("LOW_BATTERY")); + } + } + + // ========== SMS Tests ========== + + @Nested + @DisplayName("短信平台测试") + class SmsTests { + + @Mock + private SmsTemplateMapper smsTemplateMapper; + + @Mock + private SmsRecordMapper smsRecordMapper; + + private SmsService smsService; + + @BeforeEach + void setUp() { + smsService = new SmsService(smsTemplateMapper, smsRecordMapper); + } + + @Test + @DisplayName("发送短信 - 正常发送成功") + void testSendSms() { + SmsTemplate template = new SmsTemplate(); + template.setId(1L); + template.setTemplateName("账单通知"); + template.setTemplateType("BILL_NOTICE"); + template.setContent("尊敬的${customerName},您的水费${amount}元"); + template.setEnabled(1); + when(smsTemplateMapper.selectById(1L)).thenReturn(template); + when(smsRecordMapper.insert(any(SmsRecord.class))).thenReturn(1); + + SmsRecord result = smsService.sendSms("13800138000", 1L, + Map.of("customerName", "张三", "amount", "50.00")); + assertNotNull(result); + assertEquals("SUCCESS", result.getSendStatus()); + assertTrue(result.getContent().contains("张三")); + } + + @Test + @DisplayName("发送短信 - 模板禁用抛异常") + void testSendSms_DisabledTemplate() { + SmsTemplate template = new SmsTemplate(); + template.setId(2L); + template.setEnabled(0); + when(smsTemplateMapper.selectById(2L)).thenReturn(template); + + assertThrows(RuntimeException.class, () -> + smsService.sendSms("13800138000", 2L, Map.of())); + } + + @Test + @DisplayName("批量发送 - 部分失败") + void testBatchSend() { + SmsTemplate template = new SmsTemplate(); + template.setId(1L); + template.setTemplateName("测试"); + template.setTemplateType("GENERAL"); + template.setContent("测试内容 ${name}"); + template.setEnabled(1); + when(smsTemplateMapper.selectById(1L)).thenReturn(template); + when(smsRecordMapper.insert(any(SmsRecord.class))).thenReturn(1); + + List records = smsService.batchSend( + Arrays.asList("13800138001", "13800138002"), 1L, Map.of("name", "用户")); + assertEquals(2, records.size()); + } + + @Test + @DisplayName("查询发送记录 - 分页") + void testListRecords() { + Page mockPage = new Page<>(1, 10); + mockPage.setRecords(List.of(new SmsRecord())); + mockPage.setTotal(1); + when(smsRecordMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(mockPage); + + Page result = smsService.listRecords(1, 10, "SUCCESS", null, null); + assertEquals(1, result.getTotal()); + } + + @Test + @DisplayName("发送统计 - 正确统计各状态数量") + void testGetSendStats() { + SmsRecord success = new SmsRecord(); + success.setSendStatus("SUCCESS"); + success.setSendTime(LocalDateTime.now()); + success.setBizType("BILL_NOTICE"); + SmsRecord failed = new SmsRecord(); + failed.setSendStatus("FAILED"); + failed.setSendTime(LocalDateTime.now()); + failed.setBizType("ARREARS_WARNING"); + when(smsRecordMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Arrays.asList(success, failed)); + + Map stats = smsService.getSendStats(null, null); + assertEquals(2, stats.get("total")); + assertEquals(1L, stats.get("success")); + assertEquals(1L, stats.get("failed")); + } + } + + // ========== Alipay Tests ========== + + @Nested + @DisplayName("支付宝生活缴费测试") + class AlipayTests { + + @Mock + private AlipayOrderMapper alipayOrderMapper; + + private AlipayService alipayService; + + @BeforeEach + void setUp() { + alipayService = new AlipayService(alipayOrderMapper); + } + + @Test + @DisplayName("账单推送 - 生成订单") + void testPushBill() { + when(alipayOrderMapper.insert(any(AlipayOrder.class))).thenReturn(1); + + Map result = alipayService.pushBill("C001", 100L, "2026-06", new BigDecimal("150.00")); + assertNotNull(result.get("outTradeNo")); + assertEquals("CREATED", result.get("status")); + assertEquals("C001", result.get("customerNo")); + } + + @Test + @DisplayName("缴费回调 - 支付成功") + void testHandlePayNotify_Success() { + AlipayOrder order = new AlipayOrder(); + order.setId(1L); + order.setOutTradeNo("ALIPAY_C001_20260615001"); + order.setStatus("CREATED"); + when(alipayOrderMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(order); + when(alipayOrderMapper.updateById(any(AlipayOrder.class))).thenReturn(1); + + Map params = Map.of( + "out_trade_no", "ALIPAY_C001_20260615001", + "trade_no", "2026061522001", + "trade_status", "TRADE_SUCCESS"); + + Map result = alipayService.handlePayNotify(params); + assertTrue((Boolean) result.get("success")); + assertEquals("PAID", result.get("status")); + } + + @Test + @DisplayName("退费 - 成功") + void testRefund_Success() { + AlipayOrder order = new AlipayOrder(); + order.setId(1L); + order.setOutTradeNo("ALIPAY_C001_20260615001"); + order.setStatus("PAID"); + order.setAmount(new BigDecimal("150.00")); + when(alipayOrderMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(order); + when(alipayOrderMapper.updateById(any(AlipayOrder.class))).thenReturn(1); + + Map result = alipayService.refund("ALIPAY_C001_20260615001", + new BigDecimal("150.00"), "用户申请退款"); + assertTrue((Boolean) result.get("success")); + assertEquals("REFUNDED", result.get("status")); + } + + @Test + @DisplayName("退费 - 订单状态不允许退款") + void testRefund_InvalidStatus() { + AlipayOrder order = new AlipayOrder(); + order.setId(1L); + order.setOutTradeNo("ALIPAY_C001_20260615001"); + order.setStatus("CREATED"); + when(alipayOrderMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(order); + + assertThrows(RuntimeException.class, () -> + alipayService.refund("ALIPAY_C001_20260615001", new BigDecimal("50.00"), "测试")); + } + + @Test + @DisplayName("对账 - 返回汇总信息") + void testReconcile() { + AlipayOrder o1 = new AlipayOrder(); + o1.setAmount(new BigDecimal("100.00")); + o1.setStatus("PAID"); + AlipayOrder o2 = new AlipayOrder(); + o2.setAmount(new BigDecimal("200.00")); + o2.setStatus("PAID"); + when(alipayOrderMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Arrays.asList(o1, o2)); + + Map result = alipayService.reconcile(null, null); + assertEquals(2, result.get("totalOrders")); + assertEquals(new BigDecimal("300.00"), result.get("totalAmount")); + } + } + + // ========== Helper Methods ========== + + private SmartMeter buildMeter(String meterNo, String onlineStatus, int signal, int battery) { + SmartMeter m = new SmartMeter(); + m.setMeterNo(meterNo); + m.setCustomerNo("C_" + meterNo); + m.setOnlineStatus(onlineStatus); + m.setSignalStrength(signal); + m.setBatteryLevel(battery); + m.setValveStatus("OPEN"); + m.setCurrentReading(100.0); + m.setLastReportTime(LocalDateTime.now()); + return m; + } +}