feat(wm-revenue): #85 智能表平台+短信平台+支付宝对接
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user