feat(wm-revenue): #85 智能表平台+短信平台+支付宝对接

This commit is contained in:
2026-06-15 12:37:53 +08:00
parent 7447e748a0
commit 9b11a3f7bf
16 changed files with 1725 additions and 0 deletions
+106
View File
@@ -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, '通用停水通知');
@@ -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<Map<String, Object>> pushBill(@RequestBody Map<String, Object> 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<Map<String, Object>> payNotify(@RequestParam Map<String, String> params) {
return R.ok(alipayService.handlePayNotify(params));
}
/**
* 对账
*/
@GetMapping("/reconcile")
@Operation(summary = "支付宝对账")
public R<Map<String, Object>> reconcile(
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
return R.ok(alipayService.reconcile(startDate, endDate));
}
/**
* 退费
*/
@PostMapping("/refund")
@Operation(summary = "支付宝退费")
public R<Map<String, Object>> refund(@RequestBody Map<String, Object> 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<Page<AlipayOrder>> 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<AlipayOrder> getOrderDetail(@PathVariable Long id) {
return R.ok(alipayService.getOrderDetail(id));
}
}
@@ -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<Map<String, Object>> dashboard() {
return R.ok(smartMeterService.getMeterDashboard());
}
@GetMapping("/list")
@Operation(summary = "智能表列表")
public R<Page<SmartMeter>> 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<SmartMeter> detail(@PathVariable Long id) {
return R.ok(smartMeterService.getDetail(id));
}
@PostMapping("/batch-read")
@Operation(summary = "批量下发抄表指令")
@SuppressWarnings("unchecked")
public R<List<Map<String, Object>>> batchRead(@RequestBody Map<String, Object> req) {
List<String> meterNos = (List<String>) req.get("meterNos");
return R.ok(smartMeterService.batchRead(meterNos));
}
@PostMapping("/alarm")
@Operation(summary = "设置告警阈值")
public R<Map<String, Object>> setAlarm(@RequestBody Map<String, Object> req) {
Long meterId = Long.parseLong(String.valueOf(req.get("meterId")));
return R.ok(smartMeterService.setAlarm(meterId, req));
}
@GetMapping("/alarms")
@Operation(summary = "告警列表")
public R<Map<String, Object>> alarms(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
return R.ok(smartMeterService.getAlarms(page, size));
}
}
@@ -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<SmsRecord> send(@RequestBody Map<String, Object> req) {
String phone = (String) req.get("phone");
Long templateId = Long.parseLong(String.valueOf(req.get("templateId")));
@SuppressWarnings("unchecked")
Map<String, String> variables = (Map<String, String>) req.getOrDefault("variables", Map.of());
return R.ok(smsService.sendSms(phone, templateId, variables));
}
@PostMapping("/batch-send")
@Operation(summary = "批量发送短信")
@SuppressWarnings("unchecked")
public R<List<SmsRecord>> batchSend(@RequestBody Map<String, Object> req) {
List<String> phones = (List<String>) req.get("phones");
Long templateId = Long.parseLong(String.valueOf(req.get("templateId")));
Map<String, String> variables = (Map<String, String>) req.getOrDefault("variables", Map.of());
return R.ok(smsService.batchSend(phones, templateId, variables));
}
@PostMapping("/bill-notice")
@Operation(summary = "账单通知群发")
@SuppressWarnings("unchecked")
public R<List<SmsRecord>> billNotice(@RequestBody Map<String, Object> req) {
List<Map<String, String>> customerList = (List<Map<String, String>>) req.get("customerList");
return R.ok(smsService.sendBillNotice(customerList));
}
@PostMapping("/overdue-reminder")
@Operation(summary = "欠费提醒群发")
@SuppressWarnings("unchecked")
public R<List<SmsRecord>> overdueReminder(@RequestBody Map<String, Object> req) {
List<Map<String, String>> customerList = (List<Map<String, String>>) req.get("customerList");
return R.ok(smsService.sendArrearsWarning(customerList));
}
// ========== 模板管理 ==========
@GetMapping("/templates")
@Operation(summary = "短信模板列表")
public R<Page<SmsTemplate>> 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<SmsTemplate> getTemplate(@PathVariable Long id) {
return R.ok(smsService.getTemplate(id));
}
@PostMapping("/templates")
@Operation(summary = "新增短信模板")
public R<SmsTemplate> addTemplate(@RequestBody SmsTemplate template) {
return R.ok(smsService.createTemplate(template));
}
@PutMapping("/templates/{id}")
@Operation(summary = "更新模板")
public R<String> updateTemplate(@PathVariable Long id, @RequestBody SmsTemplate template) {
smsService.updateTemplate(id, template);
return R.ok("ok");
}
@DeleteMapping("/templates/{id}")
@Operation(summary = "删除模板")
public R<String> deleteTemplate(@PathVariable Long id) {
smsService.deleteTemplate(id);
return R.ok("ok");
}
// ========== 记录查询 ==========
@GetMapping("/records")
@Operation(summary = "发送记录")
public R<Page<SmsRecord>> 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<Map<String, Object>> stats(
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
return R.ok(smsService.getSendStats(startDate, endDate));
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<AlipayOrder> {
}
@@ -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<SmartMeter> {
}
@@ -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<SmsRecord> {
}
@@ -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<SmsTemplate> {
}
@@ -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<String, Object> 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<String, Object> 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<String, Object> handlePayNotify(Map<String, String> 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<String, Object> result = new LinkedHashMap<>();
result.put("outTradeNo", outTradeNo);
result.put("status", update.getStatus());
result.put("success", true);
return result;
}
/**
* 对账 - 查询指定日期范围内的订单进行对账
*/
public Map<String, Object> reconcile(String startDate, String endDate) {
LambdaQueryWrapper<AlipayOrder> 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<AlipayOrder> orders = alipayOrderMapper.selectList(qw);
BigDecimal totalAmount = orders.stream()
.map(AlipayOrder::getAmount)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, Object> 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<String, Object> 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<String, Object> 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<AlipayOrder> listOrders(int page, int size, String status, String customerNo) {
LambdaQueryWrapper<AlipayOrder> 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<AlipayOrder> 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;
}
}
@@ -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<SmartMeter> list(int page, int size, String onlineStatus, String areaCode, String keyword) {
LambdaQueryWrapper<SmartMeter> 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<SmartMeter> qw = new LambdaQueryWrapper<>();
qw.eq(SmartMeter::getMeterNo, meterNo);
return smartMeterMapper.selectOne(qw);
}
/**
* 批量查询在线水表状态(信号强度/电量/阀门)
*/
public List<Map<String, Object>> batchStatus(List<String> meterNos) {
LambdaQueryWrapper<SmartMeter> qw = new LambdaQueryWrapper<>();
qw.in(SmartMeter::getMeterNo, meterNos);
List<SmartMeter> meters = smartMeterMapper.selectList(qw);
return meters.stream().map(m -> {
Map<String, Object> 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<Map<String, Object>> batchRead(List<String> meterNos) {
LambdaQueryWrapper<SmartMeter> qw = new LambdaQueryWrapper<>();
qw.in(SmartMeter::getMeterNo, meterNos);
List<SmartMeter> meters = smartMeterMapper.selectList(qw);
return meters.stream().map(m -> {
Map<String, Object> 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<Map<String, Object>> getAlerts(String alertType, String areaCode) {
LambdaQueryWrapper<SmartMeter> 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<SmartMeter> meters = smartMeterMapper.selectList(qw);
return meters.stream().map(m -> {
Map<String, Object> 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<String> 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<String, Object> 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<String, Object> result = new LinkedHashMap<>();
result.put("meterNo", meterNo);
result.put("valveStatus", "OPEN");
result.put("operateTime", LocalDateTime.now());
result.put("success", true);
return result;
}
/**
* 远程控制 - 关阀
*/
public Map<String, Object> 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<String, Object> result = new LinkedHashMap<>();
result.put("meterNo", meterNo);
result.put("valveStatus", "CLOSED");
result.put("operateTime", LocalDateTime.now());
result.put("success", true);
return result;
}
/**
* 批量控制阀门
*/
public List<Map<String, Object>> batchValveControl(List<String> meterNos, String action) {
List<Map<String, Object>> results = new ArrayList<>();
for (String meterNo : meterNos) {
try {
Map<String, Object> r = "OPEN".equalsIgnoreCase(action) ? openValve(meterNo) : closeValve(meterNo);
results.add(r);
} catch (Exception e) {
Map<String, Object> 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<String, Object> getMeterDashboard() {
List<SmartMeter> 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<String, Object> 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<SmartMeter> listMeters(String type, String status, String area, int page, int size) {
return list(page, size, status, area, null);
}
/**
* SM-003 单表详情(含最新读数、信号强度、电池电量)
*/
public Map<String, Object> getMeterDetail(Long meterId) {
SmartMeter meter = smartMeterMapper.selectById(meterId);
if (meter == null) {
return Collections.emptyMap();
}
Map<String, Object> 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<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> setAlarm(Long meterId, Map<String, Object> 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<String, Object> 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<String, Object> result = new LinkedHashMap<>();
result.put("meterId", meterId);
result.put("success", false);
result.put("errorMsg", e.getMessage());
return result;
}
}
/**
* SM-006 告警列表
*/
public Map<String, Object> getAlarms(int page, int size) {
Map<String, Object> result = new LinkedHashMap<>();
try {
int offset = (page - 1) * size;
List<Map<String, Object>> 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<String, Object> getOverview() {
List<SmartMeter> all = smartMeterMapper.selectList(null);
Map<String, Object> 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;
}
}
@@ -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<SmsTemplate> listTemplates(int page, int size, String templateType, Integer enabled) {
LambdaQueryWrapper<SmsTemplate> 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<String, String> 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<SmsRecord> batchSend(List<String> phones, Long templateId, Map<String, String> variables) {
List<SmsRecord> 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<SmsRecord> sendArrearsWarning(List<Map<String, String>> customerList) {
LambdaQueryWrapper<SmsTemplate> 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<SmsRecord> records = new ArrayList<>();
for (Map<String, String> customer : customerList) {
String phone = customer.get("phone");
Map<String, String> vars = new HashMap<>(customer);
records.add(sendSms(phone, template.getId(), vars));
}
log.info("欠费提醒批量发送: count={}", records.size());
return records;
}
/**
* 账单通知 - 按客户编号发送
*/
public List<SmsRecord> sendBillNotice(List<Map<String, String>> customerList) {
LambdaQueryWrapper<SmsTemplate> 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<SmsRecord> records = new ArrayList<>();
for (Map<String, String> customer : customerList) {
String phone = customer.get("phone");
Map<String, String> vars = new HashMap<>(customer);
records.add(sendSms(phone, template.getId(), vars));
}
log.info("账单通知批量发送: count={}", records.size());
return records;
}
// ========== 记录查询 ==========
/**
* 分页查询发送记录
*/
public Page<SmsRecord> listRecords(int page, int size, String sendStatus, String phone, String bizType) {
LambdaQueryWrapper<SmsRecord> 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<String, Object> getSendStats(String startDate, String endDate) {
LambdaQueryWrapper<SmsRecord> 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<SmsRecord> records = smsRecordMapper.selectList(qw);
Map<String, Object> 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<String, Long> 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<String, String> variables) {
if (variables == null || variables.isEmpty()) {
return template;
}
String result = template;
for (Map.Entry<String, String> entry : variables.entrySet()) {
result = result.replace("${" + entry.getKey() + "}", entry.getValue());
}
return result;
}
}
@@ -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<SmartMeter> 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<SmartMeter> 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<SmartMeter> meters = Arrays.asList(
buildMeter("M001", "ONLINE", 85, 90),
buildMeter("M002", "OFFLINE", 10, 5));
when(smartMeterMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(meters);
List<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<SmsRecord> records = smsService.batchSend(
Arrays.asList("13800138001", "13800138002"), 1L, Map.of("name", "用户"));
assertEquals(2, records.size());
}
@Test
@DisplayName("查询发送记录 - 分页")
void testListRecords() {
Page<SmsRecord> 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<SmsRecord> 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<String, Object> 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<String, Object> 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<String, String> params = Map.of(
"out_trade_no", "ALIPAY_C001_20260615001",
"trade_no", "2026061522001",
"trade_status", "TRADE_SUCCESS");
Map<String, Object> 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<String, Object> 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<String, Object> 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;
}
}