feat(wm-revenue): #6 营收管理平台+报装管理系统增强(审计+应用接入+报装概览/任务/查询/报表)

This commit is contained in:
2026-06-15 10:32:30 +08:00
parent 5524e1e2ae
commit c4b607611d
13 changed files with 1052 additions and 0 deletions
@@ -0,0 +1,64 @@
-- =============================================
-- 智慧水务管理系统 - 营收管理平台与报装管理系统增强
-- 版本: V4 (Issue #6)
-- =============================================
-- 平台运维审计日志
CREATE TABLE IF NOT EXISTS rev_audit_log (
id BIGSERIAL PRIMARY KEY,
user_id VARCHAR(50) NOT NULL,
user_name VARCHAR(100) NOT NULL,
action VARCHAR(50) NOT NULL, -- CREATE/UPDATE/DELETE/LOGIN/EXPORT
target_type VARCHAR(50), -- customer/meter/bill/app
target_id VARCHAR(50),
detail TEXT,
ip VARCHAR(50),
created_at TIMESTAMP DEFAULT NOW()
);
COMMENT ON TABLE rev_audit_log IS '平台运维审计日志表';
CREATE INDEX IF NOT EXISTS idx_audit_user ON rev_audit_log(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_action ON rev_audit_log(action);
CREATE INDEX IF NOT EXISTS idx_audit_created ON rev_audit_log(created_at);
-- 应用接入注册表
CREATE TABLE IF NOT EXISTS rev_app_registry (
id BIGSERIAL PRIMARY KEY,
app_id VARCHAR(50) UNIQUE NOT NULL,
app_secret VARCHAR(100) NOT NULL,
app_name VARCHAR(100) NOT NULL,
redirect_uris TEXT, -- JSON array of redirect URIs
enabled SMALLINT DEFAULT 1, -- 0:disabled 1:enabled
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
COMMENT ON TABLE rev_app_registry IS '应用接入注册表';
CREATE INDEX IF NOT EXISTS idx_app_enabled ON rev_app_registry(enabled);
-- 报装任务表
CREATE TABLE IF NOT EXISTS rev_install_task (
id BIGSERIAL PRIMARY KEY,
task_id VARCHAR(50) UNIQUE NOT NULL,
apply_no VARCHAR(50) NOT NULL,
task_type VARCHAR(50) NOT NULL, -- design/construction/inspection
assignee_id BIGINT,
assignee_name VARCHAR(100),
description TEXT,
status VARCHAR(20) DEFAULT 'pending', -- pending/in_progress/completed/cancelled
remark TEXT,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
COMMENT ON TABLE rev_install_task IS '报装任务表';
CREATE INDEX IF NOT EXISTS idx_task_apply ON rev_install_task(apply_no);
CREATE INDEX IF NOT EXISTS idx_task_assignee ON rev_install_task(assignee_id);
CREATE INDEX IF NOT EXISTS idx_task_status ON rev_install_task(status);
-- 增强报装表(添加缺失的时间戳字段)
ALTER TABLE rev_installation ADD COLUMN IF NOT EXISTS dispatched_at TIMESTAMP;
ALTER TABLE rev_installation ADD COLUMN IF NOT EXISTS construction_started_at TIMESTAMP;
ALTER TABLE rev_installation ADD COLUMN IF NOT EXISTS customer_type VARCHAR(20);
COMMENT ON COLUMN rev_installation.dispatched_at IS '派单时间';
COMMENT ON COLUMN rev_installation.construction_started_at IS '施工开始时间';
COMMENT ON COLUMN rev_installation.customer_type IS '客户类型';
@@ -0,0 +1,59 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.service.AppAccessService;
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/apps")
@RequiredArgsConstructor
public class AppAccessController {
private final AppAccessService appAccessService;
@Operation(summary = "注册应用")
@PostMapping
public R<Map<String, Object>> registerApp(@RequestBody Map<String, Object> request) {
String appName = (String) request.get("appName");
String redirectUris = (String) request.get("redirectUris");
return R.ok(appAccessService.registerApp(appName, redirectUris));
}
@Operation(summary = "启用/禁用应用")
@PutMapping("/{appId}/toggle")
public R<Map<String, Object>> toggleApp(
@PathVariable String appId,
@RequestBody Map<String, Object> request) {
Boolean enabled = (Boolean) request.get("enabled");
return R.ok(appAccessService.toggleApp(appId, enabled));
}
@Operation(summary = "应用列表")
@GetMapping
public R<List<Map<String, Object>>> listApps(
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return R.ok(appAccessService.listApps(keyword, page, size));
}
@Operation(summary = "应用详情")
@GetMapping("/{appId}")
public R<Map<String, Object>> getAppDetail(@PathVariable String appId) {
return R.ok(appAccessService.getAppDetail(appId));
}
@Operation(summary = "删除应用")
@DeleteMapping("/{appId}")
public R<Void> deleteApp(@PathVariable String appId) {
appAccessService.deleteApp(appId);
return R.ok();
}
}
@@ -0,0 +1,50 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.service.InstallReportService;
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/install/reports")
@RequiredArgsConstructor
public class InstallReportController {
private final InstallReportService installReportService;
@Operation(summary = "报装周期分析")
@GetMapping("/cycle")
public R<Map<String, Object>> cycleAnalysis(
@RequestParam String startDate,
@RequestParam String endDate) {
return R.ok(installReportService.cycleAnalysis(startDate, endDate));
}
@Operation(summary = "区域分布统计")
@GetMapping("/area")
public R<List<Map<String, Object>>> areaDistribution(
@RequestParam String startDate,
@RequestParam String endDate) {
return R.ok(installReportService.areaDistribution(startDate, endDate));
}
@Operation(summary = "月度趋势")
@GetMapping("/monthly")
public R<List<Map<String, Object>>> monthlyTrend(@RequestParam(defaultValue = "2026") String year) {
return R.ok(installReportService.monthlyTrend(year));
}
@Operation(summary = "客户类型分布")
@GetMapping("/customer-type")
public R<List<Map<String, Object>>> customerTypeDistribution(
@RequestParam String startDate,
@RequestParam String endDate) {
return R.ok(installReportService.customerTypeDistribution(startDate, endDate));
}
}
@@ -0,0 +1,44 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.service.InstallationOverviewService;
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/install/overview")
@RequiredArgsConstructor
public class InstallationOverviewController {
private final InstallationOverviewService installationOverviewService;
@Operation(summary = "概览数据")
@GetMapping
public R<Map<String, Object>> getOverview() {
return R.ok(installationOverviewService.getOverview());
}
@Operation(summary = "按月统计")
@GetMapping("/stats/month")
public R<List<Map<String, Object>>> statsByMonth(@RequestParam(defaultValue = "2026") String year) {
return R.ok(installationOverviewService.statsByMonth(year));
}
@Operation(summary = "按区域统计")
@GetMapping("/stats/area")
public R<List<Map<String, Object>>> statsByArea() {
return R.ok(installationOverviewService.statsByArea());
}
@Operation(summary = "转化率分析")
@GetMapping("/conversion")
public R<Map<String, Object>> conversionAnalysis() {
return R.ok(installationOverviewService.conversionAnalysis());
}
}
@@ -0,0 +1,45 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.service.InstallationQueryService;
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/install/query")
@RequiredArgsConstructor
public class InstallationQueryController {
private final InstallationQueryService installationQueryService;
@Operation(summary = "综合查询")
@GetMapping
public R<Map<String, Object>> query(
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String status,
@RequestParam(required = false) String area,
@RequestParam(required = false) String customerType,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return R.ok(installationQueryService.query(startDate, endDate, status, area, customerType, keyword, page, size));
}
@Operation(summary = "导出CSV")
@GetMapping("/export")
public R<List<Map<String, Object>>> exportCsv(
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String status,
@RequestParam(required = false) String area,
@RequestParam(required = false) String customerType) {
return R.ok(installationQueryService.exportCsv(startDate, endDate, status, area, customerType));
}
}
@@ -0,0 +1,64 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.service.InstallationTaskService;
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/install/tasks")
@RequiredArgsConstructor
public class InstallationTaskController {
private final InstallationTaskService installationTaskService;
@Operation(summary = "创建任务")
@PostMapping
public R<Map<String, Object>> createTask(@RequestBody Map<String, Object> request) {
String applyNo = (String) request.get("applyNo");
String taskType = (String) request.get("taskType");
Long assigneeId = Long.parseLong(String.valueOf(request.get("assigneeId")));
String assigneeName = (String) request.get("assigneeName");
String description = (String) request.get("description");
return R.ok(installationTaskService.createTask(applyNo, taskType, assigneeId, assigneeName, description));
}
@Operation(summary = "更新任务状态")
@PutMapping("/{taskId}/status")
public R<Map<String, Object>> updateTaskStatus(
@PathVariable String taskId,
@RequestBody Map<String, Object> request) {
String status = (String) request.get("status");
String remark = (String) request.get("remark");
return R.ok(installationTaskService.updateTaskStatus(taskId, status, remark));
}
@Operation(summary = "任务列表")
@GetMapping
public R<List<Map<String, Object>>> listTasks(
@RequestParam(required = false) String applyNo,
@RequestParam(required = false) String assigneeName,
@RequestParam(required = false) String status,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return R.ok(installationTaskService.listTasks(applyNo, assigneeName, status, page, size));
}
@Operation(summary = "任务看板")
@GetMapping("/kanban")
public R<Map<String, Object>> kanban() {
return R.ok(installationTaskService.kanban());
}
@Operation(summary = "任务详情")
@GetMapping("/{taskId}")
public R<Map<String, Object>> getTaskDetail(@PathVariable String taskId) {
return R.ok(installationTaskService.getTaskDetail(taskId));
}
}
@@ -0,0 +1,48 @@
package com.water.revenue.controller;
import com.water.common.core.result.R;
import com.water.revenue.service.RevAuditService;
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/audit")
@RequiredArgsConstructor
public class RevAuditController {
private final RevAuditService revAuditService;
@Operation(summary = "查询审计日志")
@GetMapping("/logs")
public R<Map<String, Object>> queryLogs(
@RequestParam(required = false) String userId,
@RequestParam(required = false) String action,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return R.ok(revAuditService.queryLogs(userId, action, startDate, endDate, page, size));
}
@Operation(summary = "按天统计")
@GetMapping("/stats/day")
public R<List<Map<String, Object>>> statsByDay(
@RequestParam String startDate,
@RequestParam String endDate) {
return R.ok(revAuditService.statsByDay(startDate, endDate));
}
@Operation(summary = "按操作类型统计")
@GetMapping("/stats/action")
public R<List<Map<String, Object>>> statsByAction(
@RequestParam String startDate,
@RequestParam String endDate) {
return R.ok(revAuditService.statsByAction(startDate, endDate));
}
}
@@ -0,0 +1,101 @@
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 AppAccessService {
private final JdbcTemplate jdbcTemplate;
/**
* 注册应用
*/
public Map<String, Object> registerApp(String appName, String redirectUris) {
String appId = "APP-" + System.currentTimeMillis();
String appSecret = UUID.randomUUID().toString().replace("-", "");
jdbcTemplate.update(
"INSERT INTO rev_app_registry (app_id, app_secret, app_name, redirect_uris, enabled, created_at) VALUES (?,?,?,?,?,?)",
appId, appSecret, appName, redirectUris, 1, new java.sql.Timestamp(System.currentTimeMillis())
);
log.info("App registered: {} ({})", appName, appId);
return Map.of(
"appId", appId,
"appSecret", appSecret,
"appName", appName,
"redirectUris", redirectUris
);
}
/**
* 启用/禁用应用
*/
public Map<String, Object> toggleApp(String appId, Boolean enabled) {
int updated = jdbcTemplate.update(
"UPDATE rev_app_registry SET enabled = ? WHERE app_id = ?",
enabled ? 1 : 0, appId
);
if (updated == 0) {
throw new RuntimeException("App not found: " + appId);
}
log.info("App {} {}", appId, enabled ? "enabled" : "disabled");
return Map.of("appId", appId, "enabled", enabled);
}
/**
* 查询应用列表
*/
public List<Map<String, Object>> listApps(String keyword, Integer page, Integer size) {
StringBuilder sql = new StringBuilder("SELECT * FROM rev_app_registry WHERE 1=1");
List<Object> params = new ArrayList<>();
if (keyword != null && !keyword.isEmpty()) {
sql.append(" AND (app_name LIKE ? OR app_id LIKE ?)");
params.add("%" + keyword + "%");
params.add("%" + keyword + "%");
}
sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
params.add(size);
params.add((page - 1) * size);
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
}
/**
* 查询应用详情
*/
public Map<String, Object> getAppDetail(String appId) {
List<Map<String, Object>> apps = jdbcTemplate.queryForList(
"SELECT * FROM rev_app_registry WHERE app_id = ?",
appId
);
if (apps.isEmpty()) {
throw new RuntimeException("App not found: " + appId);
}
return apps.get(0);
}
/**
* 删除应用
*/
public void deleteApp(String appId) {
int deleted = jdbcTemplate.update("DELETE FROM rev_app_registry WHERE app_id = ?", appId);
if (deleted == 0) {
throw new RuntimeException("App not found: " + appId);
}
log.info("App deleted: {}", appId);
}
}
@@ -0,0 +1,104 @@
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 InstallReportService {
private final JdbcTemplate jdbcTemplate;
/**
* 报装周期分析(平均天数、各环节耗时)
*/
public Map<String, Object> cycleAnalysis(String startDate, String endDate) {
// 总体平均完成天数
Double avgTotalDays = jdbcTemplate.queryForObject(
"SELECT AVG(DATEDIFF(completed_at, created_at)) FROM rev_installation " +
"WHERE status = 'completed' AND created_at >= ? AND created_at <= ?",
Double.class,
startDate + " 00:00:00", endDate + " 23:59:59"
);
// 各环节平均耗时(预受理→派单)
Double avgPreAcceptToDispatch = jdbcTemplate.queryForObject(
"SELECT AVG(DATEDIFF(dispatched_at, created_at)) FROM rev_installation " +
"WHERE dispatched_at IS NOT NULL AND created_at >= ? AND created_at <= ?",
Double.class,
startDate + " 00:00:00", endDate + " 23:59:59"
);
// 各环节平均耗时(派单→施工)
Double avgDispatchToConstruction = jdbcTemplate.queryForObject(
"SELECT AVG(DATEDIFF(construction_started_at, dispatched_at)) FROM rev_installation " +
"WHERE construction_started_at IS NOT NULL AND dispatched_at IS NOT NULL " +
"AND created_at >= ? AND created_at <= ?",
Double.class,
startDate + " 00:00:00", endDate + " 23:59:59"
);
// 各环节平均耗时(施工→竣工)
Double avgConstructionToComplete = jdbcTemplate.queryForObject(
"SELECT AVG(DATEDIFF(completed_at, construction_started_at)) FROM rev_installation " +
"WHERE completed_at IS NOT NULL AND construction_started_at IS NOT NULL " +
"AND created_at >= ? AND created_at <= ?",
Double.class,
startDate + " 00:00:00", endDate + " 23:59:59"
);
return Map.of(
"avgTotalDays", avgTotalDays != null ? Math.round(avgTotalDays * 100.0) / 100.0 : 0,
"avgPreAcceptToDispatch", avgPreAcceptToDispatch != null ? Math.round(avgPreAcceptToDispatch * 100.0) / 100.0 : 0,
"avgDispatchToConstruction", avgDispatchToConstruction != null ? Math.round(avgDispatchToConstruction * 100.0) / 100.0 : 0,
"avgConstructionToComplete", avgConstructionToComplete != null ? Math.round(avgConstructionToComplete * 100.0) / 100.0 : 0
);
}
/**
* 区域分布统计
*/
public List<Map<String, Object>> areaDistribution(String startDate, String endDate) {
return jdbcTemplate.queryForList(
"SELECT area, COUNT(*) as total, " +
"SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, " +
"AVG(CASE WHEN status = 'completed' THEN DATEDIFF(completed_at, created_at) ELSE NULL END) as avg_days " +
"FROM rev_installation " +
"WHERE created_at >= ? AND created_at <= ? " +
"GROUP BY area ORDER BY total DESC",
startDate + " 00:00:00", endDate + " 23:59:59"
);
}
/**
* 月度趋势
*/
public List<Map<String, Object>> monthlyTrend(String year) {
return jdbcTemplate.queryForList(
"SELECT DATE_FORMAT(created_at, '%Y-%m') as month, " +
"COUNT(*) as total, " +
"SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, " +
"SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled " +
"FROM rev_installation WHERE YEAR(created_at) = ? " +
"GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY month",
year
);
}
/**
* 客户类型分布
*/
public List<Map<String, Object>> customerTypeDistribution(String startDate, String endDate) {
return jdbcTemplate.queryForList(
"SELECT customer_type, COUNT(*) as count FROM rev_installation " +
"WHERE created_at >= ? AND created_at <= ? " +
"GROUP BY customer_type ORDER BY count DESC",
startDate + " 00:00:00", endDate + " 23:59:59"
);
}
}
@@ -0,0 +1,112 @@
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.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class InstallationOverviewService {
private final JdbcTemplate jdbcTemplate;
/**
* 首页概览数据
*/
public Map<String, Object> getOverview() {
// 总数
Integer total = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_installation", Integer.class
);
// 待处理
Integer pending = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_installation WHERE status = 'pending'", Integer.class
);
// 进行中
Integer inProgress = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_installation WHERE status IN ('dispatched', 'in_progress')", Integer.class
);
// 本月完成
String currentMonth = YearMonth.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
Integer completedThisMonth = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_installation WHERE status = 'completed' AND DATE_FORMAT(completed_at, '%Y-%m') = ?",
Integer.class, currentMonth
);
// 平均完成天数
Double avgDays = jdbcTemplate.queryForObject(
"SELECT AVG(DATEDIFF(completed_at, created_at)) FROM rev_installation WHERE status = 'completed' AND completed_at IS NOT NULL",
Double.class
);
return Map.of(
"total", total != null ? total : 0,
"pending", pending != null ? pending : 0,
"inProgress", inProgress != null ? inProgress : 0,
"completedThisMonth", completedThisMonth != null ? completedThisMonth : 0,
"avgDays", avgDays != null ? Math.round(avgDays * 100.0) / 100.0 : 0
);
}
/**
* 按月统计
*/
public List<Map<String, Object>> statsByMonth(String year) {
return jdbcTemplate.queryForList(
"SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count, " +
"SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed " +
"FROM rev_installation WHERE YEAR(created_at) = ? " +
"GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY month",
year
);
}
/**
* 按区域统计
*/
public List<Map<String, Object>> statsByArea() {
return jdbcTemplate.queryForList(
"SELECT area, COUNT(*) as total, " +
"SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, " +
"SUM(CASE WHEN status IN ('pending', 'dispatched', 'in_progress') THEN 1 ELSE 0 END) as in_progress " +
"FROM rev_installation GROUP BY area ORDER BY total DESC"
);
}
/**
* 转化率分析
*/
public Map<String, Object> conversionAnalysis() {
Integer total = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_installation", Integer.class
);
Integer completed = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_installation WHERE status = 'completed'", Integer.class
);
Integer cancelled = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM rev_installation WHERE status = 'cancelled'", Integer.class
);
double conversionRate = total != null && total > 0 && completed != null ?
(completed * 100.0 / total) : 0;
double cancelRate = total != null && total > 0 && cancelled != null ?
(cancelled * 100.0 / total) : 0;
return Map.of(
"total", total != null ? total : 0,
"completed", completed != null ? completed : 0,
"cancelled", cancelled != null ? cancelled : 0,
"conversionRate", Math.round(conversionRate * 100.0) / 100.0,
"cancelRate", Math.round(cancelRate * 100.0) / 100.0
);
}
}
@@ -0,0 +1,117 @@
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 InstallationQueryService {
private final JdbcTemplate jdbcTemplate;
/**
* 综合查询(多条件组合)
*/
public Map<String, Object> query(String startDate, String endDate, String status, String area,
String customerType, String keyword, Integer page, Integer size) {
StringBuilder sql = new StringBuilder("SELECT * FROM rev_installation WHERE 1=1");
StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM rev_installation WHERE 1=1");
List<Object> params = new ArrayList<>();
List<Object> countParams = new ArrayList<>();
if (startDate != null && !startDate.isEmpty()) {
sql.append(" AND created_at >= ?");
countSql.append(" AND created_at >= ?");
params.add(startDate + " 00:00:00");
countParams.add(startDate + " 00:00:00");
}
if (endDate != null && !endDate.isEmpty()) {
sql.append(" AND created_at <= ?");
countSql.append(" AND created_at <= ?");
params.add(endDate + " 23:59:59");
countParams.add(endDate + " 23:59:59");
}
if (status != null && !status.isEmpty()) {
sql.append(" AND status = ?");
countSql.append(" AND status = ?");
params.add(status);
countParams.add(status);
}
if (area != null && !area.isEmpty()) {
sql.append(" AND area = ?");
countSql.append(" AND area = ?");
params.add(area);
countParams.add(area);
}
if (customerType != null && !customerType.isEmpty()) {
sql.append(" AND customer_type = ?");
countSql.append(" AND customer_type = ?");
params.add(customerType);
countParams.add(customerType);
}
if (keyword != null && !keyword.isEmpty()) {
sql.append(" AND (apply_no LIKE ? OR applicant_name LIKE ? OR address LIKE ?)");
countSql.append(" AND (apply_no LIKE ? OR applicant_name LIKE ? OR address LIKE ?)");
params.add("%" + keyword + "%");
params.add("%" + keyword + "%");
params.add("%" + keyword + "%");
countParams.add("%" + keyword + "%");
countParams.add("%" + keyword + "%");
countParams.add("%" + keyword + "%");
}
Integer total = jdbcTemplate.queryForObject(countSql.toString(), Integer.class, countParams.toArray());
sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
params.add(size);
params.add((page - 1) * size);
List<Map<String, Object>> data = jdbcTemplate.queryForList(sql.toString(), params.toArray());
return Map.of(
"total", total != null ? total : 0,
"page", page,
"size", size,
"data", data
);
}
/**
* 导出CSV(返回数据列表)
*/
public List<Map<String, Object>> exportCsv(String startDate, String endDate, String status,
String area, String customerType) {
StringBuilder sql = new StringBuilder("SELECT * FROM rev_installation WHERE 1=1");
List<Object> params = new ArrayList<>();
if (startDate != null && !startDate.isEmpty()) {
sql.append(" AND created_at >= ?");
params.add(startDate + " 00:00:00");
}
if (endDate != null && !endDate.isEmpty()) {
sql.append(" AND created_at <= ?");
params.add(endDate + " 23:59:59");
}
if (status != null && !status.isEmpty()) {
sql.append(" AND status = ?");
params.add(status);
}
if (area != null && !area.isEmpty()) {
sql.append(" AND area = ?");
params.add(area);
}
if (customerType != null && !customerType.isEmpty()) {
sql.append(" AND customer_type = ?");
params.add(customerType);
}
sql.append(" ORDER BY created_at DESC LIMIT 10000");
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
}
}
@@ -0,0 +1,132 @@
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 InstallationTaskService {
private final JdbcTemplate jdbcTemplate;
/**
* 创建任务
*/
public Map<String, Object> createTask(String applyNo, String taskType, Long assigneeId, String assigneeName, String description) {
String taskId = "TASK-" + System.currentTimeMillis();
jdbcTemplate.update(
"INSERT INTO rev_install_task (task_id, apply_no, task_type, assignee_id, assignee_name, description, status, created_at) " +
"VALUES (?,?,?,?,?,?,?,?)",
taskId, applyNo, taskType, assigneeId, assigneeName, description, "pending",
new java.sql.Timestamp(System.currentTimeMillis())
);
log.info("Task created: {} for {}", taskId, applyNo);
return Map.of(
"taskId", taskId,
"applyNo", applyNo,
"taskType", taskType,
"assigneeName", assigneeName
);
}
/**
* 更新任务状态
*/
public Map<String, Object> updateTaskStatus(String taskId, String status, String remark) {
StringBuilder sql = new StringBuilder("UPDATE rev_install_task SET status = ?");
List<Object> params = new ArrayList<>();
params.add(status);
if ("completed".equals(status)) {
sql.append(", completed_at = ?");
params.add(new java.sql.Timestamp(System.currentTimeMillis()));
}
if (remark != null && !remark.isEmpty()) {
sql.append(", remark = ?");
params.add(remark);
}
sql.append(" WHERE task_id = ?");
params.add(taskId);
int updated = jdbcTemplate.update(sql.toString(), params.toArray());
if (updated == 0) {
throw new RuntimeException("Task not found: " + taskId);
}
log.info("Task {} status updated to {}", taskId, status);
return Map.of("taskId", taskId, "status", status);
}
/**
* 查询任务列表
*/
public List<Map<String, Object>> listTasks(String applyNo, String assigneeName, String status, Integer page, Integer size) {
StringBuilder sql = new StringBuilder("SELECT * FROM rev_install_task WHERE 1=1");
List<Object> params = new ArrayList<>();
if (applyNo != null && !applyNo.isEmpty()) {
sql.append(" AND apply_no = ?");
params.add(applyNo);
}
if (assigneeName != null && !assigneeName.isEmpty()) {
sql.append(" AND assignee_name LIKE ?");
params.add("%" + assigneeName + "%");
}
if (status != null && !status.isEmpty()) {
sql.append(" AND status = ?");
params.add(status);
}
sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
params.add(size);
params.add((page - 1) * size);
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
}
/**
* 任务看板(按状态分组)
*/
public Map<String, Object> kanban() {
List<Map<String, Object>> pending = jdbcTemplate.queryForList(
"SELECT * FROM rev_install_task WHERE status = 'pending' ORDER BY created_at"
);
List<Map<String, Object>> inProgress = jdbcTemplate.queryForList(
"SELECT * FROM rev_install_task WHERE status = 'in_progress' ORDER BY created_at"
);
List<Map<String, Object>> completed = jdbcTemplate.queryForList(
"SELECT * FROM rev_install_task WHERE status = 'completed' ORDER BY completed_at DESC LIMIT 50"
);
return Map.of(
"pending", pending,
"inProgress", inProgress,
"completed", completed
);
}
/**
* 任务详情
*/
public Map<String, Object> getTaskDetail(String taskId) {
List<Map<String, Object>> tasks = jdbcTemplate.queryForList(
"SELECT * FROM rev_install_task WHERE task_id = ?",
taskId
);
if (tasks.isEmpty()) {
throw new RuntimeException("Task not found: " + taskId);
}
return tasks.get(0);
}
}
@@ -0,0 +1,112 @@
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 RevAuditService {
private final JdbcTemplate jdbcTemplate;
/**
* 记录审计日志
*/
public void log(String userId, String userName, String action, String targetType, String targetId, String detail, String ip) {
jdbcTemplate.update(
"INSERT INTO rev_audit_log (user_id, user_name, action, target_type, target_id, detail, ip, created_at) VALUES (?,?,?,?,?,?,?,?)",
userId, userName, action, targetType, targetId, detail, ip, new java.sql.Timestamp(System.currentTimeMillis())
);
log.debug("Audit log: {} {} {} {}", userName, action, targetType, targetId);
}
/**
* 查询审计日志(分页+过滤)
*/
public Map<String, Object> queryLogs(String userId, String action, String startDate, String endDate, Integer page, Integer size) {
StringBuilder sql = new StringBuilder("SELECT * FROM rev_audit_log WHERE 1=1");
List<Object> params = new ArrayList<>();
if (userId != null && !userId.isEmpty()) {
sql.append(" AND user_id = ?");
params.add(userId);
}
if (action != null && !action.isEmpty()) {
sql.append(" AND action = ?");
params.add(action);
}
if (startDate != null && !startDate.isEmpty()) {
sql.append(" AND created_at >= ?");
params.add(startDate + " 00:00:00");
}
if (endDate != null && !endDate.isEmpty()) {
sql.append(" AND created_at <= ?");
params.add(endDate + " 23:59:59");
}
sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
params.add(size);
params.add((page - 1) * size);
List<Map<String, Object>> logs = jdbcTemplate.queryForList(sql.toString(), params.toArray());
// 查询总数
StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM rev_audit_log WHERE 1=1");
List<Object> countParams = new ArrayList<>();
if (userId != null && !userId.isEmpty()) {
countSql.append(" AND user_id = ?");
countParams.add(userId);
}
if (action != null && !action.isEmpty()) {
countSql.append(" AND action = ?");
countParams.add(action);
}
if (startDate != null && !startDate.isEmpty()) {
countSql.append(" AND created_at >= ?");
countParams.add(startDate + " 00:00:00");
}
if (endDate != null && !endDate.isEmpty()) {
countSql.append(" AND created_at <= ?");
countParams.add(endDate + " 23:59:59");
}
Integer total = jdbcTemplate.queryForObject(countSql.toString(), Integer.class, countParams.toArray());
return Map.of(
"total", total != null ? total : 0,
"page", page,
"size", size,
"data", logs
);
}
/**
* 审计统计(按天)
*/
public List<Map<String, Object>> statsByDay(String startDate, String endDate) {
return jdbcTemplate.queryForList(
"SELECT DATE(created_at) as day, COUNT(*) as count FROM rev_audit_log " +
"WHERE created_at >= ? AND created_at <= ? " +
"GROUP BY DATE(created_at) ORDER BY day DESC",
startDate + " 00:00:00", endDate + " 23:59:59"
);
}
/**
* 审计统计(按操作类型)
*/
public List<Map<String, Object>> statsByAction(String startDate, String endDate) {
return jdbcTemplate.queryForList(
"SELECT action, COUNT(*) as count FROM rev_audit_log " +
"WHERE created_at >= ? AND created_at <= ? " +
"GROUP BY action ORDER BY count DESC",
startDate + " 00:00:00", endDate + " 23:59:59"
);
}
}