Преглед изворни кода

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

bot_dev2 пре 4 дана
родитељ
комит
c4b607611d

+ 64
- 0
db/postgresql/V4__issue_6_enhancements.sql Прегледај датотеку

@@ -0,0 +1,64 @@
1
+-- =============================================
2
+-- 智慧水务管理系统 - 营收管理平台与报装管理系统增强
3
+-- 版本: V4 (Issue #6)
4
+-- =============================================
5
+
6
+-- 平台运维审计日志
7
+CREATE TABLE IF NOT EXISTS rev_audit_log (
8
+    id BIGSERIAL PRIMARY KEY,
9
+    user_id VARCHAR(50) NOT NULL,
10
+    user_name VARCHAR(100) NOT NULL,
11
+    action VARCHAR(50) NOT NULL,             -- CREATE/UPDATE/DELETE/LOGIN/EXPORT
12
+    target_type VARCHAR(50),                 -- customer/meter/bill/app
13
+    target_id VARCHAR(50),
14
+    detail TEXT,
15
+    ip VARCHAR(50),
16
+    created_at TIMESTAMP DEFAULT NOW()
17
+);
18
+COMMENT ON TABLE rev_audit_log IS '平台运维审计日志表';
19
+CREATE INDEX IF NOT EXISTS idx_audit_user ON rev_audit_log(user_id);
20
+CREATE INDEX IF NOT EXISTS idx_audit_action ON rev_audit_log(action);
21
+CREATE INDEX IF NOT EXISTS idx_audit_created ON rev_audit_log(created_at);
22
+
23
+-- 应用接入注册表
24
+CREATE TABLE IF NOT EXISTS rev_app_registry (
25
+    id BIGSERIAL PRIMARY KEY,
26
+    app_id VARCHAR(50) UNIQUE NOT NULL,
27
+    app_secret VARCHAR(100) NOT NULL,
28
+    app_name VARCHAR(100) NOT NULL,
29
+    redirect_uris TEXT,                      -- JSON array of redirect URIs
30
+    enabled SMALLINT DEFAULT 1,              -- 0:disabled 1:enabled
31
+    created_at TIMESTAMP DEFAULT NOW(),
32
+    updated_at TIMESTAMP DEFAULT NOW()
33
+);
34
+COMMENT ON TABLE rev_app_registry IS '应用接入注册表';
35
+CREATE INDEX IF NOT EXISTS idx_app_enabled ON rev_app_registry(enabled);
36
+
37
+-- 报装任务表
38
+CREATE TABLE IF NOT EXISTS rev_install_task (
39
+    id BIGSERIAL PRIMARY KEY,
40
+    task_id VARCHAR(50) UNIQUE NOT NULL,
41
+    apply_no VARCHAR(50) NOT NULL,
42
+    task_type VARCHAR(50) NOT NULL,          -- design/construction/inspection
43
+    assignee_id BIGINT,
44
+    assignee_name VARCHAR(100),
45
+    description TEXT,
46
+    status VARCHAR(20) DEFAULT 'pending',    -- pending/in_progress/completed/cancelled
47
+    remark TEXT,
48
+    completed_at TIMESTAMP,
49
+    created_at TIMESTAMP DEFAULT NOW(),
50
+    updated_at TIMESTAMP DEFAULT NOW()
51
+);
52
+COMMENT ON TABLE rev_install_task IS '报装任务表';
53
+CREATE INDEX IF NOT EXISTS idx_task_apply ON rev_install_task(apply_no);
54
+CREATE INDEX IF NOT EXISTS idx_task_assignee ON rev_install_task(assignee_id);
55
+CREATE INDEX IF NOT EXISTS idx_task_status ON rev_install_task(status);
56
+
57
+-- 增强报装表(添加缺失的时间戳字段)
58
+ALTER TABLE rev_installation ADD COLUMN IF NOT EXISTS dispatched_at TIMESTAMP;
59
+ALTER TABLE rev_installation ADD COLUMN IF NOT EXISTS construction_started_at TIMESTAMP;
60
+ALTER TABLE rev_installation ADD COLUMN IF NOT EXISTS customer_type VARCHAR(20);
61
+
62
+COMMENT ON COLUMN rev_installation.dispatched_at IS '派单时间';
63
+COMMENT ON COLUMN rev_installation.construction_started_at IS '施工开始时间';
64
+COMMENT ON COLUMN rev_installation.customer_type IS '客户类型';

+ 59
- 0
wm-revenue/src/main/java/com/water/revenue/controller/AppAccessController.java Прегледај датотеку

@@ -0,0 +1,59 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.AppAccessService;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.tags.Tag;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+@Tag(name = "应用接入管理")
14
+@RestController
15
+@RequestMapping("/revenue/apps")
16
+@RequiredArgsConstructor
17
+public class AppAccessController {
18
+
19
+    private final AppAccessService appAccessService;
20
+
21
+    @Operation(summary = "注册应用")
22
+    @PostMapping
23
+    public R<Map<String, Object>> registerApp(@RequestBody Map<String, Object> request) {
24
+        String appName = (String) request.get("appName");
25
+        String redirectUris = (String) request.get("redirectUris");
26
+        return R.ok(appAccessService.registerApp(appName, redirectUris));
27
+    }
28
+
29
+    @Operation(summary = "启用/禁用应用")
30
+    @PutMapping("/{appId}/toggle")
31
+    public R<Map<String, Object>> toggleApp(
32
+            @PathVariable String appId,
33
+            @RequestBody Map<String, Object> request) {
34
+        Boolean enabled = (Boolean) request.get("enabled");
35
+        return R.ok(appAccessService.toggleApp(appId, enabled));
36
+    }
37
+
38
+    @Operation(summary = "应用列表")
39
+    @GetMapping
40
+    public R<List<Map<String, Object>>> listApps(
41
+            @RequestParam(required = false) String keyword,
42
+            @RequestParam(defaultValue = "1") Integer page,
43
+            @RequestParam(defaultValue = "10") Integer size) {
44
+        return R.ok(appAccessService.listApps(keyword, page, size));
45
+    }
46
+
47
+    @Operation(summary = "应用详情")
48
+    @GetMapping("/{appId}")
49
+    public R<Map<String, Object>> getAppDetail(@PathVariable String appId) {
50
+        return R.ok(appAccessService.getAppDetail(appId));
51
+    }
52
+
53
+    @Operation(summary = "删除应用")
54
+    @DeleteMapping("/{appId}")
55
+    public R<Void> deleteApp(@PathVariable String appId) {
56
+        appAccessService.deleteApp(appId);
57
+        return R.ok();
58
+    }
59
+}

+ 50
- 0
wm-revenue/src/main/java/com/water/revenue/controller/InstallReportController.java Прегледај датотеку

@@ -0,0 +1,50 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.InstallReportService;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.tags.Tag;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+@Tag(name = "报装报表分析")
14
+@RestController
15
+@RequestMapping("/revenue/install/reports")
16
+@RequiredArgsConstructor
17
+public class InstallReportController {
18
+
19
+    private final InstallReportService installReportService;
20
+
21
+    @Operation(summary = "报装周期分析")
22
+    @GetMapping("/cycle")
23
+    public R<Map<String, Object>> cycleAnalysis(
24
+            @RequestParam String startDate,
25
+            @RequestParam String endDate) {
26
+        return R.ok(installReportService.cycleAnalysis(startDate, endDate));
27
+    }
28
+
29
+    @Operation(summary = "区域分布统计")
30
+    @GetMapping("/area")
31
+    public R<List<Map<String, Object>>> areaDistribution(
32
+            @RequestParam String startDate,
33
+            @RequestParam String endDate) {
34
+        return R.ok(installReportService.areaDistribution(startDate, endDate));
35
+    }
36
+
37
+    @Operation(summary = "月度趋势")
38
+    @GetMapping("/monthly")
39
+    public R<List<Map<String, Object>>> monthlyTrend(@RequestParam(defaultValue = "2026") String year) {
40
+        return R.ok(installReportService.monthlyTrend(year));
41
+    }
42
+
43
+    @Operation(summary = "客户类型分布")
44
+    @GetMapping("/customer-type")
45
+    public R<List<Map<String, Object>>> customerTypeDistribution(
46
+            @RequestParam String startDate,
47
+            @RequestParam String endDate) {
48
+        return R.ok(installReportService.customerTypeDistribution(startDate, endDate));
49
+    }
50
+}

+ 44
- 0
wm-revenue/src/main/java/com/water/revenue/controller/InstallationOverviewController.java Прегледај датотеку

@@ -0,0 +1,44 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.InstallationOverviewService;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.tags.Tag;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+@Tag(name = "报装首页概览")
14
+@RestController
15
+@RequestMapping("/revenue/install/overview")
16
+@RequiredArgsConstructor
17
+public class InstallationOverviewController {
18
+
19
+    private final InstallationOverviewService installationOverviewService;
20
+
21
+    @Operation(summary = "概览数据")
22
+    @GetMapping
23
+    public R<Map<String, Object>> getOverview() {
24
+        return R.ok(installationOverviewService.getOverview());
25
+    }
26
+
27
+    @Operation(summary = "按月统计")
28
+    @GetMapping("/stats/month")
29
+    public R<List<Map<String, Object>>> statsByMonth(@RequestParam(defaultValue = "2026") String year) {
30
+        return R.ok(installationOverviewService.statsByMonth(year));
31
+    }
32
+
33
+    @Operation(summary = "按区域统计")
34
+    @GetMapping("/stats/area")
35
+    public R<List<Map<String, Object>>> statsByArea() {
36
+        return R.ok(installationOverviewService.statsByArea());
37
+    }
38
+
39
+    @Operation(summary = "转化率分析")
40
+    @GetMapping("/conversion")
41
+    public R<Map<String, Object>> conversionAnalysis() {
42
+        return R.ok(installationOverviewService.conversionAnalysis());
43
+    }
44
+}

+ 45
- 0
wm-revenue/src/main/java/com/water/revenue/controller/InstallationQueryController.java Прегледај датотеку

@@ -0,0 +1,45 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.InstallationQueryService;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.tags.Tag;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+@Tag(name = "报装综合查询")
14
+@RestController
15
+@RequestMapping("/revenue/install/query")
16
+@RequiredArgsConstructor
17
+public class InstallationQueryController {
18
+
19
+    private final InstallationQueryService installationQueryService;
20
+
21
+    @Operation(summary = "综合查询")
22
+    @GetMapping
23
+    public R<Map<String, Object>> query(
24
+            @RequestParam(required = false) String startDate,
25
+            @RequestParam(required = false) String endDate,
26
+            @RequestParam(required = false) String status,
27
+            @RequestParam(required = false) String area,
28
+            @RequestParam(required = false) String customerType,
29
+            @RequestParam(required = false) String keyword,
30
+            @RequestParam(defaultValue = "1") Integer page,
31
+            @RequestParam(defaultValue = "10") Integer size) {
32
+        return R.ok(installationQueryService.query(startDate, endDate, status, area, customerType, keyword, page, size));
33
+    }
34
+
35
+    @Operation(summary = "导出CSV")
36
+    @GetMapping("/export")
37
+    public R<List<Map<String, Object>>> exportCsv(
38
+            @RequestParam(required = false) String startDate,
39
+            @RequestParam(required = false) String endDate,
40
+            @RequestParam(required = false) String status,
41
+            @RequestParam(required = false) String area,
42
+            @RequestParam(required = false) String customerType) {
43
+        return R.ok(installationQueryService.exportCsv(startDate, endDate, status, area, customerType));
44
+    }
45
+}

+ 64
- 0
wm-revenue/src/main/java/com/water/revenue/controller/InstallationTaskController.java Прегледај датотеку

@@ -0,0 +1,64 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.InstallationTaskService;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.tags.Tag;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+@Tag(name = "报装任务管理")
14
+@RestController
15
+@RequestMapping("/revenue/install/tasks")
16
+@RequiredArgsConstructor
17
+public class InstallationTaskController {
18
+
19
+    private final InstallationTaskService installationTaskService;
20
+
21
+    @Operation(summary = "创建任务")
22
+    @PostMapping
23
+    public R<Map<String, Object>> createTask(@RequestBody Map<String, Object> request) {
24
+        String applyNo = (String) request.get("applyNo");
25
+        String taskType = (String) request.get("taskType");
26
+        Long assigneeId = Long.parseLong(String.valueOf(request.get("assigneeId")));
27
+        String assigneeName = (String) request.get("assigneeName");
28
+        String description = (String) request.get("description");
29
+        return R.ok(installationTaskService.createTask(applyNo, taskType, assigneeId, assigneeName, description));
30
+    }
31
+
32
+    @Operation(summary = "更新任务状态")
33
+    @PutMapping("/{taskId}/status")
34
+    public R<Map<String, Object>> updateTaskStatus(
35
+            @PathVariable String taskId,
36
+            @RequestBody Map<String, Object> request) {
37
+        String status = (String) request.get("status");
38
+        String remark = (String) request.get("remark");
39
+        return R.ok(installationTaskService.updateTaskStatus(taskId, status, remark));
40
+    }
41
+
42
+    @Operation(summary = "任务列表")
43
+    @GetMapping
44
+    public R<List<Map<String, Object>>> listTasks(
45
+            @RequestParam(required = false) String applyNo,
46
+            @RequestParam(required = false) String assigneeName,
47
+            @RequestParam(required = false) String status,
48
+            @RequestParam(defaultValue = "1") Integer page,
49
+            @RequestParam(defaultValue = "10") Integer size) {
50
+        return R.ok(installationTaskService.listTasks(applyNo, assigneeName, status, page, size));
51
+    }
52
+
53
+    @Operation(summary = "任务看板")
54
+    @GetMapping("/kanban")
55
+    public R<Map<String, Object>> kanban() {
56
+        return R.ok(installationTaskService.kanban());
57
+    }
58
+
59
+    @Operation(summary = "任务详情")
60
+    @GetMapping("/{taskId}")
61
+    public R<Map<String, Object>> getTaskDetail(@PathVariable String taskId) {
62
+        return R.ok(installationTaskService.getTaskDetail(taskId));
63
+    }
64
+}

+ 48
- 0
wm-revenue/src/main/java/com/water/revenue/controller/RevAuditController.java Прегледај датотеку

@@ -0,0 +1,48 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.RevAuditService;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.tags.Tag;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+@Tag(name = "平台运维审计")
14
+@RestController
15
+@RequestMapping("/revenue/audit")
16
+@RequiredArgsConstructor
17
+public class RevAuditController {
18
+
19
+    private final RevAuditService revAuditService;
20
+
21
+    @Operation(summary = "查询审计日志")
22
+    @GetMapping("/logs")
23
+    public R<Map<String, Object>> queryLogs(
24
+            @RequestParam(required = false) String userId,
25
+            @RequestParam(required = false) String action,
26
+            @RequestParam(required = false) String startDate,
27
+            @RequestParam(required = false) String endDate,
28
+            @RequestParam(defaultValue = "1") Integer page,
29
+            @RequestParam(defaultValue = "10") Integer size) {
30
+        return R.ok(revAuditService.queryLogs(userId, action, startDate, endDate, page, size));
31
+    }
32
+
33
+    @Operation(summary = "按天统计")
34
+    @GetMapping("/stats/day")
35
+    public R<List<Map<String, Object>>> statsByDay(
36
+            @RequestParam String startDate,
37
+            @RequestParam String endDate) {
38
+        return R.ok(revAuditService.statsByDay(startDate, endDate));
39
+    }
40
+
41
+    @Operation(summary = "按操作类型统计")
42
+    @GetMapping("/stats/action")
43
+    public R<List<Map<String, Object>>> statsByAction(
44
+            @RequestParam String startDate,
45
+            @RequestParam String endDate) {
46
+        return R.ok(revAuditService.statsByAction(startDate, endDate));
47
+    }
48
+}

+ 101
- 0
wm-revenue/src/main/java/com/water/revenue/service/AppAccessService.java Прегледај датотеку

@@ -0,0 +1,101 @@
1
+package com.water.revenue.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.util.*;
9
+
10
+@Slf4j
11
+@Service
12
+@RequiredArgsConstructor
13
+public class AppAccessService {
14
+
15
+    private final JdbcTemplate jdbcTemplate;
16
+
17
+    /**
18
+     * 注册应用
19
+     */
20
+    public Map<String, Object> registerApp(String appName, String redirectUris) {
21
+        String appId = "APP-" + System.currentTimeMillis();
22
+        String appSecret = UUID.randomUUID().toString().replace("-", "");
23
+        
24
+        jdbcTemplate.update(
25
+            "INSERT INTO rev_app_registry (app_id, app_secret, app_name, redirect_uris, enabled, created_at) VALUES (?,?,?,?,?,?)",
26
+            appId, appSecret, appName, redirectUris, 1, new java.sql.Timestamp(System.currentTimeMillis())
27
+        );
28
+        
29
+        log.info("App registered: {} ({})", appName, appId);
30
+        return Map.of(
31
+            "appId", appId,
32
+            "appSecret", appSecret,
33
+            "appName", appName,
34
+            "redirectUris", redirectUris
35
+        );
36
+    }
37
+
38
+    /**
39
+     * 启用/禁用应用
40
+     */
41
+    public Map<String, Object> toggleApp(String appId, Boolean enabled) {
42
+        int updated = jdbcTemplate.update(
43
+            "UPDATE rev_app_registry SET enabled = ? WHERE app_id = ?",
44
+            enabled ? 1 : 0, appId
45
+        );
46
+        
47
+        if (updated == 0) {
48
+            throw new RuntimeException("App not found: " + appId);
49
+        }
50
+        
51
+        log.info("App {} {}", appId, enabled ? "enabled" : "disabled");
52
+        return Map.of("appId", appId, "enabled", enabled);
53
+    }
54
+
55
+    /**
56
+     * 查询应用列表
57
+     */
58
+    public List<Map<String, Object>> listApps(String keyword, Integer page, Integer size) {
59
+        StringBuilder sql = new StringBuilder("SELECT * FROM rev_app_registry WHERE 1=1");
60
+        List<Object> params = new ArrayList<>();
61
+        
62
+        if (keyword != null && !keyword.isEmpty()) {
63
+            sql.append(" AND (app_name LIKE ? OR app_id LIKE ?)");
64
+            params.add("%" + keyword + "%");
65
+            params.add("%" + keyword + "%");
66
+        }
67
+        
68
+        sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
69
+        params.add(size);
70
+        params.add((page - 1) * size);
71
+        
72
+        return jdbcTemplate.queryForList(sql.toString(), params.toArray());
73
+    }
74
+
75
+    /**
76
+     * 查询应用详情
77
+     */
78
+    public Map<String, Object> getAppDetail(String appId) {
79
+        List<Map<String, Object>> apps = jdbcTemplate.queryForList(
80
+            "SELECT * FROM rev_app_registry WHERE app_id = ?",
81
+            appId
82
+        );
83
+        
84
+        if (apps.isEmpty()) {
85
+            throw new RuntimeException("App not found: " + appId);
86
+        }
87
+        
88
+        return apps.get(0);
89
+    }
90
+
91
+    /**
92
+     * 删除应用
93
+     */
94
+    public void deleteApp(String appId) {
95
+        int deleted = jdbcTemplate.update("DELETE FROM rev_app_registry WHERE app_id = ?", appId);
96
+        if (deleted == 0) {
97
+            throw new RuntimeException("App not found: " + appId);
98
+        }
99
+        log.info("App deleted: {}", appId);
100
+    }
101
+}

+ 104
- 0
wm-revenue/src/main/java/com/water/revenue/service/InstallReportService.java Прегледај датотеку

@@ -0,0 +1,104 @@
1
+package com.water.revenue.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.util.*;
9
+
10
+@Slf4j
11
+@Service
12
+@RequiredArgsConstructor
13
+public class InstallReportService {
14
+
15
+    private final JdbcTemplate jdbcTemplate;
16
+
17
+    /**
18
+     * 报装周期分析(平均天数、各环节耗时)
19
+     */
20
+    public Map<String, Object> cycleAnalysis(String startDate, String endDate) {
21
+        // 总体平均完成天数
22
+        Double avgTotalDays = jdbcTemplate.queryForObject(
23
+            "SELECT AVG(DATEDIFF(completed_at, created_at)) FROM rev_installation " +
24
+            "WHERE status = 'completed' AND created_at >= ? AND created_at <= ?",
25
+            Double.class,
26
+            startDate + " 00:00:00", endDate + " 23:59:59"
27
+        );
28
+
29
+        // 各环节平均耗时(预受理→派单)
30
+        Double avgPreAcceptToDispatch = jdbcTemplate.queryForObject(
31
+            "SELECT AVG(DATEDIFF(dispatched_at, created_at)) FROM rev_installation " +
32
+            "WHERE dispatched_at IS NOT NULL AND created_at >= ? AND created_at <= ?",
33
+            Double.class,
34
+            startDate + " 00:00:00", endDate + " 23:59:59"
35
+        );
36
+
37
+        // 各环节平均耗时(派单→施工)
38
+        Double avgDispatchToConstruction = jdbcTemplate.queryForObject(
39
+            "SELECT AVG(DATEDIFF(construction_started_at, dispatched_at)) FROM rev_installation " +
40
+            "WHERE construction_started_at IS NOT NULL AND dispatched_at IS NOT NULL " +
41
+            "AND created_at >= ? AND created_at <= ?",
42
+            Double.class,
43
+            startDate + " 00:00:00", endDate + " 23:59:59"
44
+        );
45
+
46
+        // 各环节平均耗时(施工→竣工)
47
+        Double avgConstructionToComplete = jdbcTemplate.queryForObject(
48
+            "SELECT AVG(DATEDIFF(completed_at, construction_started_at)) FROM rev_installation " +
49
+            "WHERE completed_at IS NOT NULL AND construction_started_at IS NOT NULL " +
50
+            "AND created_at >= ? AND created_at <= ?",
51
+            Double.class,
52
+            startDate + " 00:00:00", endDate + " 23:59:59"
53
+        );
54
+
55
+        return Map.of(
56
+            "avgTotalDays", avgTotalDays != null ? Math.round(avgTotalDays * 100.0) / 100.0 : 0,
57
+            "avgPreAcceptToDispatch", avgPreAcceptToDispatch != null ? Math.round(avgPreAcceptToDispatch * 100.0) / 100.0 : 0,
58
+            "avgDispatchToConstruction", avgDispatchToConstruction != null ? Math.round(avgDispatchToConstruction * 100.0) / 100.0 : 0,
59
+            "avgConstructionToComplete", avgConstructionToComplete != null ? Math.round(avgConstructionToComplete * 100.0) / 100.0 : 0
60
+        );
61
+    }
62
+
63
+    /**
64
+     * 区域分布统计
65
+     */
66
+    public List<Map<String, Object>> areaDistribution(String startDate, String endDate) {
67
+        return jdbcTemplate.queryForList(
68
+            "SELECT area, COUNT(*) as total, " +
69
+            "SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, " +
70
+            "AVG(CASE WHEN status = 'completed' THEN DATEDIFF(completed_at, created_at) ELSE NULL END) as avg_days " +
71
+            "FROM rev_installation " +
72
+            "WHERE created_at >= ? AND created_at <= ? " +
73
+            "GROUP BY area ORDER BY total DESC",
74
+            startDate + " 00:00:00", endDate + " 23:59:59"
75
+        );
76
+    }
77
+
78
+    /**
79
+     * 月度趋势
80
+     */
81
+    public List<Map<String, Object>> monthlyTrend(String year) {
82
+        return jdbcTemplate.queryForList(
83
+            "SELECT DATE_FORMAT(created_at, '%Y-%m') as month, " +
84
+            "COUNT(*) as total, " +
85
+            "SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, " +
86
+            "SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled " +
87
+            "FROM rev_installation WHERE YEAR(created_at) = ? " +
88
+            "GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY month",
89
+            year
90
+        );
91
+    }
92
+
93
+    /**
94
+     * 客户类型分布
95
+     */
96
+    public List<Map<String, Object>> customerTypeDistribution(String startDate, String endDate) {
97
+        return jdbcTemplate.queryForList(
98
+            "SELECT customer_type, COUNT(*) as count FROM rev_installation " +
99
+            "WHERE created_at >= ? AND created_at <= ? " +
100
+            "GROUP BY customer_type ORDER BY count DESC",
101
+            startDate + " 00:00:00", endDate + " 23:59:59"
102
+        );
103
+    }
104
+}

+ 112
- 0
wm-revenue/src/main/java/com/water/revenue/service/InstallationOverviewService.java Прегледај датотеку

@@ -0,0 +1,112 @@
1
+package com.water.revenue.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.time.YearMonth;
9
+import java.time.format.DateTimeFormatter;
10
+import java.util.*;
11
+
12
+@Slf4j
13
+@Service
14
+@RequiredArgsConstructor
15
+public class InstallationOverviewService {
16
+
17
+    private final JdbcTemplate jdbcTemplate;
18
+
19
+    /**
20
+     * 首页概览数据
21
+     */
22
+    public Map<String, Object> getOverview() {
23
+        // 总数
24
+        Integer total = jdbcTemplate.queryForObject(
25
+            "SELECT COUNT(*) FROM rev_installation", Integer.class
26
+        );
27
+
28
+        // 待处理
29
+        Integer pending = jdbcTemplate.queryForObject(
30
+            "SELECT COUNT(*) FROM rev_installation WHERE status = 'pending'", Integer.class
31
+        );
32
+
33
+        // 进行中
34
+        Integer inProgress = jdbcTemplate.queryForObject(
35
+            "SELECT COUNT(*) FROM rev_installation WHERE status IN ('dispatched', 'in_progress')", Integer.class
36
+        );
37
+
38
+        // 本月完成
39
+        String currentMonth = YearMonth.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
40
+        Integer completedThisMonth = jdbcTemplate.queryForObject(
41
+            "SELECT COUNT(*) FROM rev_installation WHERE status = 'completed' AND DATE_FORMAT(completed_at, '%Y-%m') = ?",
42
+            Integer.class, currentMonth
43
+        );
44
+
45
+        // 平均完成天数
46
+        Double avgDays = jdbcTemplate.queryForObject(
47
+            "SELECT AVG(DATEDIFF(completed_at, created_at)) FROM rev_installation WHERE status = 'completed' AND completed_at IS NOT NULL",
48
+            Double.class
49
+        );
50
+
51
+        return Map.of(
52
+            "total", total != null ? total : 0,
53
+            "pending", pending != null ? pending : 0,
54
+            "inProgress", inProgress != null ? inProgress : 0,
55
+            "completedThisMonth", completedThisMonth != null ? completedThisMonth : 0,
56
+            "avgDays", avgDays != null ? Math.round(avgDays * 100.0) / 100.0 : 0
57
+        );
58
+    }
59
+
60
+    /**
61
+     * 按月统计
62
+     */
63
+    public List<Map<String, Object>> statsByMonth(String year) {
64
+        return jdbcTemplate.queryForList(
65
+            "SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count, " +
66
+            "SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed " +
67
+            "FROM rev_installation WHERE YEAR(created_at) = ? " +
68
+            "GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY month",
69
+            year
70
+        );
71
+    }
72
+
73
+    /**
74
+     * 按区域统计
75
+     */
76
+    public List<Map<String, Object>> statsByArea() {
77
+        return jdbcTemplate.queryForList(
78
+            "SELECT area, COUNT(*) as total, " +
79
+            "SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, " +
80
+            "SUM(CASE WHEN status IN ('pending', 'dispatched', 'in_progress') THEN 1 ELSE 0 END) as in_progress " +
81
+            "FROM rev_installation GROUP BY area ORDER BY total DESC"
82
+        );
83
+    }
84
+
85
+    /**
86
+     * 转化率分析
87
+     */
88
+    public Map<String, Object> conversionAnalysis() {
89
+        Integer total = jdbcTemplate.queryForObject(
90
+            "SELECT COUNT(*) FROM rev_installation", Integer.class
91
+        );
92
+        Integer completed = jdbcTemplate.queryForObject(
93
+            "SELECT COUNT(*) FROM rev_installation WHERE status = 'completed'", Integer.class
94
+        );
95
+        Integer cancelled = jdbcTemplate.queryForObject(
96
+            "SELECT COUNT(*) FROM rev_installation WHERE status = 'cancelled'", Integer.class
97
+        );
98
+
99
+        double conversionRate = total != null && total > 0 && completed != null ? 
100
+            (completed * 100.0 / total) : 0;
101
+        double cancelRate = total != null && total > 0 && cancelled != null ? 
102
+            (cancelled * 100.0 / total) : 0;
103
+
104
+        return Map.of(
105
+            "total", total != null ? total : 0,
106
+            "completed", completed != null ? completed : 0,
107
+            "cancelled", cancelled != null ? cancelled : 0,
108
+            "conversionRate", Math.round(conversionRate * 100.0) / 100.0,
109
+            "cancelRate", Math.round(cancelRate * 100.0) / 100.0
110
+        );
111
+    }
112
+}

+ 117
- 0
wm-revenue/src/main/java/com/water/revenue/service/InstallationQueryService.java Прегледај датотеку

@@ -0,0 +1,117 @@
1
+package com.water.revenue.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.util.*;
9
+
10
+@Slf4j
11
+@Service
12
+@RequiredArgsConstructor
13
+public class InstallationQueryService {
14
+
15
+    private final JdbcTemplate jdbcTemplate;
16
+
17
+    /**
18
+     * 综合查询(多条件组合)
19
+     */
20
+    public Map<String, Object> query(String startDate, String endDate, String status, String area, 
21
+                                      String customerType, String keyword, Integer page, Integer size) {
22
+        StringBuilder sql = new StringBuilder("SELECT * FROM rev_installation WHERE 1=1");
23
+        StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM rev_installation WHERE 1=1");
24
+        List<Object> params = new ArrayList<>();
25
+        List<Object> countParams = new ArrayList<>();
26
+
27
+        if (startDate != null && !startDate.isEmpty()) {
28
+            sql.append(" AND created_at >= ?");
29
+            countSql.append(" AND created_at >= ?");
30
+            params.add(startDate + " 00:00:00");
31
+            countParams.add(startDate + " 00:00:00");
32
+        }
33
+        if (endDate != null && !endDate.isEmpty()) {
34
+            sql.append(" AND created_at <= ?");
35
+            countSql.append(" AND created_at <= ?");
36
+            params.add(endDate + " 23:59:59");
37
+            countParams.add(endDate + " 23:59:59");
38
+        }
39
+        if (status != null && !status.isEmpty()) {
40
+            sql.append(" AND status = ?");
41
+            countSql.append(" AND status = ?");
42
+            params.add(status);
43
+            countParams.add(status);
44
+        }
45
+        if (area != null && !area.isEmpty()) {
46
+            sql.append(" AND area = ?");
47
+            countSql.append(" AND area = ?");
48
+            params.add(area);
49
+            countParams.add(area);
50
+        }
51
+        if (customerType != null && !customerType.isEmpty()) {
52
+            sql.append(" AND customer_type = ?");
53
+            countSql.append(" AND customer_type = ?");
54
+            params.add(customerType);
55
+            countParams.add(customerType);
56
+        }
57
+        if (keyword != null && !keyword.isEmpty()) {
58
+            sql.append(" AND (apply_no LIKE ? OR applicant_name LIKE ? OR address LIKE ?)");
59
+            countSql.append(" AND (apply_no LIKE ? OR applicant_name LIKE ? OR address LIKE ?)");
60
+            params.add("%" + keyword + "%");
61
+            params.add("%" + keyword + "%");
62
+            params.add("%" + keyword + "%");
63
+            countParams.add("%" + keyword + "%");
64
+            countParams.add("%" + keyword + "%");
65
+            countParams.add("%" + keyword + "%");
66
+        }
67
+
68
+        Integer total = jdbcTemplate.queryForObject(countSql.toString(), Integer.class, countParams.toArray());
69
+
70
+        sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
71
+        params.add(size);
72
+        params.add((page - 1) * size);
73
+
74
+        List<Map<String, Object>> data = jdbcTemplate.queryForList(sql.toString(), params.toArray());
75
+
76
+        return Map.of(
77
+            "total", total != null ? total : 0,
78
+            "page", page,
79
+            "size", size,
80
+            "data", data
81
+        );
82
+    }
83
+
84
+    /**
85
+     * 导出CSV(返回数据列表)
86
+     */
87
+    public List<Map<String, Object>> exportCsv(String startDate, String endDate, String status, 
88
+                                                 String area, String customerType) {
89
+        StringBuilder sql = new StringBuilder("SELECT * FROM rev_installation WHERE 1=1");
90
+        List<Object> params = new ArrayList<>();
91
+
92
+        if (startDate != null && !startDate.isEmpty()) {
93
+            sql.append(" AND created_at >= ?");
94
+            params.add(startDate + " 00:00:00");
95
+        }
96
+        if (endDate != null && !endDate.isEmpty()) {
97
+            sql.append(" AND created_at <= ?");
98
+            params.add(endDate + " 23:59:59");
99
+        }
100
+        if (status != null && !status.isEmpty()) {
101
+            sql.append(" AND status = ?");
102
+            params.add(status);
103
+        }
104
+        if (area != null && !area.isEmpty()) {
105
+            sql.append(" AND area = ?");
106
+            params.add(area);
107
+        }
108
+        if (customerType != null && !customerType.isEmpty()) {
109
+            sql.append(" AND customer_type = ?");
110
+            params.add(customerType);
111
+        }
112
+
113
+        sql.append(" ORDER BY created_at DESC LIMIT 10000");
114
+
115
+        return jdbcTemplate.queryForList(sql.toString(), params.toArray());
116
+    }
117
+}

+ 132
- 0
wm-revenue/src/main/java/com/water/revenue/service/InstallationTaskService.java Прегледај датотеку

@@ -0,0 +1,132 @@
1
+package com.water.revenue.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.util.*;
9
+
10
+@Slf4j
11
+@Service
12
+@RequiredArgsConstructor
13
+public class InstallationTaskService {
14
+
15
+    private final JdbcTemplate jdbcTemplate;
16
+
17
+    /**
18
+     * 创建任务
19
+     */
20
+    public Map<String, Object> createTask(String applyNo, String taskType, Long assigneeId, String assigneeName, String description) {
21
+        String taskId = "TASK-" + System.currentTimeMillis();
22
+        
23
+        jdbcTemplate.update(
24
+            "INSERT INTO rev_install_task (task_id, apply_no, task_type, assignee_id, assignee_name, description, status, created_at) " +
25
+            "VALUES (?,?,?,?,?,?,?,?)",
26
+            taskId, applyNo, taskType, assigneeId, assigneeName, description, "pending",
27
+            new java.sql.Timestamp(System.currentTimeMillis())
28
+        );
29
+        
30
+        log.info("Task created: {} for {}", taskId, applyNo);
31
+        return Map.of(
32
+            "taskId", taskId,
33
+            "applyNo", applyNo,
34
+            "taskType", taskType,
35
+            "assigneeName", assigneeName
36
+        );
37
+    }
38
+
39
+    /**
40
+     * 更新任务状态
41
+     */
42
+    public Map<String, Object> updateTaskStatus(String taskId, String status, String remark) {
43
+        StringBuilder sql = new StringBuilder("UPDATE rev_install_task SET status = ?");
44
+        List<Object> params = new ArrayList<>();
45
+        params.add(status);
46
+        
47
+        if ("completed".equals(status)) {
48
+            sql.append(", completed_at = ?");
49
+            params.add(new java.sql.Timestamp(System.currentTimeMillis()));
50
+        }
51
+        
52
+        if (remark != null && !remark.isEmpty()) {
53
+            sql.append(", remark = ?");
54
+            params.add(remark);
55
+        }
56
+        
57
+        sql.append(" WHERE task_id = ?");
58
+        params.add(taskId);
59
+        
60
+        int updated = jdbcTemplate.update(sql.toString(), params.toArray());
61
+        if (updated == 0) {
62
+            throw new RuntimeException("Task not found: " + taskId);
63
+        }
64
+        
65
+        log.info("Task {} status updated to {}", taskId, status);
66
+        return Map.of("taskId", taskId, "status", status);
67
+    }
68
+
69
+    /**
70
+     * 查询任务列表
71
+     */
72
+    public List<Map<String, Object>> listTasks(String applyNo, String assigneeName, String status, Integer page, Integer size) {
73
+        StringBuilder sql = new StringBuilder("SELECT * FROM rev_install_task WHERE 1=1");
74
+        List<Object> params = new ArrayList<>();
75
+        
76
+        if (applyNo != null && !applyNo.isEmpty()) {
77
+            sql.append(" AND apply_no = ?");
78
+            params.add(applyNo);
79
+        }
80
+        if (assigneeName != null && !assigneeName.isEmpty()) {
81
+            sql.append(" AND assignee_name LIKE ?");
82
+            params.add("%" + assigneeName + "%");
83
+        }
84
+        if (status != null && !status.isEmpty()) {
85
+            sql.append(" AND status = ?");
86
+            params.add(status);
87
+        }
88
+        
89
+        sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
90
+        params.add(size);
91
+        params.add((page - 1) * size);
92
+        
93
+        return jdbcTemplate.queryForList(sql.toString(), params.toArray());
94
+    }
95
+
96
+    /**
97
+     * 任务看板(按状态分组)
98
+     */
99
+    public Map<String, Object> kanban() {
100
+        List<Map<String, Object>> pending = jdbcTemplate.queryForList(
101
+            "SELECT * FROM rev_install_task WHERE status = 'pending' ORDER BY created_at"
102
+        );
103
+        List<Map<String, Object>> inProgress = jdbcTemplate.queryForList(
104
+            "SELECT * FROM rev_install_task WHERE status = 'in_progress' ORDER BY created_at"
105
+        );
106
+        List<Map<String, Object>> completed = jdbcTemplate.queryForList(
107
+            "SELECT * FROM rev_install_task WHERE status = 'completed' ORDER BY completed_at DESC LIMIT 50"
108
+        );
109
+        
110
+        return Map.of(
111
+            "pending", pending,
112
+            "inProgress", inProgress,
113
+            "completed", completed
114
+        );
115
+    }
116
+
117
+    /**
118
+     * 任务详情
119
+     */
120
+    public Map<String, Object> getTaskDetail(String taskId) {
121
+        List<Map<String, Object>> tasks = jdbcTemplate.queryForList(
122
+            "SELECT * FROM rev_install_task WHERE task_id = ?",
123
+            taskId
124
+        );
125
+        
126
+        if (tasks.isEmpty()) {
127
+            throw new RuntimeException("Task not found: " + taskId);
128
+        }
129
+        
130
+        return tasks.get(0);
131
+    }
132
+}

+ 112
- 0
wm-revenue/src/main/java/com/water/revenue/service/RevAuditService.java Прегледај датотеку

@@ -0,0 +1,112 @@
1
+package com.water.revenue.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.time.LocalDate;
9
+import java.util.*;
10
+
11
+@Slf4j
12
+@Service
13
+@RequiredArgsConstructor
14
+public class RevAuditService {
15
+
16
+    private final JdbcTemplate jdbcTemplate;
17
+
18
+    /**
19
+     * 记录审计日志
20
+     */
21
+    public void log(String userId, String userName, String action, String targetType, String targetId, String detail, String ip) {
22
+        jdbcTemplate.update(
23
+            "INSERT INTO rev_audit_log (user_id, user_name, action, target_type, target_id, detail, ip, created_at) VALUES (?,?,?,?,?,?,?,?)",
24
+            userId, userName, action, targetType, targetId, detail, ip, new java.sql.Timestamp(System.currentTimeMillis())
25
+        );
26
+        log.debug("Audit log: {} {} {} {}", userName, action, targetType, targetId);
27
+    }
28
+
29
+    /**
30
+     * 查询审计日志(分页+过滤)
31
+     */
32
+    public Map<String, Object> queryLogs(String userId, String action, String startDate, String endDate, Integer page, Integer size) {
33
+        StringBuilder sql = new StringBuilder("SELECT * FROM rev_audit_log WHERE 1=1");
34
+        List<Object> params = new ArrayList<>();
35
+
36
+        if (userId != null && !userId.isEmpty()) {
37
+            sql.append(" AND user_id = ?");
38
+            params.add(userId);
39
+        }
40
+        if (action != null && !action.isEmpty()) {
41
+            sql.append(" AND action = ?");
42
+            params.add(action);
43
+        }
44
+        if (startDate != null && !startDate.isEmpty()) {
45
+            sql.append(" AND created_at >= ?");
46
+            params.add(startDate + " 00:00:00");
47
+        }
48
+        if (endDate != null && !endDate.isEmpty()) {
49
+            sql.append(" AND created_at <= ?");
50
+            params.add(endDate + " 23:59:59");
51
+        }
52
+
53
+        sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
54
+        params.add(size);
55
+        params.add((page - 1) * size);
56
+
57
+        List<Map<String, Object>> logs = jdbcTemplate.queryForList(sql.toString(), params.toArray());
58
+
59
+        // 查询总数
60
+        StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM rev_audit_log WHERE 1=1");
61
+        List<Object> countParams = new ArrayList<>();
62
+        if (userId != null && !userId.isEmpty()) {
63
+            countSql.append(" AND user_id = ?");
64
+            countParams.add(userId);
65
+        }
66
+        if (action != null && !action.isEmpty()) {
67
+            countSql.append(" AND action = ?");
68
+            countParams.add(action);
69
+        }
70
+        if (startDate != null && !startDate.isEmpty()) {
71
+            countSql.append(" AND created_at >= ?");
72
+            countParams.add(startDate + " 00:00:00");
73
+        }
74
+        if (endDate != null && !endDate.isEmpty()) {
75
+            countSql.append(" AND created_at <= ?");
76
+            countParams.add(endDate + " 23:59:59");
77
+        }
78
+
79
+        Integer total = jdbcTemplate.queryForObject(countSql.toString(), Integer.class, countParams.toArray());
80
+
81
+        return Map.of(
82
+            "total", total != null ? total : 0,
83
+            "page", page,
84
+            "size", size,
85
+            "data", logs
86
+        );
87
+    }
88
+
89
+    /**
90
+     * 审计统计(按天)
91
+     */
92
+    public List<Map<String, Object>> statsByDay(String startDate, String endDate) {
93
+        return jdbcTemplate.queryForList(
94
+            "SELECT DATE(created_at) as day, COUNT(*) as count FROM rev_audit_log " +
95
+            "WHERE created_at >= ? AND created_at <= ? " +
96
+            "GROUP BY DATE(created_at) ORDER BY day DESC",
97
+            startDate + " 00:00:00", endDate + " 23:59:59"
98
+        );
99
+    }
100
+
101
+    /**
102
+     * 审计统计(按操作类型)
103
+     */
104
+    public List<Map<String, Object>> statsByAction(String startDate, String endDate) {
105
+        return jdbcTemplate.queryForList(
106
+            "SELECT action, COUNT(*) as count FROM rev_audit_log " +
107
+            "WHERE created_at >= ? AND created_at <= ? " +
108
+            "GROUP BY action ORDER BY count DESC",
109
+            startDate + " 00:00:00", endDate + " 23:59:59"
110
+        );
111
+    }
112
+}