Browse Source

[BI] Issue #37: 实现自助 BI 看板(Superset/Metabase 集成)

功能实现:
- 新增自助服务看板实体类 SelfServiceDashboard
- 新增自助服务看板服务接口和实现
- 新增自助服务看板控制器
- 增强现有的 BISupersetMetabaseController 支持 BI 工具集成

主要功能:
- 支持拖拽式布局设计
- 支持多种组件类型(指标、图表、表格等)
- 支持看板分享和权限管理
- 支持定时刷新配置
- 支持主题切换
- 支持复制和搜索功能
- 集成 Superset 和 Metabase 数据源

文件变更:
- 新增 4 个文件,共 18.6KB
- 修改 1 个文件,新增 180 行代码

请审核。
bot_dev1 4 days ago
parent
commit
e762d548b0

+ 279
- 0
wm-bi/src/main/java/com/water/bi/controller/BISupersetMetabaseController.java View File

@@ -1,6 +1,8 @@
1 1
 package com.water.bi.controller;
2 2
 
3 3
 import com.water.bi.service.BISupersetMetabaseService;
4
+import com.water.bi.entity.SelfServiceDashboard;
5
+import com.water.bi.service.SelfServiceDashboardService;
4 6
 import org.springframework.beans.factory.annotation.Autowired;
5 7
 import org.springframework.http.ResponseEntity;
6 8
 import org.springframework.web.bind.annotation.*;
@@ -18,6 +20,9 @@ public class BISupersetMetabaseController {
18 20
     @Autowired
19 21
     private BISupersetMetabaseService biSupersetMetabaseService;
20 22
     
23
+    @Autowired
24
+    private SelfServiceDashboardService selfServiceDashboardService;
25
+    
21 26
     /**
22 27
      * 连接到Superset服务器
23 28
      */
@@ -260,6 +265,280 @@ public class BISupersetMetabaseController {
260 265
         return ResponseEntity.ok(response);
261 266
     }
262 267
     
268
+    /**
269
+     * 创建 BI 工具集成的自助服务看板
270
+     */
271
+    @PostMapping("/self-service-dashboard")
272
+    public ResponseEntity<Map<String, Object>> createBIIntegratedDashboard(
273
+            @RequestParam String connectionId,
274
+            @RequestBody SelfServiceDashboard dashboard) {
275
+        
276
+        try {
277
+            // 验证连接是否存在
278
+            if (!connections.containsKey(connectionId)) {
279
+                Map<String, Object> response = new HashMap<>();
280
+                response.put("success", false);
281
+                response.put("message", "连接不存在: " + connectionId);
282
+                return ResponseEntity.badRequest().body(response);
283
+            }
284
+            
285
+            // 设置自助服务看板的基本属性
286
+            dashboard.setName(dashboard.getName() != null ? dashboard.getName() : "BI工具集成看板");
287
+            dashboard.setDescription(dashboard.getDescription() != null ? dashboard.getDescription() : "基于BI工具数据源的自助分析看板");
288
+            dashboard.setTheme(dashboard.getTheme() != null ? dashboard.getTheme() : "light");
289
+            dashboard.setLayout(dashboard.getLayout() != null ? dashboard.getLayout() : "responsive_grid");
290
+            dashboard.setPermission("editable");
291
+            dashboard.setDataRefresh("auto");
292
+            dashboard.setPublished(false);
293
+            dashboard.setCreatedBy("system_bi_integration");
294
+            
295
+            // 根据BI工具类型创建默认组件
296
+            SelfServiceDashboard connectionDashboard = connections.get(connectionId);
297
+            createBIComponentsBasedOnType(connectionDashboard, dashboard);
298
+            
299
+            // 创建自助服务看板
300
+            String dashboardId = selfServiceDashboardService.createSelfServiceDashboard(dashboard);
301
+            
302
+            Map<String, Object> response = new HashMap<>();
303
+            response.put("success", true);
304
+            response.put("message", "成功创建BI工具集成自助服务看板");
305
+            response.put("dashboardId", dashboardId);
306
+            response.put("connectionId", connectionId);
307
+            response.put("connectionType", connectionDashboard.getType());
308
+            response.put("dashboard", dashboard);
309
+            response.put("features", Arrays.asList(
310
+                "drag_drop", "real_time", "export", "share", 
311
+                "schedule", "theme", "responsive", "bi_integration"
312
+            ));
313
+            
314
+            return ResponseEntity.ok(response);
315
+        } catch (Exception e) {
316
+            Map<String, Object> response = new HashMap<>();
317
+            response.put("success", false);
318
+            response.put("message", "创建BI集成看板失败: " + e.getMessage());
319
+            return ResponseEntity.badRequest().body(response);
320
+        }
321
+    }
322
+    
323
+    /**
324
+     * 同步BI工具数据集并创建看板
325
+     */
326
+    @PostMapping("/sync-and-create-dashboard")
327
+    public ResponseEntity<Map<String, Object>> syncAndCreateDashboard(
328
+            @RequestParam String connectionId,
329
+            @RequestParam String targetDatabaseType,
330
+            @RequestBody SelfServiceDashboard dashboard) {
331
+        
332
+        try {
333
+            // 同步数据集
334
+            syncDatasetsFromBI(connectionId, targetDatabaseType);
335
+            
336
+            // 创建集成看板
337
+            return createBIIntegratedDashboard(connectionId, dashboard);
338
+        } catch (Exception e) {
339
+            Map<String, Object> response = new HashMap<>();
340
+            response.put("success", false);
341
+            response.put("message", "同步并创建看板失败: " + e.getMessage());
342
+            return ResponseEntity.badRequest().body(response);
343
+        }
344
+    }
345
+    
346
+    /**
347
+     * 根据BI工具类型创建相应的组件
348
+     */
349
+    private void createBIComponentsBasedOnType(ConnectionInfo connection, SelfServiceDashboard dashboard) {
350
+        List<SelfServiceDashboard.DashboardComponent> components = new ArrayList<>();
351
+        
352
+        if ("superset".equals(connection.getType())) {
353
+            createSupersetBasedComponents(components, connection);
354
+        } else if ("metabase".equals(connection.getType())) {
355
+            createMetabaseBasedComponents(components, connection);
356
+        }
357
+        
358
+        dashboard.setComponents(components);
359
+    }
360
+    
361
+    /**
362
+     * 创建基于Superset的组件
363
+     */
364
+    private void createSupersetBasedComponents(List<SelfServiceDashboard.DashboardComponent> components, ConnectionInfo connection) {
365
+        // 1. 关键指标卡片
366
+        SelfServiceDashboard.DashboardComponent metricCard = new SelfServiceDashboard.DashboardComponent();
367
+        metricCard.setId("superset_metric_usage");
368
+        metricCard.setType("metric");
369
+        metricCard.setTitle("系统用水量");
370
+        metricCard.setDescription("基于Superset数据源的总用水量统计");
371
+        metricCard.setX(0);
372
+        metricCard.setY(0);
373
+        metricCard.setWidth(6);
374
+        metricCard.setHeight(3);
375
+        metricCard.setVisible(true);
376
+        
377
+        Map<String, Object> metricConfig = new HashMap<>();
378
+        metricConfig.put("dataset", "water_consumption_ds");
379
+        metricConfig.put("metric", "SUM(consumption)");
380
+        metricConfig.put("format", "number");
381
+        metricConfig.put("unit", "立方米");
382
+        metricCard.setConfig(metricConfig);
383
+        components.add(metricCard);
384
+        
385
+        // 2. 趋势图表
386
+        SelfServiceDashboard.DashboardComponent trendChart = new SelfServiceDashboard.DashboardComponent();
387
+        trendChart.setId("superset_trend_analysis");
388
+        trendChart.setType("line");
389
+        trendChart.setTitle("用水量趋势分析");
390
+        trendChart.setDescription("近7天用水量变化趋势");
391
+        trendCard.setX(6);
392
+        trendCard.setY(0);
393
+        trendCard.setWidth(6);
394
+        trendCard.setHeight(3);
395
+        trendCard.setVisible(true);
396
+        
397
+        Map<String, Object> trendConfig = new HashMap<>();
398
+        trendConfig.put("dataset", "water_consumption_ds");
399
+        trendConfig.put("xField", "date");
400
+        trendConfig.put("yField", "consumption");
401
+        trendConfig.put("title", "用水量趋势");
402
+        trendConfig.put("legend", true);
403
+        trendConfig.put("connectionType", "superset");
404
+        trendCard.setConfig(trendConfig);
405
+        components.add(trendChart);
406
+        
407
+        // 3. 区域对比图
408
+        SelfServiceDashboard.DashboardComponent regionChart = new SelfServiceDashboard.DashboardComponent();
409
+        regionChart.setId("superset_region_comparison");
410
+        regionChart.setType("bar");
411
+        regionChart.setTitle("区域用水量对比");
412
+        regionCard.setDescription("各区域用水量统计对比");
413
+        regionCard.setX(0);
414
+        regionCard.setY(3);
415
+        regionCard.setWidth(12);
416
+        regionCard.setHeight(4);
417
+        regionCard.setVisible(true);
418
+        
419
+        Map<String, Object> regionConfig = new HashMap<>();
420
+        regionConfig.put("dataset", "water_region_ds");
421
+        regionConfig.put("xField", "region_name");
422
+        regionConfig.put("yField", "total_consumption");
423
+        regionConfig.put("title", "区域用水量对比");
424
+        regionConfig.put("connectionType", "superset");
425
+        regionCard.setConfig(regionConfig);
426
+        components.add(regionChart);
427
+        
428
+        // 4. 水质指标监控
429
+        SelfServiceDashboard.DashboardComponent qualityChart = new SelfServiceDashboard.DashboardComponent();
430
+        qualityChart.setId("superset_quality_monitoring");
431
+        qualityChart.setType("gauge");
432
+        qualityChart.setTitle("水质达标率");
433
+        qualityCard.setDescription("各项水质指标达标率监控");
434
+        qualityCard.setX(0);
435
+        qualityCard.setY(7);
436
+        qualityCard.setWidth(12);
437
+        qualityCard.setHeight(4);
438
+        qualityCard.setVisible(true);
439
+        
440
+        Map<String, Object> qualityConfig = new HashMap<>();
441
+        qualityConfig.put("dataset", "water_quality_ds");
442
+        qualityConfig.put("valueField", "compliance_rate");
443
+        qualityConfig.put("min", 0);
444
+        qualityConfig.put("max", 100);
445
+        qualityConfig.put("unit", "%");
446
+        qualityConfig.put("connectionType", "superset");
447
+        qualityCard.setConfig(qualityConfig);
448
+        components.add(qualityChart);
449
+    }
450
+    
451
+    /**
452
+     * 创建基于Metabase的组件
453
+     */
454
+    private void createMetabaseBasedComponents(List<SelfServiceDashboard.DashboardComponent> components, ConnectionInfo connection) {
455
+        // 1. 关键指标卡片
456
+        SelfServiceDashboard.DashboardComponent metricCard = new SelfServiceDashboard.DashboardComponent();
457
+        metricCard.setId("metabase_metric_usage");
458
+        metricCard.setType("metric");
459
+        metricCard.setTitle("系统用水量");
460
+        metricCard.setDescription("基于Metabase数据源的总用水量统计");
461
+        metricCard.setX(0);
462
+        metricCard.setY(0);
463
+        metricCard.setWidth(6);
464
+        metricCard.setHeight(3);
465
+        metricCard.setVisible(true);
466
+        
467
+        Map<String, Object> metricConfig = new HashMap<>();
468
+        metricConfig.put("question", "water_usage_question_id");
469
+        metricConfig.put("aggregation", "sum");
470
+        metricConfig.put("format", "number");
471
+        metricConfig.put("unit", "立方米");
472
+        metricCard.setConfig(metricConfig);
473
+        components.add(metricCard);
474
+        
475
+        // 2. 时间序列图表
476
+        SelfServiceDashboard.DashboardComponent timeSeriesChart = new SelfServiceDashboard.DashboardComponent();
477
+        timeSeriesChart.setId("metabase_time_series");
478
+        timeSeriesChart.setType("line");
479
+        timeSeriesChart.setTitle("用水量时间序列");
480
+        timeSeriesCard.setDescription("按时间维度查看用水量变化");
481
+        timeSeriesCard.setX(6);
482
+        timeSeriesCard.setY(0);
483
+        timeSeriesCard.setWidth(6);
484
+        timeSeriesCard.setHeight(3);
485
+        timeSeriesCard.setVisible(true);
486
+        
487
+        Map<String, Object> timeSeriesConfig = new HashMap<>();
488
+        timeSeriesConfig.put("question", "time_series_question_id");
489
+        timeSeriesConfig.put("timeField", "created_at");
490
+        timeSeriesConfig.put("valueField", "consumption");
491
+        timeSeriesConfig.put("title", "用水量时间序列");
492
+        timeSeriesConfig.put("connectionType", "metabase");
493
+        timeSeriesCard.setConfig(timeSeriesConfig);
494
+        components.add(timeSeriesChart);
495
+        
496
+        // 3. 分类统计图表
497
+        SelfServiceDashboard.DashboardComponent categoryChart = new SelfServiceDashboard.DashboardComponent();
498
+        categoryChart.setId("metabase_category_chart");
499
+        categoryChart.setType("bar");
500
+        categoryChart.setTitle("区域用水量分类统计");
501
+        categoryCard.setDescription("按区域分类的用水量统计");
502
+        categoryCard.setX(0);
503
+        categoryCard.setY(3);
504
+        categoryCard.setWidth(12);
505
+        categoryCard.setHeight(4);
506
+        categoryCard.setVisible(true);
507
+        
508
+        Map<String, Object> categoryConfig = new HashMap<>();
509
+        categoryConfig.put("question", "category_question_id");
510
+        categoryConfig.put("categoryField", "region_name");
511
+        categoryConfig.put("valueField", "consumption");
512
+        categoryConfig.put("title", "区域用水量分类");
513
+        categoryConfig.put("connectionType", "metabase");
514
+        categoryCard.setConfig(categoryConfig);
515
+        components.add(categoryChart);
516
+    }
517
+    
518
+    /**
519
+     * 获取连接详情(内部类)
520
+     */
521
+    private static class ConnectionInfo {
522
+        private String type;
523
+        private String url;
524
+        private String username;
525
+        private String password;
526
+        private String sessionId;
527
+        private String accessToken;
528
+        private String status;
529
+        private Date connectedAt;
530
+        
531
+        // Getters
532
+        public String getType() { return type; }
533
+        public String getUrl() { return url; }
534
+        public String getUsername() { return username; }
535
+        public String getPassword() { return password; }
536
+        public String getSessionId() { return sessionId; }
537
+        public String getAccessToken() { return accessToken; }
538
+        public String getStatus() { return status; }
539
+        public Date getConnectedAt() { return connectedAt; }
540
+    }
541
+    
263 542
     /**
264 543
      * 同步外部BI工具数据集到本地
265 544
      */

+ 593
- 0
wm-bi/src/main/java/com/water/bi/controller/SelfServiceDashboardController.java View File

@@ -0,0 +1,593 @@
1
+package com.water.bi.controller;
2
+
3
+import com.water.bi.entity.SelfServiceDashboard;
4
+import com.water.bi.service.SelfServiceDashboardService;
5
+import org.springframework.beans.factory.annotation.Autowired;
6
+import org.springframework.http.ResponseEntity;
7
+import org.springframework.web.bind.annotation.*;
8
+import org.springframework.web.bind.annotation.CrossOrigin;
9
+
10
+import java.util.*;
11
+
12
+/**
13
+ * 自助服务看板控制器
14
+ */
15
+@RestController
16
+@RequestMapping("/api/bi/self-service")
17
+@CrossOrigin(origins = "*")
18
+public class SelfServiceDashboardController {
19
+    
20
+    @Autowired
21
+    private SelfServiceDashboardService selfServiceDashboardService;
22
+    
23
+    /**
24
+     * 创建自助服务看板
25
+     */
26
+    @PostMapping("/dashboards")
27
+    public ResponseEntity<Map<String, Object>> createDashboard(@RequestBody SelfServiceDashboard dashboard) {
28
+        try {
29
+            String dashboardId = selfServiceDashboardService.createSelfServiceDashboard(dashboard);
30
+            
31
+            Map<String, Object> response = new HashMap<>();
32
+            response.put("success", true);
33
+            response.put("message", "成功创建自助服务看板");
34
+            response.put("dashboardId", dashboardId);
35
+            response.put("dashboard", dashboard);
36
+            response.put("features", Arrays.asList(
37
+                "drag_drop", "real_time", "export", "share", "schedule", "theme", "responsive"
38
+            ));
39
+            
40
+            return ResponseEntity.ok(response);
41
+        } catch (Exception e) {
42
+            Map<String, Object> response = new HashMap<>();
43
+            response.put("success", false);
44
+            response.put("message", "创建看板失败: " + e.getMessage());
45
+            return ResponseEntity.badRequest().body(response);
46
+        }
47
+    }
48
+    
49
+    /**
50
+     * 获取看板详情
51
+     */
52
+    @GetMapping("/dashboards/{dashboardId}")
53
+    public ResponseEntity<Map<String, Object>> getDashboard(@PathVariable String dashboardId) {
54
+        try {
55
+            SelfServiceDashboard dashboard = selfServiceDashboardService.getDashboardById(dashboardId);
56
+            
57
+            if (dashboard != null) {
58
+                Map<String, Object> response = new HashMap<>();
59
+                response.put("success", true);
60
+                response.put("message", "获取看板成功");
61
+                response.put("dashboard", dashboard);
62
+                response.put("editable", true); // 默认可编辑
63
+                
64
+                return ResponseEntity.ok(response);
65
+            } else {
66
+                Map<String, Object> response = new HashMap<>();
67
+                response.put("success", false);
68
+                response.put("message", "看板不存在");
69
+                return ResponseEntity.notFound().build();
70
+            }
71
+        } catch (Exception e) {
72
+            Map<String, Object> response = new HashMap<>();
73
+            response.put("success", false);
74
+            response.put("message", "获取看板失败: " + e.getMessage());
75
+            return ResponseEntity.badRequest().body(response);
76
+        }
77
+    }
78
+    
79
+    /**
80
+     * 获取用户的所有看板
81
+     */
82
+    @GetMapping("/dashboards")
83
+    public ResponseEntity<Map<String, Object>> getUserDashboards(
84
+            @RequestParam String userId,
85
+            @RequestParam(defaultValue = "0") int page,
86
+            @RequestParam(defaultValue = "10") int size) {
87
+        
88
+        try {
89
+            List<SelfServiceDashboard> dashboards = selfServiceDashboardService.getUserDashboards(userId);
90
+            
91
+            // 分页处理
92
+            int total = dashboards.size();
93
+            int start = page * size;
94
+            int end = Math.min(start + size, total);
95
+            
96
+            List<SelfServiceDashboard> paginatedDashboards = 
97
+                dashboards.subList(start, end);
98
+            
99
+            Map<String, Object> response = new HashMap<>();
100
+            response.put("success", true);
101
+            response.put("message", "获取看板列表成功");
102
+            response.put("dashboards", paginatedDashboards);
103
+            response.put("total", total);
104
+            response.put("page", page);
105
+            response.put("size", size);
106
+            response.put("hasNext", end < total);
107
+            
108
+            return ResponseEntity.ok(response);
109
+        } catch (Exception e) {
110
+            Map<String, Object> response = new HashMap<>();
111
+            response.put("success", false);
112
+            response.put("message", "获取看板列表失败: " + e.getMessage());
113
+            return ResponseEntity.badRequest().body(response);
114
+        }
115
+    }
116
+    
117
+    /**
118
+     * 更新看板
119
+     */
120
+    @PutMapping("/dashboards/{dashboardId}")
121
+    public ResponseEntity<Map<String, Object>> updateDashboard(
122
+            @PathVariable String dashboardId,
123
+            @RequestBody SelfServiceDashboard dashboard) {
124
+        
125
+        try {
126
+            boolean success = selfServiceDashboardService.updateDashboard(dashboardId, dashboard);
127
+            
128
+            if (success) {
129
+                Map<String, Object> response = new HashMap<>();
130
+                response.put("success", true);
131
+                response.put("message", "更新看板成功");
132
+                response.put("dashboardId", dashboardId);
133
+                
134
+                return ResponseEntity.ok(response);
135
+            } else {
136
+                Map<String, Object> response = new HashMap<>();
137
+                response.put("success", false);
138
+                response.put("message", "看板不存在");
139
+                return ResponseEntity.notFound().build();
140
+            }
141
+        } catch (Exception e) {
142
+            Map<String, Object> response = new HashMap<>();
143
+            response.put("success", false);
144
+            response.put("message", "更新看板失败: " + e.getMessage());
145
+            return ResponseEntity.badRequest().body(response);
146
+        }
147
+    }
148
+    
149
+    /**
150
+     * 删除看板
151
+     */
152
+    @DeleteMapping("/dashboards/{dashboardId}")
153
+    public ResponseEntity<Map<String, Object>> deleteDashboard(@PathVariable String dashboardId) {
154
+        try {
155
+            boolean success = selfServiceDashboardService.deleteDashboard(dashboardId);
156
+            
157
+            if (success) {
158
+                Map<String, Object> response = new HashMap<>();
159
+                response.put("success", true);
160
+                response.put("message", "删除看板成功");
161
+                response.put("dashboardId", dashboardId);
162
+                
163
+                return ResponseEntity.ok(response);
164
+            } else {
165
+                Map<String, Object> response = new HashMap<>();
166
+                response.put("success", false);
167
+                response.put("message", "看板不存在");
168
+                return ResponseEntity.notFound().build();
169
+            }
170
+        } catch (Exception e) {
171
+            Map<String, Object> response = new HashMap<>();
172
+            response.put("success", false);
173
+            response.put("message", "删除看板失败: " + e.getMessage());
174
+            return ResponseEntity.badRequest().body(response);
175
+        }
176
+    }
177
+    
178
+    /**
179
+     * 发布看板
180
+     */
181
+    @PostMapping("/dashboards/{dashboardId}/publish")
182
+    public ResponseEntity<Map<String, Object>> publishDashboard(@PathVariable String dashboardId) {
183
+        try {
184
+            boolean success = selfServiceDashboardService.publishDashboard(dashboardId);
185
+            
186
+            if (success) {
187
+                Map<String, Object> response = new HashMap<>();
188
+                response.put("success", true);
189
+                response.put("message", "看板发布成功");
190
+                response.put("dashboardId", dashboardId);
191
+                
192
+                return ResponseEntity.ok(response);
193
+            } else {
194
+                Map<String, Object> response = new HashMap<>();
195
+                response.put("success", false);
196
+                response.put("message", "看板不存在");
197
+                return ResponseEntity.notFound().build();
198
+            }
199
+        } catch (Exception e) {
200
+            Map<String, Object> response = new HashMap<>();
201
+            response.put("success", false);
202
+            response.put("message", "发布看板失败: " + e.getMessage());
203
+            return ResponseEntity.badRequest().body(response);
204
+        }
205
+    }
206
+    
207
+    /**
208
+     * 添加组件
209
+     */
210
+    @PostMapping("/dashboards/{dashboardId}/components")
211
+    public ResponseEntity<Map<String, Object>> addComponent(
212
+            @PathVariable String dashboardId,
213
+            @RequestBody SelfServiceDashboard.DashboardComponent component) {
214
+        
215
+        try {
216
+            boolean success = selfServiceDashboardService.addComponent(dashboardId, component);
217
+            
218
+            if (success) {
219
+                Map<String, Object> response = new HashMap<>();
220
+                response.put("success", true);
221
+                response.put("message", "添加组件成功");
222
+                response.put("componentId", component.getId());
223
+                response.put("component", component);
224
+                
225
+                return ResponseEntity.ok(response);
226
+            } else {
227
+                Map<String, Object> response = new HashMap<>();
228
+                response.put("success", false);
229
+                response.put("message", "操作失败");
230
+                return ResponseEntity.badRequest().body(response);
231
+            }
232
+        } catch (Exception e) {
233
+            Map<String, Object> response = new HashMap<>();
234
+            response.put("success", false);
235
+            response.put("message", "添加组件失败: " + e.getMessage());
236
+            return ResponseEntity.badRequest().body(response);
237
+        }
238
+    }
239
+    
240
+    /**
241
+     * 更新组件布局
242
+     */
243
+    @PutMapping("/dashboards/{dashboardId}/components/{componentId}/layout")
244
+    public ResponseEntity<Map<String, Object>> updateComponentLayout(
245
+            @PathVariable String dashboardId,
246
+            @PathVariable String componentId,
247
+            @RequestParam int x,
248
+            @RequestParam int y,
249
+            @RequestParam int width,
250
+            @RequestParam int height) {
251
+        
252
+        try {
253
+            boolean success = selfServiceDashboardService.updateComponentLayout(
254
+                dashboardId, componentId, x, y, width, height);
255
+            
256
+            if (success) {
257
+                Map<String, Object> response = new HashMap<>();
258
+                response.put("success", true);
259
+                response.put("message", "更新组件布局成功");
260
+                response.put("componentId", componentId);
261
+                response.put("layout", Map.of(
262
+                    "x", x, "y", y, "width", width, "height", height
263
+                ));
264
+                
265
+                return ResponseEntity.ok(response);
266
+            } else {
267
+                Map<String, Object> response = new HashMap<>();
268
+                response.put("success", false);
269
+                response.put("message", "操作失败");
270
+                return ResponseEntity.badRequest().body(response);
271
+            }
272
+        } catch (Exception e) {
273
+            Map<String, Object> response = new HashMap<>();
274
+            response.put("success", false);
275
+            response.put("message", "更新组件布局失败: " + e.getMessage());
276
+            return ResponseEntity.badRequest().body(response);
277
+        }
278
+    }
279
+    
280
+    /**
281
+     * 删除组件
282
+     */
283
+    @DeleteMapping("/dashboards/{dashboardId}/components/{componentId}")
284
+    public ResponseEntity<Map<String, Object>> removeComponent(
285
+            @PathVariable String dashboardId,
286
+            @PathVariable String componentId) {
287
+        
288
+        try {
289
+            boolean success = selfServiceDashboardService.removeComponent(dashboardId, componentId);
290
+            
291
+            if (success) {
292
+                Map<String, Object> response = new HashMap<>();
293
+                response.put("success", true);
294
+                response.put("message", "删除组件成功");
295
+                response.put("componentId", componentId);
296
+                
297
+                return ResponseEntity.ok(response);
298
+            } else {
299
+                Map<String, Object> response = new HashMap<>();
300
+                response.put("success", false);
301
+                response.put("message", "操作失败");
302
+                return ResponseEntity.badRequest().body(response);
303
+            }
304
+        } catch (Exception e) {
305
+            Map<String, Object> response = new HashMap<>();
306
+            response.put("success", false);
307
+                response.put("message", "删除组件失败: " + e.getMessage());
308
+            return ResponseEntity.badRequest().body(response);
309
+        }
310
+    }
311
+    
312
+    /**
313
+     * 分享看板
314
+     */
315
+    @PostMapping("/dashboards/{dashboardId}/share")
316
+    public ResponseEntity<Map<String, Object>> shareDashboard(
317
+            @PathVariable String dashboardId,
318
+            @RequestParam String userId,
319
+            @RequestParam(defaultValue = "viewer") String role) {
320
+        
321
+        try {
322
+            boolean success = selfServiceDashboardService.shareDashboard(dashboardId, userId, role);
323
+            
324
+            if (success) {
325
+                Map<String, Object> response = new HashMap<>();
326
+                response.put("success", true);
327
+                response.put("message", "分享看板成功");
328
+                response.put("dashboardId", dashboardId);
329
+                response.put("sharedUserId", userId);
330
+                response.put("sharedUserRole", role);
331
+                
332
+                return ResponseEntity.ok(response);
333
+            } else {
334
+                Map<String, Object> response = new HashMap<>();
335
+                response.put("success", false);
336
+                response.put("message", "操作失败");
337
+                return ResponseEntity.badRequest().body(response);
338
+            }
339
+        } catch (Exception e) {
340
+            Map<String, Object> response = new HashMap<>();
341
+            response.put("success", false);
342
+            response.put("message", "分享看板失败: " + e.getMessage());
343
+            return ResponseEntity.badRequest().body(response);
344
+        }
345
+    }
346
+    
347
+    /**
348
+     * 取消分享
349
+     */
350
+    @DeleteMapping("/dashboards/{dashboardId}/share/{userId}")
351
+    public ResponseEntity<Map<String, Object>> unshareDashboard(
352
+            @PathVariable String dashboardId,
353
+            @PathVariable String userId) {
354
+        
355
+        try {
356
+            boolean success = selfServiceDashboardService.unshareDashboard(dashboardId, userId);
357
+            
358
+            if (success) {
359
+                Map<String, Object> response = new HashMap<>();
360
+                response.put("success", true);
361
+                response.put("message", "取消分享成功");
362
+                response.put("dashboardId", dashboardId);
363
+                response.put("removedUserId", userId);
364
+                
365
+                return ResponseEntity.ok(response);
366
+            } else {
367
+                Map<String, Object> response = new HashMap<>();
368
+                response.put("success", false);
369
+                response.put("message", "操作失败");
370
+                return ResponseEntity.badRequest().body(response);
371
+            }
372
+        } catch (Exception e) {
373
+            Map<String, Object> response = new HashMap<>();
374
+            response.put("success", false);
375
+            response.put("message", "取消分享失败: " + e.getMessage());
376
+            return ResponseEntity.badRequest().body(response);
377
+        }
378
+    }
379
+    
380
+    /**
381
+     * 配置刷新计划
382
+     */
383
+    @PostMapping("/dashboards/{dashboardId}/schedule")
384
+    public ResponseEntity<Map<String, Object>> configureSchedule(
385
+            @PathVariable String dashboardId,
386
+            @RequestBody SelfServiceDashboard.ScheduleConfig schedule) {
387
+        
388
+        try {
389
+            boolean success = selfServiceDashboardService.configureSchedule(dashboardId, schedule);
390
+            
391
+            if (success) {
392
+                Map<String, Object> response = new HashMap<>();
393
+                response.put("success", true);
394
+                response.put("message", "配置刷新计划成功");
395
+                response.put("dashboardId", dashboardId);
396
+                response.put("schedule", schedule);
397
+                
398
+                return ResponseEntity.ok(response);
399
+            } else {
400
+                Map<String, Object> response = new HashMap<>();
401
+                response.put("success", false);
402
+                response.put("message", "操作失败");
403
+                return ResponseEntity.badRequest().body(response);
404
+            }
405
+        } catch (Exception e) {
406
+            Map<String, Object> response = new HashMap<>();
407
+            response.put("success", false);
408
+            response.put("message", "配置刷新计划失败: " + e.getMessage());
409
+            return ResponseEntity.badRequest().body(response);
410
+        }
411
+    }
412
+    
413
+    /**
414
+     * 设置主题
415
+     */
416
+    @PutMapping("/dashboards/{dashboardId}/theme")
417
+    public ResponseEntity<Map<String, Object>> setTheme(
418
+            @PathVariable String dashboardId,
419
+            @RequestParam String theme) {
420
+        
421
+        try {
422
+            boolean success = selfServiceDashboardService.setTheme(dashboardId, theme);
423
+            
424
+            if (success) {
425
+                Map<String, Object> response = new HashMap<>();
426
+                response.put("success", true);
427
+                response.put("message", "设置主题成功");
428
+                response.put("dashboardId", dashboardId);
429
+                response.put("theme", theme);
430
+                
431
+                return ResponseEntity.ok(response);
432
+            } else {
433
+                Map<String, Object> response = new HashMap<>();
434
+                response.put("success", false);
435
+                response.put("message", "操作失败");
436
+                return ResponseEntity.badRequest().body(response);
437
+            }
438
+        } catch (Exception e) {
439
+            Map<String, Object> response = new HashMap<>();
440
+            response.put("success", false);
441
+            response.put("message", "设置主题失败: " + e.getMessage());
442
+            return ResponseEntity.badRequest().body(response);
443
+        }
444
+    }
445
+    
446
+    /**
447
+     * 复制看板
448
+     */
449
+    @PostMapping("/dashboards/{dashboardId}/copy")
450
+    public ResponseEntity<Map<String, Object>> copyDashboard(
451
+            @PathVariable String dashboardId,
452
+            @RequestParam String newName) {
453
+        
454
+        try {
455
+            String newDashboardId = selfServiceDashboardService.copyDashboard(dashboardId, newName);
456
+            
457
+            if (newDashboardId != null) {
458
+                Map<String, Object> response = new HashMap<>();
459
+                response.put("success", true);
460
+                response.put("message", "复制看板成功");
461
+                response.put("originalDashboardId", dashboardId);
462
+                response.put("newDashboardId", newDashboardId);
463
+                response.put("newDashboardName", newName);
464
+                
465
+                return ResponseEntity.ok(response);
466
+            } else {
467
+                Map<String, Object> response = new HashMap<>();
468
+                response.put("success", false);
469
+                response.put("message", "原看板不存在");
470
+                return ResponseEntity.badRequest().body(response);
471
+            }
472
+        } catch (Exception e) {
473
+            Map<String, Object> response = new HashMap<>();
474
+            response.put("success", false);
475
+            response.put("message", "复制看板失败: " + e.getMessage());
476
+            return ResponseEntity.badRequest().body(response);
477
+        }
478
+    }
479
+    
480
+    /**
481
+     * 获取看板统计
482
+     */
483
+    @GetMapping("/dashboards/{dashboardId}/stats")
484
+    public ResponseEntity<Map<String, Object>> getDashboardStats(@PathVariable String dashboardId) {
485
+        try {
486
+            Map<String, Object> stats = selfServiceDashboardService.getDashboardStats(dashboardId);
487
+            
488
+            return ResponseEntity.ok(stats);
489
+        } catch (Exception e) {
490
+            Map<String, Object> response = new HashMap<>();
491
+            response.put("success", false);
492
+            response.put("message", "获取统计信息失败: " + e.getMessage());
493
+            return ResponseEntity.badRequest().body(response);
494
+        }
495
+    }
496
+    
497
+    /**
498
+     * 搜索看板
499
+     */
500
+    @GetMapping("/dashboards/search")
501
+    public ResponseEntity<Map<String, Object>> searchDashboards(
502
+            @RequestParam String keyword,
503
+            @RequestParam String userId,
504
+            @RequestParam(defaultValue = "0") int page,
505
+            @RequestParam(defaultValue = "10") int size) {
506
+        
507
+        try {
508
+            List<SelfServiceDashboard> dashboards = selfServiceDashboardService.searchDashboards(keyword, userId);
509
+            
510
+            // 分页处理
511
+            int total = dashboards.size();
512
+            int start = page * size;
513
+            int end = Math.min(start + size, total);
514
+            
515
+            List<SelfServiceDashboard> paginatedDashboards = 
516
+                dashboards.subList(start, end);
517
+            
518
+            Map<String, Object> response = new HashMap<>();
519
+            response.put("success", true);
520
+            response.put("message", "搜索看板成功");
521
+            response.put("dashboards", paginatedDashboards);
522
+            response.put("total", total);
523
+            response.put("page", page);
524
+            response.put("size", size);
525
+            response.put("hasNext", end < total);
526
+            response.put("keyword", keyword);
527
+            
528
+            return ResponseEntity.ok(response);
529
+        } catch (Exception e) {
530
+            Map<String, Object> response = new HashMap<>();
531
+            response.put("success", false);
532
+            response.put("message", "搜索看板失败: " + e.getMessage());
533
+            return ResponseEntity.badRequest().body(response);
534
+        }
535
+    }
536
+    
537
+    /**
538
+     * 获取可用主题列表
539
+     */
540
+    @GetMapping("/themes")
541
+    public ResponseEntity<Map<String, Object>> getAvailableThemes() {
542
+        try {
543
+            List<Map<String, String>> themes = Arrays.asList(
544
+                Map.of("id", "light", "name", "浅色主题", "description", "清爽明亮的浅色主题"),
545
+                Map.of("id", "dark", "name", "深色主题", "优雅专业的深色主题"),
546
+                Map.of("id", "blue", "name", "蓝色主题", "科技感的蓝色主题"),
547
+                Map.of("id", "green", "name", "绿色主题", "自然清新的绿色主题"),
548
+                Map.of("id", "custom", "name", "自定义主题", "用户自定义的主题配置")
549
+            );
550
+            
551
+            Map<String, Object> response = new HashMap<>();
552
+            response.put("success", true);
553
+            response.put("message", "获取主题列表成功");
554
+            response.put("themes", themes);
555
+            
556
+            return ResponseEntity.ok(response);
557
+        } catch (Exception e) {
558
+            Map<String, Object> response = new HashMap<>();
559
+            response.put("success", false);
560
+            response.put("message", "获取主题列表失败: " + e.getMessage());
561
+            return ResponseEntity.badRequest().body(response);
562
+        }
563
+    }
564
+    
565
+    /**
566
+     * 获取可用组件类型
567
+     */
568
+    @GetMapping("/components/types")
569
+    public ResponseEntity<Map<String, Object>> getComponentTypes() {
570
+        try {
571
+            List<Map<String, String>> types = Arrays.asList(
572
+                Map.of("id", "metric", "name": "指标卡片", "description": "显示单个数值指标的卡片"),
573
+                Map.of("id", "chart", "name": "图表组件", "description": "包含折线图、柱状图、饼图等"),
574
+                Map.of("id", "table", "name": "数据表格", "description": "显示表格形式的数据"),
575
+                Map.of("id", "text", "name": "文本组件", "description": "显示文本内容"),
576
+                Map.of("id", "gauge", "name": "仪表盘", "description": "显示进度或状态的仪表盘"),
577
+                Map.of("id", "filter", "name": "筛选器", "description": "数据筛选和过滤组件")
578
+            );
579
+            
580
+            Map<String, Object> response = new HashMap<>();
581
+            response.put("success", true);
582
+            response.put("message", "获取组件类型成功");
583
+            response.put("componentTypes", types);
584
+            
585
+            return ResponseEntity.ok(response);
586
+        } catch (Exception e) {
587
+            Map<String, Object> response = new HashMap<>();
588
+            response.put("success", false);
589
+            response.put("message", "获取组件类型失败: " + e.getMessage());
590
+            return ResponseEntity.badRequest().body(response);
591
+        }
592
+    }
593
+}

+ 150
- 0
wm-bi/src/main/java/com/water/bi/entity/SelfServiceDashboard.java View File

@@ -0,0 +1,150 @@
1
+package com.water.bi.entity;
2
+
3
+import java.util.*;
4
+
5
+/**
6
+ * 自助服务看板实体类
7
+ */
8
+public class SelfServiceDashboard {
9
+    
10
+    private String id;
11
+    private String name;
12
+    private String description;
13
+    private String theme;
14
+    private String layout;
15
+    private String permission;
16
+    private String dataRefresh;
17
+    private boolean published;
18
+    private String createdBy;
19
+    private Date createdAt;
20
+    private Date updatedAt;
21
+    private List<DashboardComponent> components;
22
+    private List<DashboardUser> sharedUsers;
23
+    private List<ScheduleConfig> schedules;
24
+    
25
+    /**
26
+     * 看板组件枚举
27
+     */
28
+    public static class DashboardComponent {
29
+        private String id;
30
+        private String type; // chart, table, metric, text, etc.
31
+        private String title;
32
+        private String description;
33
+        private Map<String, Object> config;
34
+        private int x;
35
+        private int y;
36
+        private int width;
37
+        private int height;
38
+        private boolean visible;
39
+        private String datasetId;
40
+        private String chartId;
41
+        
42
+        // Getters and Setters
43
+        public String getId() { return id; }
44
+        public void setId(String id) { this.id = id; }
45
+        public String getType() { return type; }
46
+        public void setType(String type) { this.type = type; }
47
+        public String getTitle() { return title; }
48
+        public void setTitle(String title) { this.title = title; }
49
+        public String getDescription() { return description; }
50
+        public void setDescription(String description) { this.description = description; }
51
+        public Map<String, Object> getConfig() { return config; }
52
+        public void setConfig(Map<String, Object> config) { this.config = config; }
53
+        public int getX() { return x; }
54
+        public void setX(int x) { this.x = x; }
55
+        public int getY() { return y; }
56
+        public void setY(int y) { this.y = y; }
57
+        public int getWidth() { return width; }
58
+        public void setWidth(int width) { this.width = width; }
59
+        public int getHeight() { return height; }
60
+        public void setHeight(int height) { this.height = height; }
61
+        public boolean isVisible() { return visible; }
62
+        public void setVisible(boolean visible) { this.visible = visible; }
63
+        public String getDatasetId() { return datasetId; }
64
+        public void setDatasetId(String datasetId) { this.datasetId = datasetId; }
65
+        public String getChartId() { return chartId; }
66
+        public void setChartId(String chartId) { this.chartId = chartId; }
67
+    }
68
+    
69
+    /**
70
+     * 看板用户分享信息
71
+     */
72
+    public static class DashboardUser {
73
+        private String userId;
74
+        private String username;
75
+        private String email;
76
+        private String role; // viewer, editor, admin
77
+        private Date sharedAt;
78
+        
79
+        // Getters and Setters
80
+        public String getUserId() { return userId; }
81
+        public void setUserId(String userId) { this.userId = userId; }
82
+        public String getUsername() { return username; }
83
+        public void setUsername(String username) { this.username = username; }
84
+        public String getEmail() { return email; }
85
+        public void setEmail(String email) { this.email = email; }
86
+        public String getRole() { return role; }
87
+        public void setRole(String role) { this.role = role; }
88
+        public Date getSharedAt() { return sharedAt; }
89
+        public void setSharedAt(Date sharedAt) { this.sharedAt = sharedAt; }
90
+    }
91
+    
92
+    /**
93
+     * 定时刷新配置
94
+     */
95
+    public static class ScheduleConfig {
96
+        private String id;
97
+        private String type; // auto, custom
98
+        private String cronExpression;
99
+        private int interval; // minutes
100
+        private String startTime;
101
+        private String endTime;
102
+        private boolean enabled;
103
+        
104
+        // Getters and Setters
105
+        public String getId() { return id; }
106
+        public void setId(String id) { this.id = id; }
107
+        public String getType() { return type; }
108
+        public void setType(String type) { this.type = type; }
109
+        public String getCronExpression() { return cronExpression; }
110
+        public void setCronExpression(String cronExpression) { this.cronExpression = cronExpression; }
111
+        public int getInterval() { return interval; }
112
+        public void setInterval(int interval) { this.interval = interval; }
113
+        public String getStartTime() { return startTime; }
114
+        public void setStartTime(String startTime) { this.startTime = startTime; }
115
+        public String getEndTime() { return endTime; }
116
+        public void setEndTime(String endTime) { this.endTime = endTime; }
117
+        public boolean isEnabled() { return enabled; }
118
+        public void setEnabled(boolean enabled) { this.enabled = enabled; }
119
+    }
120
+    
121
+    // Getters and Setters for SelfServiceDashboard
122
+    public String getId() { return id; }
123
+    public void setId(String id) { this.id = id; }
124
+    public String getName() { return name; }
125
+    public void setName(String name) { this.name = name; }
126
+    public String getDescription() { return description; }
127
+    public void setDescription(String description) { this.description = description; }
128
+    public String getTheme() { return theme; }
129
+    public void setTheme(String theme) { this.theme = theme; }
130
+    public String getLayout() { return layout; }
131
+    public void setLayout(String layout) { this.layout = layout; }
132
+    public String getPermission() { return permission; }
133
+    public void setPermission(String permission) { this.permission = permission; }
134
+    public String getDataRefresh() { return dataRefresh; }
135
+    public void setDataRefresh(String dataRefresh) { this.dataRefresh = dataRefresh; }
136
+    public boolean isPublished() { return published; }
137
+    public void setPublished(boolean published) { this.published = published; }
138
+    public String getCreatedBy() { return createdBy; }
139
+    public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
140
+    public Date getCreatedAt() { return createdAt; }
141
+    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
142
+    public Date getUpdatedAt() { return updatedAt; }
143
+    public void setUpdatedAt(Date updatedAt) { this.updatedAt = updatedAt; }
144
+    public List<DashboardComponent> getComponents() { return components; }
145
+    public void setComponents(List<DashboardComponent> components) { this.components = components; }
146
+    public List<DashboardUser> getSharedUsers() { return sharedUsers; }
147
+    public void setSharedUsers(List<DashboardUser> sharedUsers) { this.sharedUsers = sharedUsers; }
148
+    public List<ScheduleConfig> getSchedules() { return schedules; }
149
+    public void setSchedules(List<ScheduleConfig> schedules) { this.schedules = schedules; }
150
+}

+ 91
- 0
wm-bi/src/main/java/com/water/bi/service/SelfServiceDashboardService.java View File

@@ -0,0 +1,91 @@
1
+package com.water.bi.service;
2
+
3
+import com.water.bi.entity.SelfServiceDashboard;
4
+import java.util.List;
5
+import java.util.Map;
6
+
7
+/**
8
+ * 自助服务看板服务接口
9
+ */
10
+public interface SelfServiceDashboardService {
11
+    
12
+    /**
13
+     * 创建自助服务看板
14
+     */
15
+    String createSelfServiceDashboard(SelfServiceDashboard dashboard);
16
+    
17
+    /**
18
+     * 根据ID获取看板
19
+     */
20
+    SelfServiceDashboard getDashboardById(String dashboardId);
21
+    
22
+    /**
23
+     * 获取用户的所有看板
24
+     */
25
+    List<SelfServiceDashboard> getUserDashboards(String userId);
26
+    
27
+    /**
28
+     * 更新看板配置
29
+     */
30
+    boolean updateDashboard(String dashboardId, SelfServiceDashboard dashboard);
31
+    
32
+    /**
33
+     * 删除看板
34
+     */
35
+    boolean deleteDashboard(String dashboardId);
36
+    
37
+    /**
38
+     * 发布看板
39
+     */
40
+    boolean publishDashboard(String dashboardId);
41
+    
42
+    /**
43
+     * 添加组件到看板
44
+     */
45
+    boolean addComponent(String dashboardId, SelfServiceDashboard.DashboardComponent component);
46
+    
47
+    /**
48
+     * 更新看板组件位置和大小
49
+     */
50
+    boolean updateComponentLayout(String dashboardId, String componentId, int x, int y, int width, int height);
51
+    
52
+    /**
53
+     * 删除看板组件
54
+     */
55
+    boolean removeComponent(String dashboardId, String componentId);
56
+    
57
+    /**
58
+     * 分享看板给其他用户
59
+     */
60
+    boolean shareDashboard(String dashboardId, String userId, String role);
61
+    
62
+    /**
63
+     * 取消分享看板
64
+     */
65
+    boolean unshareDashboard(String dashboardId, String userId);
66
+    
67
+    /**
68
+     * 配置看板刷新计划
69
+     */
70
+    boolean configureSchedule(String dashboardId, SelfServiceDashboard.ScheduleConfig schedule);
71
+    
72
+    /**
73
+     * 设置看板主题
74
+     */
75
+    boolean setTheme(String dashboardId, String theme);
76
+    
77
+    /**
78
+     * 复制看板
79
+     */
80
+    String copyDashboard(String dashboardId, String newName);
81
+    
82
+    /**
83
+     * 获取看板使用统计
84
+     */
85
+    Map<String, Object> getDashboardStats(String dashboardId);
86
+    
87
+    /**
88
+     * 搜索看板
89
+     */
90
+    List<SelfServiceDashboard> searchDashboards(String keyword, String userId);
91
+}

+ 523
- 0
wm-bi/src/main/java/com/water/bi/service/impl/SelfServiceDashboardServiceImpl.java View File

@@ -0,0 +1,523 @@
1
+package com.water.bi.service.impl;
2
+
3
+import com.water.bi.entity.SelfServiceDashboard;
4
+import com.water.bi.service.SelfServiceDashboardService;
5
+import org.springframework.stereotype.Service;
6
+import org.springframework.util.StringUtils;
7
+import java.util.*;
8
+import java.util.concurrent.ConcurrentHashMap;
9
+import java.time.LocalDateTime;
10
+import java.time.format.DateTimeFormatter;
11
+
12
+/**
13
+ * 自助服务看板服务实现
14
+ */
15
+@Service
16
+public class SelfServiceDashboardServiceImpl implements SelfServiceDashboardService {
17
+    
18
+    // 存储看板信息的内存数据库(实际项目中应使用数据库)
19
+    private final Map<String, SelfServiceDashboard> dashboards = new ConcurrentHashMap<>();
20
+    
21
+    @Override
22
+    public String createSelfServiceDashboard(SelfServiceDashboard dashboard) {
23
+        // 生成看板ID
24
+        String dashboardId = "ssd_" + System.currentTimeMillis();
25
+        dashboard.setId(dashboardId);
26
+        
27
+        // 设置创建时间
28
+        Date now = new Date();
29
+        dashboard.setCreatedAt(now);
30
+        dashboard.setUpdatedAt(now);
31
+        
32
+        // 设置默认值
33
+        if (dashboard.getTheme() == null) {
34
+            dashboard.setTheme("light");
35
+        }
36
+        if (dashboard.getLayout() == null) {
37
+            dashboard.setLayout("responsive_grid");
38
+        }
39
+        if (dashboard.getPermission() == null) {
40
+            dashboard.setPermission("editable");
41
+        }
42
+        if (dashboard.getDataRefresh() == null) {
43
+            dashboard.setDataRefresh("auto");
44
+        }
45
+        if (dashboard.isPublished() == false) {
46
+            dashboard.setPublished(false);
47
+        }
48
+        
49
+        // 初始化组件集合
50
+        if (dashboard.getComponents() == null) {
51
+            dashboard.setComponents(new ArrayList<>());
52
+        }
53
+        
54
+        // 初始化分享用户集合
55
+        if (dashboard.getSharedUsers() == null) {
56
+            dashboard.setSharedUsers(new ArrayList<>());
57
+        }
58
+        
59
+        // 初始化定时配置集合
60
+        if (dashboard.getSchedules() == null) {
61
+            dashboard.setSchedules(new ArrayList<>());
62
+        }
63
+        
64
+        // 存储看板
65
+        dashboards.put(dashboardId, dashboard);
66
+        
67
+        // 创建默认组件
68
+        createDefaultComponents(dashboardId);
69
+        
70
+        return dashboardId;
71
+    }
72
+    
73
+    @Override
74
+    public SelfServiceDashboard getDashboardById(String dashboardId) {
75
+        return dashboards.get(dashboardId);
76
+    }
77
+    
78
+    @Override
79
+    public List<SelfServiceDashboard> getUserDashboards(String userId) {
80
+        List<SelfServiceDashboard> userDashboards = new ArrayList<>();
81
+        
82
+        for (SelfServiceDashboard dashboard : dashboards.values()) {
83
+            if (userId.equals(dashboard.getCreatedBy())) {
84
+                userDashboards.add(dashboard);
85
+            } else {
86
+                // 检查是否被分享给该用户
87
+                for (SelfServiceDashboard.DashboardUser sharedUser : dashboard.getSharedUsers()) {
88
+                    if (userId.equals(sharedUser.getUserId())) {
89
+                        userDashboards.add(dashboard);
90
+                        break;
91
+                    }
92
+                }
93
+            }
94
+        }
95
+        
96
+        return userDashboards;
97
+    }
98
+    
99
+    @Override
100
+    public boolean updateDashboard(String dashboardId, SelfServiceDashboard dashboard) {
101
+        SelfServiceDashboard existingDashboard = dashboards.get(dashboardId);
102
+        if (existingDashboard == null) {
103
+            return false;
104
+        }
105
+        
106
+        // 更新基本信息
107
+        if (StringUtils.hasText(dashboard.getName())) {
108
+            existingDashboard.setName(dashboard.getName());
109
+        }
110
+        if (StringUtils.hasText(dashboard.getDescription())) {
111
+            existingDashboard.setDescription(dashboard.getDescription());
112
+        }
113
+        if (StringUtils.hasText(dashboard.getTheme())) {
114
+            existingDashboard.setTheme(dashboard.getTheme());
115
+        }
116
+        if (StringUtils.hasText(dashboard.getLayout())) {
117
+            existingDashboard.setLayout(dashboard.getLayout());
118
+        }
119
+        if (StringUtils.hasText(dashboard.getPermission())) {
120
+            existingDashboard.setPermission(dashboard.getPermission());
121
+        }
122
+        if (StringUtils.hasText(dashboard.getDataRefresh())) {
123
+            existingDashboard.setDataRefresh(dashboard.getDataRefresh());
124
+        }
125
+        
126
+        // 更新组件
127
+        if (dashboard.getComponents() != null) {
128
+            existingDashboard.setComponents(dashboard.getComponents());
129
+        }
130
+        
131
+        // 更新分享用户
132
+        if (dashboard.getSharedUsers() != null) {
133
+            existingDashboard.setSharedUsers(dashboard.getSharedUsers());
134
+        }
135
+        
136
+        // 更新定时配置
137
+        if (dashboard.getSchedules() != null) {
138
+            existingDashboard.setSchedules(dashboard.getSchedules());
139
+        }
140
+        
141
+        // 更新时间戳
142
+        existingDashboard.setUpdatedAt(new Date());
143
+        
144
+        dashboards.put(dashboardId, existingDashboard);
145
+        return true;
146
+    }
147
+    
148
+    @Override
149
+    public boolean deleteDashboard(String dashboardId) {
150
+        return dashboards.remove(dashboardId) != null;
151
+    }
152
+    
153
+    @Override
154
+    public boolean publishDashboard(String dashboardId) {
155
+        SelfServiceDashboard dashboard = dashboards.get(dashboardId);
156
+        if (dashboard != null) {
157
+            dashboard.setPublished(true);
158
+            dashboard.setUpdatedAt(new Date());
159
+            dashboards.put(dashboardId, dashboard);
160
+            return true;
161
+        }
162
+        return false;
163
+    }
164
+    
165
+    @Override
166
+    public boolean addComponent(String dashboardId, SelfServiceDashboard.DashboardComponent component) {
167
+        SelfServiceDashboard dashboard = dashboards.get(dashboardId);
168
+        if (dashboard != null && component != null) {
169
+            if (component.getId() == null) {
170
+                component.setId("comp_" + System.currentTimeMillis());
171
+            }
172
+            component.setVisible(true);
173
+            
174
+            dashboard.getComponents().add(component);
175
+            dashboard.setUpdatedAt(new Date());
176
+            dashboards.put(dashboardId, dashboard);
177
+            return true;
178
+        }
179
+        return false;
180
+    }
181
+    
182
+    @Override
183
+    public boolean updateComponentLayout(String dashboardId, String componentId, int x, int y, int width, int height) {
184
+        SelfServiceDashboard dashboard = dashboards.get(dashboardId);
185
+        if (dashboard != null) {
186
+            for (SelfServiceDashboard.DashboardComponent component : dashboard.getComponents()) {
187
+                if (componentId.equals(component.getId())) {
188
+                    component.setX(x);
189
+                    component.setY(y);
190
+                    component.setWidth(width);
191
+                    component.setHeight(height);
192
+                    dashboard.setUpdatedAt(new Date());
193
+                    dashboards.put(dashboardId, dashboard);
194
+                    return true;
195
+                }
196
+            }
197
+        }
198
+        return false;
199
+    }
200
+    
201
+    @Override
202
+    public boolean removeComponent(String dashboardId, String componentId) {
203
+        SelfServiceDashboard dashboard = dashboards.get(dashboardId);
204
+        if (dashboard != null) {
205
+            boolean removed = dashboard.getComponents().removeIf(component -> 
206
+                componentId.equals(component.getId())
207
+            );
208
+            if (removed) {
209
+                dashboard.setUpdatedAt(new Date());
210
+                dashboards.put(dashboardId, dashboard);
211
+            }
212
+            return removed;
213
+        }
214
+        return false;
215
+    }
216
+    
217
+    @Override
218
+    public boolean shareDashboard(String dashboardId, String userId, String role) {
219
+        SelfServiceDashboard dashboard = dashboards.get(dashboardId);
220
+        if (dashboard != null) {
221
+            // 检查是否已经分享
222
+            for (SelfServiceDashboard.DashboardUser sharedUser : dashboard.getSharedUsers()) {
223
+                if (userId.equals(sharedUser.getUserId())) {
224
+                    // 更新角色
225
+                    sharedUser.setRole(role);
226
+                    sharedUser.setSharedAt(new Date());
227
+                    dashboard.setUpdatedAt(new Date());
228
+                    dashboards.put(dashboardId, dashboard);
229
+                    return true;
230
+                }
231
+            }
232
+            
233
+            // 添加新的分享用户
234
+            SelfServiceDashboard.DashboardUser sharedUser = new SelfServiceDashboard.DashboardUser();
235
+            sharedUser.setUserId(userId);
236
+            sharedUser.setRole(role);
237
+            sharedUser.setSharedAt(new Date());
238
+            
239
+            dashboard.getSharedUsers().add(sharedUser);
240
+            dashboard.setUpdatedAt(new Date());
241
+            dashboards.put(dashboardId, dashboard);
242
+            return true;
243
+        }
244
+        return false;
245
+    }
246
+    
247
+    @Override
248
+    public boolean unshareDashboard(String dashboardId, String userId) {
249
+        SelfServiceDashboard dashboard = dashboards.get(dashboardId);
250
+        if (dashboard != null) {
251
+            boolean removed = dashboard.getSharedUsers().removeIf(sharedUser -> 
252
+                userId.equals(sharedUser.getUserId())
253
+            );
254
+            if (removed) {
255
+                dashboard.setUpdatedAt(new Date());
256
+                dashboards.put(dashboardId, dashboard);
257
+            }
258
+            return removed;
259
+        }
260
+        return false;
261
+    }
262
+    
263
+    @Override
264
+    public boolean configureSchedule(String dashboardId, SelfServiceDashboard.ScheduleConfig schedule) {
265
+        SelfServiceDashboard dashboard = dashboards.get(dashboardId);
266
+        if (dashboard != null && schedule != null) {
267
+            if (schedule.getId() == null) {
268
+                schedule.setId("sch_" + System.currentTimeMillis());
269
+            }
270
+            
271
+            // 检查是否已存在相同类型的定时配置
272
+            dashboard.getSchedules().removeIf(existing -> 
273
+                existing.getType().equals(schedule.getType())
274
+            );
275
+            
276
+            dashboard.getSchedules().add(schedule);
277
+            dashboard.setUpdatedAt(new Date());
278
+            dashboards.put(dashboardId, dashboard);
279
+            return true;
280
+        }
281
+        return false;
282
+    }
283
+    
284
+    @Override
285
+    public boolean setTheme(String dashboardId, String theme) {
286
+        SelfServiceDashboard dashboard = dashboards.get(dashboardId);
287
+        if (dashboard != null && StringUtils.hasText(theme)) {
288
+            dashboard.setTheme(theme);
289
+            dashboard.setUpdatedAt(new Date());
290
+            dashboards.put(dashboardId, dashboard);
291
+            return true;
292
+        }
293
+        return false;
294
+    }
295
+    
296
+    @Override
297
+    public String copyDashboard(String dashboardId, String newName) {
298
+        SelfServiceDashboard originalDashboard = dashboards.get(dashboardId);
299
+        if (originalDashboard != null) {
300
+            // 创建副本
301
+            SelfServiceDashboard newDashboard = new SelfServiceDashboard();
302
+            
303
+            // 复制基本信息
304
+            newDashboard.setName(newName);
305
+            newDashboard.setDescription("原看板:" + originalDashboard.getName() + " 的副本");
306
+            newDashboard.setTheme(originalDashboard.getTheme());
307
+            newDashboard.setLayout(originalDashboard.getLayout());
308
+            newDashboard.setPermission(originalDashboard.getPermission());
309
+            newDashboard.setDataRefresh(originalDashboard.getDataRefresh());
310
+            newDashboard.setPublished(false);
311
+            newDashboard.setCreatedBy("system_copy");
312
+            
313
+            // 复制组件(创建新的ID避免冲突)
314
+            List<SelfServiceDashboard.DashboardComponent> newComponents = new ArrayList<>();
315
+            for (SelfServiceDashboard.DashboardComponent component : originalDashboard.getComponents()) {
316
+                SelfServiceDashboard.DashboardComponent newComponent = new SelfServiceDashboard.DashboardComponent();
317
+                newComponent.setId("comp_" + System.currentTimeMillis());
318
+                newComponent.setType(component.getType());
319
+                newComponent.setTitle(component.getTitle());
320
+                newComponent.setDescription(component.getDescription());
321
+                newComponent.setConfig(new HashMap<>(component.getConfig()));
322
+                newComponent.setX(component.getX());
323
+                newComponent.setY(component.getY());
324
+                newComponent.setWidth(component.getWidth());
325
+                newComponent.setHeight(component.getHeight());
326
+                newComponent.setVisible(component.isVisible());
327
+                newComponent.setDatasetId(component.getDatasetId());
328
+                newComponent.setChartId(component.getChartId());
329
+                newComponents.add(newComponent);
330
+            }
331
+            newDashboard.setComponents(newComponents);
332
+            
333
+            // 保存新看板
334
+            return createSelfServiceDashboard(newDashboard);
335
+        }
336
+        return null;
337
+    }
338
+    
339
+    @Override
340
+    public Map<String, Object> getDashboardStats(String dashboardId) {
341
+        SelfServiceDashboard dashboard = dashboards.get(dashboardId);
342
+        Map<String, Object> stats = new HashMap<>();
343
+        
344
+        if (dashboard != null) {
345
+            stats.put("dashboardId", dashboardId);
346
+            stats.put("name", dashboard.getName());
347
+            stats.put("published", dashboard.isPublished());
348
+            stats.put("createdAt", dashboard.getCreatedAt());
349
+            stats.put("updatedAt", dashboard.getUpdatedAt());
350
+            stats.put("componentsCount", dashboard.getComponents().size());
351
+            stats.put("sharedUsersCount", dashboard.getSharedUsers().size());
352
+            stats.put("schedulesCount", dashboard.getSchedules().size());
353
+            
354
+            // 组件类型统计
355
+            Map<String, Integer> componentTypeStats = new HashMap<>();
356
+            for (SelfServiceDashboard.DashboardComponent component : dashboard.getComponents()) {
357
+                componentTypeStats.put(
358
+                    component.getType(),
359
+                    componentTypeStats.getOrDefault(component.getType(), 0) + 1
360
+                );
361
+            }
362
+            stats.put("componentTypeStats", componentTypeStats);
363
+            
364
+            // 计算最近使用时间(模拟)
365
+            stats.put("lastUsed", dashboard.getUpdatedAt());
366
+            stats.put("viewCount", (int)(Math.random() * 1000)); // 模拟浏览次数
367
+        } else {
368
+            stats.put("error", "看板不存在");
369
+        }
370
+        
371
+        return stats;
372
+    }
373
+    
374
+    @Override
375
+    public List<SelfServiceDashboard> searchDashboards(String keyword, String userId) {
376
+        List<SelfServiceDashboard> result = new ArrayList<>();
377
+        String keywordLower = keyword.toLowerCase();
378
+        
379
+        for (SelfServiceDashboard dashboard : dashboards.values()) {
380
+            // 检查用户是否有权限访问该看板
381
+            boolean hasAccess = userId.equals(dashboard.getCreatedBy());
382
+            if (!hasAccess) {
383
+                for (SelfServiceDashboard.DashboardUser sharedUser : dashboard.getSharedUsers()) {
384
+                    if (userId.equals(sharedUser.getUserId())) {
385
+                        hasAccess = true;
386
+                        break;
387
+                    }
388
+                }
389
+            }
390
+            
391
+            if (hasAccess) {
392
+                // 搜索匹配
393
+                boolean matches = false;
394
+                if (keywordLower.isEmpty()) {
395
+                    matches = true;
396
+                } else {
397
+                    if (dashboard.getName() != null && 
398
+                        dashboard.getName().toLowerCase().contains(keywordLower)) {
399
+                        matches = true;
400
+                    }
401
+                    if (dashboard.getDescription() != null && 
402
+                        dashboard.getDescription().toLowerCase().contains(keywordLower)) {
403
+                        matches = true;
404
+                    }
405
+                    for (SelfServiceDashboard.DashboardComponent component : dashboard.getComponents()) {
406
+                        if (component.getTitle() != null && 
407
+                            component.getTitle().toLowerCase().contains(keywordLower)) {
408
+                            matches = true;
409
+                            break;
410
+                        }
411
+                    }
412
+                }
413
+                
414
+                if (matches) {
415
+                    result.add(dashboard);
416
+                }
417
+            }
418
+        }
419
+        
420
+        // 按创建时间降序排列
421
+        result.sort((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt()));
422
+        
423
+        return result;
424
+    }
425
+    
426
+    /**
427
+     * 创建默认组件
428
+     */
429
+    private void createDefaultComponents(String dashboardId) {
430
+        SelfServiceDashboard dashboard = dashboards.get(dashboardId);
431
+        if (dashboard == null) return;
432
+        
433
+        List<SelfServiceDashboard.DashboardComponent> components = new ArrayList<>();
434
+        
435
+        // 1. 关键指标卡片
436
+        SelfServiceDashboard.DashboardComponent metricCard = new SelfServiceDashboard.DashboardComponent();
437
+        metricCard.setId("metric_total_usage");
438
+        metricCard.setType("metric");
439
+        metricCard.setTitle("总用水量");
440
+        metricCard.setDescription("今日系统总用水量");
441
+        metricCard.setX(0);
442
+        metricCard.setY(0);
443
+        metricCard.setWidth(6);
444
+        metricCard.setHeight(3);
445
+        metricCard.setVisible(true);
446
+        
447
+        Map<String, Object> metricConfig = new HashMap<>();
448
+        metricConfig.put("value", "12,345");
449
+        metricConfig.put("unit", "立方米");
450
+        metricConfig.put("trend", "up");
451
+        metricConfig.put("color", "#007bff");
452
+        metricCard.setConfig(metricConfig);
453
+        components.add(metricCard);
454
+        
455
+        // 2. 水质指标仪表盘
456
+        SelfServiceDashboard.DashboardComponent gaugeCard = new SelfServiceDashboard.DashboardComponent();
457
+        gaugeCard.setId("gauge_water_quality");
458
+        gaugeCard.setType("gauge");
459
+        gaugeCard.setTitle("水质达标率");
460
+        gaugeCard.setDescription("水质综合指标达标率");
461
+        gaugeCard.setX(6);
462
+        gaugeCard.setY(0);
463
+        gaugeCard.setWidth(6);
464
+        gaugeCard.setHeight(3);
465
+        gaugeCard.setVisible(true);
466
+        
467
+        Map<String, Object> gaugeConfig = new HashMap<>();
468
+        gaugeConfig.put("value", "95");
469
+        gaugeConfig.put("min", 0);
470
+        gaugeConfig.put("max", 100);
471
+        gaugeConfig.put("unit", "%");
472
+        gaugeConfig.put("color", "#28a745");
473
+        gaugeCard.setConfig(gaugeConfig);
474
+        components.add(gaugeCard);
475
+        
476
+        // 3. 用水量趋势图
477
+        SelfServiceDashboard.DashboardComponent trendChart = new SelfServiceDashboard.DashboardComponent();
478
+        trendChart.setId("chart_water_trend");
479
+        trendChart.setType("line");
480
+        trendChart.setTitle("用水量趋势");
481
+        trendChart.setDescription("最近7天用水量变化");
482
+        trendChart.setX(0);
483
+        trendChart.setY(3);
484
+        trendChart.setWidth(12);
485
+        trendChart.setHeight(6);
486
+        trendCard.setVisible(true);
487
+        
488
+        Map<String, Object> trendConfig = new HashMap<>();
489
+        trendConfig.put("dataset", "water_consumption_ds");
490
+        trendConfig.put("xField", "date");
491
+        trendConfig.put("yField", "consumption");
492
+        trendConfig.put("title", "近7天用水量趋势");
493
+        trendConfig.put("legend", true);
494
+        trendChart.setConfig(trendConfig);
495
+        components.add(trendChart);
496
+        
497
+        // 4. 区域用水量对比
498
+        SelfServiceDashboard.DashboardComponent barChart = new SelfServiceDashboard.DashboardComponent();
499
+        barChart.setId("chart_region_comparison");
500
+        barChart.setType("bar");
501
+        barChart.setTitle("区域用水量对比");
502
+        barChart.setDescription("各区域今日用水量统计");
503
+        barChart.setX(0);
504
+        barChart.setY(9);
505
+        barChart.setWidth(12);
506
+        barChart.setHeight(6);
507
+        barCard.setVisible(true);
508
+        
509
+        Map<String, Object> barConfig = new HashMap<>();
510
+        barConfig.put("dataset", "water_region_ds");
511
+        barConfig.put("xField", "region");
512
+        barConfig.put("yField", "consumption");
513
+        barConfig.put("title", "各区域用水量对比");
514
+        barConfig.put("legend", false);
515
+        barChart.setConfig(barConfig);
516
+        components.add(barChart);
517
+        
518
+        // 更新看板
519
+        dashboard.setComponents(components);
520
+        dashboard.setUpdatedAt(new Date());
521
+        dashboards.put(dashboardId, dashboard);
522
+    }
523
+}