From c4b607611d00bd32885e8805b885bcfc20320cfb Mon Sep 17 00:00:00 2001 From: bot_dev2 Date: Mon, 15 Jun 2026 10:32:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(wm-revenue):=20#6=20=E8=90=A5=E6=94=B6?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=B9=B3=E5=8F=B0+=E6=8A=A5=E8=A3=85?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F=E5=A2=9E=E5=BC=BA=EF=BC=88?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1+=E5=BA=94=E7=94=A8=E6=8E=A5=E5=85=A5+?= =?UTF-8?q?=E6=8A=A5=E8=A3=85=E6=A6=82=E8=A7=88/=E4=BB=BB=E5=8A=A1/?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2/=E6=8A=A5=E8=A1=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/postgresql/V4__issue_6_enhancements.sql | 64 +++++++++ .../controller/AppAccessController.java | 59 ++++++++ .../controller/InstallReportController.java | 50 +++++++ .../InstallationOverviewController.java | 44 ++++++ .../InstallationQueryController.java | 45 ++++++ .../InstallationTaskController.java | 64 +++++++++ .../controller/RevAuditController.java | 48 +++++++ .../revenue/service/AppAccessService.java | 101 ++++++++++++++ .../revenue/service/InstallReportService.java | 104 ++++++++++++++ .../service/InstallationOverviewService.java | 112 +++++++++++++++ .../service/InstallationQueryService.java | 117 ++++++++++++++++ .../service/InstallationTaskService.java | 132 ++++++++++++++++++ .../revenue/service/RevAuditService.java | 112 +++++++++++++++ 13 files changed, 1052 insertions(+) create mode 100644 db/postgresql/V4__issue_6_enhancements.sql create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/AppAccessController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/InstallReportController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/InstallationOverviewController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/InstallationQueryController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/InstallationTaskController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/controller/RevAuditController.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/AppAccessService.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/InstallReportService.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/InstallationOverviewService.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/InstallationQueryService.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/InstallationTaskService.java create mode 100644 wm-revenue/src/main/java/com/water/revenue/service/RevAuditService.java diff --git a/db/postgresql/V4__issue_6_enhancements.sql b/db/postgresql/V4__issue_6_enhancements.sql new file mode 100644 index 00000000..714f43f1 --- /dev/null +++ b/db/postgresql/V4__issue_6_enhancements.sql @@ -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 '客户类型'; diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/AppAccessController.java b/wm-revenue/src/main/java/com/water/revenue/controller/AppAccessController.java new file mode 100644 index 00000000..5715f580 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/AppAccessController.java @@ -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> registerApp(@RequestBody Map 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> toggleApp( + @PathVariable String appId, + @RequestBody Map request) { + Boolean enabled = (Boolean) request.get("enabled"); + return R.ok(appAccessService.toggleApp(appId, enabled)); + } + + @Operation(summary = "应用列表") + @GetMapping + public R>> 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> getAppDetail(@PathVariable String appId) { + return R.ok(appAccessService.getAppDetail(appId)); + } + + @Operation(summary = "删除应用") + @DeleteMapping("/{appId}") + public R deleteApp(@PathVariable String appId) { + appAccessService.deleteApp(appId); + return R.ok(); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/InstallReportController.java b/wm-revenue/src/main/java/com/water/revenue/controller/InstallReportController.java new file mode 100644 index 00000000..be041c7d --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/InstallReportController.java @@ -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> cycleAnalysis( + @RequestParam String startDate, + @RequestParam String endDate) { + return R.ok(installReportService.cycleAnalysis(startDate, endDate)); + } + + @Operation(summary = "区域分布统计") + @GetMapping("/area") + public R>> areaDistribution( + @RequestParam String startDate, + @RequestParam String endDate) { + return R.ok(installReportService.areaDistribution(startDate, endDate)); + } + + @Operation(summary = "月度趋势") + @GetMapping("/monthly") + public R>> monthlyTrend(@RequestParam(defaultValue = "2026") String year) { + return R.ok(installReportService.monthlyTrend(year)); + } + + @Operation(summary = "客户类型分布") + @GetMapping("/customer-type") + public R>> customerTypeDistribution( + @RequestParam String startDate, + @RequestParam String endDate) { + return R.ok(installReportService.customerTypeDistribution(startDate, endDate)); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/InstallationOverviewController.java b/wm-revenue/src/main/java/com/water/revenue/controller/InstallationOverviewController.java new file mode 100644 index 00000000..f06dc1be --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/InstallationOverviewController.java @@ -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> getOverview() { + return R.ok(installationOverviewService.getOverview()); + } + + @Operation(summary = "按月统计") + @GetMapping("/stats/month") + public R>> statsByMonth(@RequestParam(defaultValue = "2026") String year) { + return R.ok(installationOverviewService.statsByMonth(year)); + } + + @Operation(summary = "按区域统计") + @GetMapping("/stats/area") + public R>> statsByArea() { + return R.ok(installationOverviewService.statsByArea()); + } + + @Operation(summary = "转化率分析") + @GetMapping("/conversion") + public R> conversionAnalysis() { + return R.ok(installationOverviewService.conversionAnalysis()); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/InstallationQueryController.java b/wm-revenue/src/main/java/com/water/revenue/controller/InstallationQueryController.java new file mode 100644 index 00000000..3e0814a8 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/InstallationQueryController.java @@ -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> 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>> 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)); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/InstallationTaskController.java b/wm-revenue/src/main/java/com/water/revenue/controller/InstallationTaskController.java new file mode 100644 index 00000000..99c1087a --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/InstallationTaskController.java @@ -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> createTask(@RequestBody Map 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> updateTaskStatus( + @PathVariable String taskId, + @RequestBody Map 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>> 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> kanban() { + return R.ok(installationTaskService.kanban()); + } + + @Operation(summary = "任务详情") + @GetMapping("/{taskId}") + public R> getTaskDetail(@PathVariable String taskId) { + return R.ok(installationTaskService.getTaskDetail(taskId)); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/controller/RevAuditController.java b/wm-revenue/src/main/java/com/water/revenue/controller/RevAuditController.java new file mode 100644 index 00000000..4f176864 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/controller/RevAuditController.java @@ -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> 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>> statsByDay( + @RequestParam String startDate, + @RequestParam String endDate) { + return R.ok(revAuditService.statsByDay(startDate, endDate)); + } + + @Operation(summary = "按操作类型统计") + @GetMapping("/stats/action") + public R>> statsByAction( + @RequestParam String startDate, + @RequestParam String endDate) { + return R.ok(revAuditService.statsByAction(startDate, endDate)); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/AppAccessService.java b/wm-revenue/src/main/java/com/water/revenue/service/AppAccessService.java new file mode 100644 index 00000000..b0bf0c31 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/AppAccessService.java @@ -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 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 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> listApps(String keyword, Integer page, Integer size) { + StringBuilder sql = new StringBuilder("SELECT * FROM rev_app_registry WHERE 1=1"); + List 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 getAppDetail(String appId) { + List> 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); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/InstallReportService.java b/wm-revenue/src/main/java/com/water/revenue/service/InstallReportService.java new file mode 100644 index 00000000..346dcb78 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/InstallReportService.java @@ -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 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> 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> 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> 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" + ); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/InstallationOverviewService.java b/wm-revenue/src/main/java/com/water/revenue/service/InstallationOverviewService.java new file mode 100644 index 00000000..fb1304a7 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/InstallationOverviewService.java @@ -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 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> 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> 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 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 + ); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/InstallationQueryService.java b/wm-revenue/src/main/java/com/water/revenue/service/InstallationQueryService.java new file mode 100644 index 00000000..027469f6 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/InstallationQueryService.java @@ -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 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 params = new ArrayList<>(); + List 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> data = jdbcTemplate.queryForList(sql.toString(), params.toArray()); + + return Map.of( + "total", total != null ? total : 0, + "page", page, + "size", size, + "data", data + ); + } + + /** + * 导出CSV(返回数据列表) + */ + public List> exportCsv(String startDate, String endDate, String status, + String area, String customerType) { + StringBuilder sql = new StringBuilder("SELECT * FROM rev_installation WHERE 1=1"); + List 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()); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/InstallationTaskService.java b/wm-revenue/src/main/java/com/water/revenue/service/InstallationTaskService.java new file mode 100644 index 00000000..6b04f964 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/InstallationTaskService.java @@ -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 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 updateTaskStatus(String taskId, String status, String remark) { + StringBuilder sql = new StringBuilder("UPDATE rev_install_task SET status = ?"); + List 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> listTasks(String applyNo, String assigneeName, String status, Integer page, Integer size) { + StringBuilder sql = new StringBuilder("SELECT * FROM rev_install_task WHERE 1=1"); + List 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 kanban() { + List> pending = jdbcTemplate.queryForList( + "SELECT * FROM rev_install_task WHERE status = 'pending' ORDER BY created_at" + ); + List> inProgress = jdbcTemplate.queryForList( + "SELECT * FROM rev_install_task WHERE status = 'in_progress' ORDER BY created_at" + ); + List> 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 getTaskDetail(String taskId) { + List> 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); + } +} diff --git a/wm-revenue/src/main/java/com/water/revenue/service/RevAuditService.java b/wm-revenue/src/main/java/com/water/revenue/service/RevAuditService.java new file mode 100644 index 00000000..5993e623 --- /dev/null +++ b/wm-revenue/src/main/java/com/water/revenue/service/RevAuditService.java @@ -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 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 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> logs = jdbcTemplate.queryForList(sql.toString(), params.toArray()); + + // 查询总数 + StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM rev_audit_log WHERE 1=1"); + List 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> 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> 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" + ); + } +}