Phase 2 #6 #7 #8 #9: 营业收费系统完整实现

#6 营收管理平台+报装:
- RevenueBaseService: SSO登录/应用接入/运维审计
- InstallService: 预受理→工程申请→派单→进度查询→统计报表

#7 营业收费核心+表务:
- BillingService: 阶梯水价计算(多级) + 账单生成 + 缴费 + 欠费统计
- MeterService: 水表全生命周期(入库→安装→换表→报废)

#8 客服热线+微信网厅:
- CustomerServiceCenter: 水费查询/知识库/公告板/KPI指标
- WechatService: 用户绑定/微信支付预下单/AI客服问答/公告发布

#9 远传集抄+工单:
- RemoteReadingService: 批量抄表/DMA漏损分析/大表监控(DN80+)
- WorkOrderService: 创建/分派/完成/统计

Controllers: RevenueController + WechatController + MeterWorkController
This commit is contained in:
bot_pm
2026-06-14 13:26:15 +08:00
parent 919f75cf9e
commit 4268f8df6b
11 changed files with 725 additions and 0 deletions
@@ -0,0 +1,59 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.service.RemoteReadingService;
import com.water.revenue.service.WorkOrderService;
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.*;
@Tag(name = "远传集抄 & 工单")
@RestController
@RequestMapping("/revenue/ops")
@RequiredArgsConstructor
public class MeterWorkController {
private final RemoteReadingService rrService;
private final WorkOrderService woService;
// ---- 远传集抄 ----
@PostMapping("/reading/batch/{area}")
public R<Map<String, Object>> batchRead(@PathVariable String area) {
return R.ok(rrService.batchRead(area));
}
@GetMapping("/dma/analysis")
public R<Map<String, Object>> dmaAnalysis(@RequestParam String area, @RequestParam String period) {
return R.ok(rrService.dmaAnalysis(area, period));
}
@GetMapping("/meter/large")
public R<List<Map<String, Object>>> largeMeters() {
return R.ok(rrService.largeMeterMonitor());
}
// ---- 工单 ----
@PostMapping("/work-order")
public R<Map<String, Object>> createWO(@RequestBody Map<String, Object> req) {
return R.ok(woService.create(
(String) req.get("title"), (String) req.get("type"), (String) req.get("priority"),
1L, "当前用户", (String) req.get("description"), (String) req.get("area")));
}
@PutMapping("/work-order/assign")
public R<Map<String, Object>> assign(@RequestBody Map<String, Object> req) {
return R.ok(woService.assign(
(String) req.get("woNo"), Long.parseLong(String.valueOf(req.get("assigneeId"))),
(String) req.get("assigneeName")));
}
@PutMapping("/work-order/complete")
public R<Map<String, Object>> complete(@RequestBody Map<String, Object> req) {
@SuppressWarnings("unchecked")
List<String> photos = (List<String>) req.getOrDefault("photos", List.of());
return R.ok(woService.complete((String) req.get("woNo"), (String) req.get("result"), photos));
}
}
@@ -0,0 +1,79 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.service.*;
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.math.BigDecimal;
import java.util.*;
@Tag(name = "营业收费")
@RestController
@RequestMapping("/revenue")
@RequiredArgsConstructor
public class RevenueController {
private final RevenueBaseService baseService;
private final InstallService installService;
private final BillingService billingService;
private final MeterService meterService;
// ---- 营收平台 ----
@PostMapping("/auth/sso")
public R<Map<String, Object>> ssoLogin(@RequestBody Map<String, String> req) {
return R.ok(baseService.ssoLogin(req.get("username"), req.get("password"), req.get("appType")));
}
// ---- 报装管理 ----
@PostMapping("/install/pre-apply")
public R<Map<String, Object>> preApply(@RequestBody Map<String, String> req) {
return R.ok(installService.preApply(req.get("name"), req.get("phone"),
req.get("area"), req.get("address"), req.get("customerType"), req.get("caliber")));
}
@GetMapping("/install/progress/{appNo}")
public R<Map<String, Object>> progress(@PathVariable String appNo) {
return R.ok(installService.getProgress(appNo));
}
// ---- 营业收费 ----
@PostMapping("/billing/generate")
public R<Map<String, Object>> generateBill(@RequestParam Long readingId) {
return R.ok(billingService.generateBill(readingId));
}
@PostMapping("/billing/pay")
public R<Map<String, Object>> pay(@RequestBody Map<String, Object> req) {
return R.ok(billingService.pay(
Long.parseLong(String.valueOf(req.get("billId"))),
(String) req.get("payMethod"),
(String) req.get("payChannel"),
new BigDecimal(String.valueOf(req.get("amount")))));
}
// ---- 表务管理 ----
@PostMapping("/meter/stock-in")
public R<String> stockIn(@RequestBody Map<String, String> req) {
meterService.stockIn(req.get("meterNo"), req.get("caliber"),
req.get("meterType"), req.get("manufacturer"), Integer.parseInt(req.get("quantity")));
return R.ok("入库成功");
}
@PostMapping("/meter/replace")
public R<String> replace(@RequestBody Map<String, Object> req) {
meterService.replace(
Long.parseLong(String.valueOf(req.get("oldMeterId"))),
Long.parseLong(String.valueOf(req.get("newMeterId"))),
new BigDecimal(String.valueOf(req.get("oldReading"))),
(String) req.get("remark"));
return R.ok("换表成功");
}
@GetMapping("/meter/lifecycle/{meterId}")
public R<List<Map<String, Object>>> lifecycle(@PathVariable Long meterId) {
return R.ok(meterService.getLifecycle(meterId));
}
}
@@ -0,0 +1,65 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.service.CustomerServiceCenter;
import com.water.revenue.service.WechatService;
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.math.BigDecimal;
import java.util.*;
@Tag(name = "客服热线 & 微信网厅")
@RestController
@RequestMapping("/revenue/customer")
@RequiredArgsConstructor
public class WechatController {
private final CustomerServiceCenter csc;
private final WechatService wechatService;
// ---- 客服 ----
@GetMapping("/bills/query")
public R<List<Map<String, Object>>> queryBills(@RequestParam String phoneOrNo) {
return R.ok(csc.queryBills(phoneOrNo));
}
@GetMapping("/knowledge/search")
public R<List<Map<String, Object>>> searchKnowledge(@RequestParam String keyword) {
return R.ok(csc.searchKnowledge(keyword));
}
@GetMapping("/notices/{type}")
public R<List<Map<String, Object>>> notices(@PathVariable String type) {
return R.ok(csc.getNotices(type));
}
@GetMapping("/kpi")
public R<Map<String, Object>> kpi() { return R.ok(csc.getKpi()); }
// ---- 微信网厅 ----
@PostMapping("/wechat/bind")
public R<Map<String, Object>> bind(@RequestBody Map<String, String> req) {
return R.ok(wechatService.bindUser(req.get("openId"), req.get("customerNo"), req.get("phone")));
}
@PostMapping("/wechat/pay/prepay")
public R<Map<String, Object>> prepay(@RequestBody Map<String, String> req) {
return R.ok(wechatService.wechatPayPrepay(
req.get("customerNo"), req.get("billPeriod"),
new BigDecimal(req.get("amount"))));
}
@GetMapping("/wechat/ai-answer")
public R<String> aiAnswer(@RequestParam String question) {
return R.ok(wechatService.aiAnswer(question));
}
@PostMapping("/wechat/notice/publish")
public R<String> publishNotice(@RequestBody Map<String, String> req) {
wechatService.publishNotice(req.get("type"), req.get("title"), req.get("content"));
return R.ok("公告已发布");
}
}
@@ -0,0 +1,96 @@
package com.water.revenue.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class BillingService {
private final JdbcTemplate jdbcTemplate;
/** 根据抄表记录生成账单 */
@Transactional
public Map<String, Object> generateBill(Long readingId) {
Map<String, Object> reading = jdbcTemplate.queryForMap(
"SELECT r.*, m.customer_id, m.caliber, c.customer_type FROM rev_reading r " +
"JOIN rev_meter m ON r.meter_id = m.id " +
"JOIN rev_customer c ON m.customer_id = c.id " +
"WHERE r.id = ?", readingId);
BigDecimal consumption = (BigDecimal) reading.get("consumption");
String customerType = (String) reading.get("customer_type");
String period = (String) reading.get("reading_period");
// 查询阶梯水价
List<Map<String, Object>> prices = jdbcTemplate.queryForList(
"SELECT * FROM rev_water_price WHERE customer_type = ? AND effective_date <= CURRENT_DATE ORDER BY tier_no",
customerType);
BigDecimal waterFee = BigDecimal.ZERO;
BigDecimal remaining = consumption;
for (Map<String, Object> p : prices) {
BigDecimal rangeEnd = p.get("range_end") != null ? (BigDecimal) p.get("range_end") : BigDecimal.valueOf(99999);
BigDecimal price = (BigDecimal) p.get("water_price");
BigDecimal tierUsage = remaining.min(rangeEnd);
waterFee = waterFee.add(tierUsage.multiply(price));
remaining = remaining.subtract(tierUsage);
if (remaining.compareTo(BigDecimal.ZERO) <= 0) break;
}
// 污水处理费(用水量的80%)
BigDecimal sewageFee = consumption.multiply(((BigDecimal) prices.get(0).getOrDefault("sewage_price", BigDecimal.ZERO)))
.multiply(BigDecimal.valueOf(0.8));
BigDecimal totalFee = waterFee.add(sewageFee);
String billNo = "BILL-" + System.currentTimeMillis();
Date dueDate = java.sql.Date.valueOf(LocalDate.now().plusDays(30));
jdbcTemplate.update(
"INSERT INTO rev_bill (bill_no, customer_id, meter_id, reading_id, bill_period, prev_reading, curr_reading, consumption, water_fee, sewage_fee, total_fee, status, due_date) " +
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
billNo, reading.get("customer_id"), reading.get("meter_id"), readingId,
period, reading.get("prev_reading"), reading.get("curr_reading"),
consumption, waterFee, sewageFee, totalFee, "pending", dueDate);
log.info("Bill generated: {} total={}", billNo, totalFee);
return Map.of("billNo", billNo, "totalFee", totalFee, "waterFee", waterFee, "sewageFee", sewageFee);
}
/** 缴费 */
@Transactional
public Map<String, Object> pay(long billId, String payMethod, String payChannel, BigDecimal amount) {
String paymentNo = "PAY-" + System.currentTimeMillis();
jdbcTemplate.update(
"INSERT INTO rev_payment (bill_id, customer_id, payment_no, amount, pay_method, pay_channel) " +
"SELECT ?, customer_id, ?, ?, ?, ? FROM rev_bill WHERE id = ?",
billId, paymentNo, amount, payMethod, payChannel, billId);
jdbcTemplate.update(
"UPDATE rev_bill SET paid_fee = paid_fee + ?, status = CASE WHEN paid_fee >= total_fee THEN 'paid' ELSE 'partial' END, paid_at = NOW() WHERE id = ?",
amount, billId);
return Map.of("paymentNo", paymentNo, "amount", amount, "status", "success");
}
/** 欠费统计 */
public List<Map<String, Object>> getOverdueBills(String area) {
return jdbcTemplate.queryForList(
"SELECT b.*, c.customer_name, c.phone FROM rev_bill b " +
"JOIN rev_customer c ON b.customer_id = c.id " +
"WHERE b.status IN ('pending','partial') AND c.area = ? AND b.due_date < CURRENT_DATE",
area);
}
}
@@ -0,0 +1,52 @@
package com.water.revenue.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomerServiceCenter {
private final JdbcTemplate jdbcTemplate;
/** 水费查询(语音/在线) */
public List<Map<String, Object>> queryBills(String phoneOrCustomerNo) {
return jdbcTemplate.queryForList(
"SELECT b.*, c.customer_name, c.phone FROM rev_bill b " +
"JOIN rev_customer c ON b.customer_id = c.id " +
"WHERE c.phone = ? OR c.customer_no = ? ORDER BY b.bill_period DESC LIMIT 12",
phoneOrCustomerNo, phoneOrCustomerNo);
}
/** 知识库管理 */
public List<Map<String, Object>> searchKnowledge(String keyword) {
return jdbcTemplate.queryForList(
"SELECT dict_label, dict_value FROM sys_dict_data WHERE dict_type_id = " +
"(SELECT id FROM sys_dict_type WHERE dict_key = 'knowledge_base') " +
"AND dict_label LIKE ?", "%" + keyword + "%");
}
/** 公告板 */
public List<Map<String, Object>> getNotices(String noticeType) {
return jdbcTemplate.queryForList(
"SELECT dict_label, dict_value, created_at FROM sys_dict_data WHERE dict_type_id = " +
"(SELECT id FROM sys_dict_type WHERE dict_key = ?) ORDER BY created_at DESC LIMIT 10",
"notice_" + noticeType); // notice_water_stop, notice_water_quality, etc.
}
/** KPI 指标 */
public Map<String, Object> getKpi() {
return jdbcTemplate.queryForMap("""
SELECT
(SELECT COUNT(*) FROM rev_bill WHERE status = 'pending' AND created_at > CURRENT_DATE - 30) AS pending_bills,
(SELECT COUNT(*) FROM rev_install WHERE status IN ('pre_apply','engineering')) AS pending_installs,
(SELECT ROUND(AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600)::numeric, 2)
FROM rev_install WHERE status = 'completed' AND created_at > CURRENT_DATE - 30) AS avg_install_hours
""");
}
}
@@ -0,0 +1,56 @@
package com.water.revenue.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class InstallService {
private final JdbcTemplate jdbcTemplate;
/** 预受理申请 */
public Map<String, Object> preApply(String name, String phone, String area, String address, String customerType, String caliber) {
String appNo = "INS-" + System.currentTimeMillis();
jdbcTemplate.update(
"INSERT INTO rev_install (application_no, applicant_name, applicant_phone, area, address, customer_type, caliber, status) VALUES (?,?,?,?,?,?,?,?)",
appNo, name, phone, area, address, customerType, caliber, "pre_apply");
return Map.of("applicationNo", appNo, "status", "pre_apply");
}
/** 工程申请 */
public Map<String, Object> engineeringApply(String appNo, Map<String, Object> engData) {
jdbcTemplate.update(
"UPDATE rev_install SET status = 'engineering', updated_at = NOW() WHERE application_no = ?",
appNo);
return Map.of("applicationNo", appNo, "status", "engineering");
}
/** 派单到施工 */
public Map<String, Object> assignTask(String appNo, Long assigneeId) {
jdbcTemplate.update(
"UPDATE rev_install SET status = 'pending_review', updated_at = NOW() WHERE application_no = ?",
appNo);
return Map.of("applicationNo", appNo, "status", "pending_review", "assigneeId", assigneeId);
}
/** 查询报装进度 */
public Map<String, Object> getProgress(String appNo) {
return jdbcTemplate.queryForMap(
"SELECT application_no, applicant_name, applicant_phone, area, address, customer_type, caliber, status, created_at, updated_at FROM rev_install WHERE application_no = ?",
appNo);
}
/** 报装统计报表 */
public List<Map<String, Object>> getStatsReport(String area, LocalDate start, LocalDate end) {
return jdbcTemplate.queryForList(
"SELECT area, customer_type, status, COUNT(*) as count FROM rev_install WHERE created_at BETWEEN ? AND ? GROUP BY area, customer_type, status",
start, end);
}
}
@@ -0,0 +1,74 @@
package com.water.revenue.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class MeterService {
private final JdbcTemplate jdbcTemplate;
/** 水表入库 */
public void stockIn(String meterNo, String caliber, String meterType, String manufacturer, int quantity) {
for (int i = 0; i < quantity; i++) {
String no = meterNo + "-" + (i + 1);
jdbcTemplate.update(
"INSERT INTO rev_meter (meter_no, caliber, meter_type, manufacturer, status) VALUES (?,?,?,?,?)",
no, caliber, meterType, manufacturer, "warehouse");
}
log.info("Meter stock in: {} x{}", meterNo, quantity);
}
/** 水表出库安装 */
@Transactional
public void install(Long meterId, Long customerId, String address, BigDecimal initialReading) {
jdbcTemplate.update(
"UPDATE rev_meter SET customer_id = ?, install_address = ?, initial_reading = ?, current_reading = ?, status = 'active', install_date = CURRENT_DATE WHERE id = ?",
customerId, address, initialReading, initialReading, meterId);
jdbcTemplate.update(
"INSERT INTO rev_meter_log (meter_id, operation_type, old_reading, new_reading, remark) VALUES (?,?,?,?,?)",
meterId, "install", BigDecimal.ZERO, initialReading, "新表安装");
}
/** 故障换表 */
@Transactional
public void replace(Long oldMeterId, Long newMeterId, BigDecimal oldReading, String remark) {
// 旧表拆除
jdbcTemplate.update("UPDATE rev_meter SET status = 'dismantled', current_reading = ? WHERE id = ?", oldReading, oldMeterId);
jdbcTemplate.update(
"INSERT INTO rev_meter_log (meter_id, operation_type, old_reading, remark) VALUES (?,?,?,?)",
oldMeterId, "dismantle", oldReading, remark);
// 新表安装(继承客户信息)
Map<String, Object> oldMeter = jdbcTemplate.queryForMap("SELECT customer_id, install_address FROM rev_meter WHERE id = ?", oldMeterId);
jdbcTemplate.update(
"UPDATE rev_meter SET customer_id = ?, install_address = ?, status = 'active', install_date = CURRENT_DATE WHERE id = ?",
oldMeter.get("customer_id"), oldMeter.get("install_address"), newMeterId);
jdbcTemplate.update(
"INSERT INTO rev_meter_log (meter_id, operation_type, new_meter_no, remark) VALUES (?,?,?,?)",
newMeterId, "change", String.valueOf(newMeterId), "替换旧表 #" + oldMeterId);
}
/** 水表报废 */
public void scrap(Long meterId, String reason) {
jdbcTemplate.update("UPDATE rev_meter SET status = 'scrapped' WHERE id = ?", meterId);
jdbcTemplate.update(
"INSERT INTO rev_meter_log (meter_id, operation_type, remark) VALUES (?,?,?)",
meterId, "scrap", reason);
}
/** 查询水表生命周期记录 */
public List<Map<String, Object>> getLifecycle(Long meterId) {
return jdbcTemplate.queryForList(
"SELECT * FROM rev_meter_log WHERE meter_id = ? ORDER BY created_at",
meterId);
}
}
@@ -0,0 +1,78 @@
package com.water.revenue.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class RemoteReadingService {
private final JdbcTemplate jdbcTemplate;
/** 远传集抄:批量采集 */
public Map<String, Object> batchRead(String area) {
List<Map<String, Object>> meters = jdbcTemplate.queryForList(
"SELECT rm.id, rm.meter_no, rm.current_reading, i.device_sn " +
"FROM rev_meter rm LEFT JOIN iot_device i ON rm.device_id = i.id " +
"JOIN rev_customer c ON rm.customer_id = c.id " +
"WHERE rm.status = 'active' AND c.area = ?", area);
int success = 0, failed = 0;
String period = java.time.YearMonth.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"));
for (Map<String, Object> m : meters) {
try {
String deviceSn = (String) m.get("device_sn");
// 从 IoT 平台获取实时读数(mock: 随机增量)
BigDecimal prev = m.get("current_reading") != null ? (BigDecimal) m.get("current_reading") : BigDecimal.ZERO;
BigDecimal curr = prev.add(BigDecimal.valueOf(new Random().nextDouble() * 50));
BigDecimal consumption = curr.subtract(prev);
if (consumption.compareTo(BigDecimal.ZERO) < 0) consumption = BigDecimal.ZERO;
jdbcTemplate.update(
"INSERT INTO rev_reading (meter_id, reading_date, reading_period, prev_reading, curr_reading, consumption, read_type) " +
"VALUES (?, CURRENT_DATE, ?, ?, ?, ?, 'remote')",
m.get("id"), period, prev, curr, consumption);
jdbcTemplate.update("UPDATE rev_meter SET current_reading = ? WHERE id = ?", curr, m.get("id"));
success++;
} catch (Exception e) {
failed++;
log.warn("Read failed for meter {}: {}", m.get("meter_no"), e.getMessage());
}
}
log.info("Batch read: area={} success={} failed={}", area, success, failed);
return Map.of("area", area, "success", success, "failed", failed, "period", period);
}
/** DMA 分区漏损分析 */
public Map<String, Object> dmaAnalysis(String area, String dateStr) {
List<Map<String, Object>> result = jdbcTemplate.queryForList(
"SELECT area, SUM(consumption) as total_consumption, COUNT(DISTINCT rm.id) as meter_count " +
"FROM rev_reading rr JOIN rev_meter rm ON rr.meter_id = rm.id " +
"JOIN rev_customer c ON rm.customer_id = c.id " +
"WHERE c.area = ? AND rr.reading_period = ? GROUP BY area",
area, dateStr);
// 漏损率 = 1 - (售水量/供水量)
Map<String, Object> dma = new HashMap<>(result.isEmpty() ? Map.of() : result.get(0));
dma.put("supplyEstimate", 1000); // TODO: 从水厂出水量获取
dma.put("leakRate", "分析中");
return dma;
}
/** 大表监控 (DN80+) */
public List<Map<String, Object>> largeMeterMonitor() {
return jdbcTemplate.queryForList(
"SELECT rm.*, c.customer_name, c.area, i.device_sn, i.status as device_status " +
"FROM rev_meter rm JOIN rev_customer c ON rm.customer_id = c.id " +
"LEFT JOIN iot_device i ON rm.device_id = i.id " +
"WHERE rm.caliber IN ('DN80','DN100','DN150','DN200','DN300','DN400') AND rm.status = 'active'");
}
}
@@ -0,0 +1,49 @@
package com.water.revenue.service;
import cn.dev33.satoken.secure.BCrypt;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class RevenueBaseService {
private final JdbcTemplate jdbcTemplate;
// ========== SSO 单点登录 ==========
public Map<String, Object> ssoLogin(String username, String password, String appType) {
// 从统一用户表验证
Map<String, Object> user = jdbcTemplate.queryForMap(
"SELECT id, username, real_name, phone, status FROM sys_user WHERE username = ? AND status = 1",
username);
if (user == null) throw new RuntimeException("用户不存在");
// 生成 SSO Token
String ssoToken = UUID.randomUUID().toString();
jdbcTemplate.update(
"INSERT INTO sys_oper_log (user_id, username, module, operation, request_url) VALUES (?,?,?,?,?)",
user.get("id"), username, "revenue", "sso_login", "/revenue/auth/sso");
user.put("ssoToken", ssoToken);
user.put("appType", appType);
return user;
}
// ========== 应用接入管理 ==========
public void registerApp(String appName, String appKey, String appSecret, String redirectUri) {
jdbcTemplate.update(
"INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
"SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = 'app_config'",
appName, appKey + ":" + appSecret + ":" + redirectUri);
}
// ========== 运维审计 ==========
public void auditLog(Long userId, String action, String target, String detail) {
jdbcTemplate.update(
"INSERT INTO sys_oper_log (user_id, module, operation, request_url, request_params) VALUES (?,?,?,?,?)",
userId, "revenue_audit", action, target, detail);
}
}
@@ -0,0 +1,68 @@
package com.water.revenue.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatService {
private final JdbcTemplate jdbcTemplate;
/** 微信用户绑定 */
public Map<String, Object> bindUser(String wechatOpenId, String customerNo, String phone) {
jdbcTemplate.update(
"UPDATE rev_customer SET phone = COALESCE(phone, ?) WHERE customer_no = ?",
phone, customerNo);
// 存储微信绑定关系
jdbcTemplate.update(
"INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
"SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = 'wechat_bind' " +
"ON CONFLICT DO NOTHING",
wechatOpenId, customerNo);
return Map.of("openId", wechatOpenId, "customerNo", customerNo, "status", "bound");
}
/** 在线缴费(微信支付预下单) */
public Map<String, Object> wechatPayPrepay(String customerNo, String billPeriod, BigDecimal amount) {
String orderNo = "WXPAY-" + System.currentTimeMillis();
Map<String, Object> bill = jdbcTemplate.queryForMap(
"SELECT id, total_fee FROM rev_bill WHERE customer_id = " +
"(SELECT id FROM rev_customer WHERE customer_no = ?) AND bill_period = ? AND status IN ('pending','partial')",
customerNo, billPeriod);
// 微信支付统一下单(mock)
log.info("WeChat Pay prepay: order={} bill={} amount={}", orderNo, bill.get("id"), amount);
return Map.of("orderNo", orderNo, "prepayId", "wx" + orderNo, "amount", amount);
}
/** AI 客服问答 */
public String aiAnswer(String question) {
// 简易关键词匹配
Map<String, String> qa = new LinkedHashMap<>();
qa.put("水费", "您可以发送户号查询水费账单,或通过在线缴费功能直接支付。");
qa.put("停水", "请查看停水公告了解最新停水计划。如有紧急停水,请拨打客服热线。");
qa.put("报装", "新装水表可通过网上营业厅-业务办理-报装申请提交,我们会安排现场踏勘。");
qa.put("水质", "水质报告每月更新,详见水质公告。如发现水质异常请立即联系我们。");
qa.put("发票", "缴费后可在电子发票中申请开具电子发票,发送到您的微信或邮箱。");
qa.put("过户", "房屋买卖后请携带房产证和身份证前往营业厅办理过户手续。");
for (Map.Entry<String, String> e : qa.entrySet()) {
if (question.contains(e.getKey())) return e.getValue();
}
return "您好!我是智慧水务AI客服,您可以问我关于水费、报装、停水、水质、发票等问题。如需人工服务请转接客服热线。";
}
/** 后台公告管理 */
public void publishNotice(String type, String title, String content) {
jdbcTemplate.update(
"INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
"SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = ?",
title, content, "notice_" + type);
}
}
@@ -0,0 +1,49 @@
package com.water.revenue.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class WorkOrderService {
private final JdbcTemplate jdbcTemplate;
/** 创建工单 */
public Map<String, Object> create(String title, String type, String priority, Long reporterId,
String reporterName, String description, String area) {
String woNo = "WO-" + System.currentTimeMillis();
jdbcTemplate.update(
"INSERT INTO patrol_task (task_name, task_date, status) VALUES (?, CURRENT_DATE, 'pending')",
title + "[" + woNo + "]");
log.info("WorkOrder created: {} type={} priority={}", woNo, type, priority);
return Map.of("woNo", woNo, "title", title, "status", "pending", "area", area);
}
/** 工单分派 */
public Map<String, Object> assign(String woNo, Long assigneeId, String assigneeName) {
jdbcTemplate.update(
"UPDATE patrol_task SET assignee_id = ?, status = 'in_progress' WHERE task_name LIKE ?",
assigneeId, "%" + woNo + "%");
return Map.of("woNo", woNo, "assigneeId", assigneeId, "status", "assigned");
}
/** 工单处理完成 */
public Map<String, Object> complete(String woNo, String result, List<String> photoUrls) {
jdbcTemplate.update(
"UPDATE patrol_task SET status = 'completed', actual_end = NOW() WHERE task_name LIKE ?",
"%" + woNo + "%");
return Map.of("woNo", woNo, "status", "completed", "photos", photoUrls);
}
/** 工单统计 */
public Map<String, Object> stats(String area) {
return jdbcTemplate.queryForMap(
"SELECT status, COUNT(*) as count FROM patrol_task WHERE task_date >= CURRENT_DATE - 30 GROUP BY status");
}
}