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