liujie
2026-07-31 67de309a7c464369026427813746ad86e39bd155
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
package com.ruoyi.web.controller.tool;
 
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.dto.SysDoorDevicePageRequest;
import com.ruoyi.system.dto.SysEmployeePageRequest;
import com.ruoyi.system.dto.SysVisitorMachinePageRequest;
import com.ruoyi.system.dto.SysVisitAreaTreeRequest;
 
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
/**
 * 集群版对接中台时,将基础信息中台查询结果适配成当前页面字段。
 *
 * <p>目的:前端页面已绑定本系统字段名,直接透传中台字段会导致表格空列。</p>
 * <p>影响范围:部门、区域、员工、访客机、门禁设备的查询和详情接口。</p>
 * <p>注意:访客记录、入园承诺、版本更新暂无明确基础信息中台接口映射,仍由只读拦截保证不能写。</p>
 */
public final class MiddlePlatformDataQuery {
 
    /**
     * 目的:工具类只提供静态中台查询适配方法,禁止实例化。
     */
    private MiddlePlatformDataQuery() {
    }
 
    public static String login(String username, String password) throws IOException {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("username", username);
        body.put("password", password);
        JSONObject root = JSON.parseObject(BaseDataApiUtil.requestJson(BaseDataApiUtil.Api.POST_AUTH_USER_LOGIN, body));
        if (root == null) {
            throw new IOException("middle platform login returned empty response");
        }
        if (Boolean.FALSE.equals(root.getBoolean("success"))) {
            if (isNotFoundResponse(root)) {
                return null;
            }
            throw new IOException(StringUtils.defaultIfEmpty(root.getString("description"), "middle platform login failed"));
        }
        String token = root.getString("data");
        if (!StringUtils.isNotBlank(token)) {
            throw new IOException("middle platform login returned empty token");
        }
        return token;
    }
 
    public static void logout(String token) throws IOException {
        if (!StringUtils.isNotBlank(token)) {
            return;
        }
        Map<String, String> headers = new LinkedHashMap<>();
        headers.put("Authorization", "Bearer " + token);
        BaseDataApiUtil.request(BaseDataApiUtil.Api.POST_AUTH_USER_LOGOUT, null, Collections.emptyMap(), headers);
    }
 
    /**
     * 目的:查询中台区域树,并转换为访问区域页面使用的 areaId/areaName/children 字段。
     */
    public static Object areaTree(SysVisitAreaTreeRequest request) throws IOException {
        Map<String, Object> params = new LinkedHashMap<>();
        Object data = data(BaseDataApiUtil.request(BaseDataApiUtil.Api.GET_REGION_TREE, params));
        String keyword = request == null ? null : request.getAreaName();
        return mapRegionTree(data, keyword);
    }
 
    /**
     * 目的:按区域 ID 查询中台区域详情,供区域详情弹窗复用。
     */
    public static Object areaDetail(Object areaId) throws IOException {
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("id", areaId);
        return mapRegion(data(BaseDataApiUtil.request(BaseDataApiUtil.Api.GET_REGION_DETAILS, params)), null);
    }
 
    /**
     * 目的:查询中台组织树并扁平化为若依部门列表,兼容前端 handleTree。
     */
    public static Object deptList(SysDept dept) throws IOException {
        Object data = data(BaseDataApiUtil.request(BaseDataApiUtil.Api.GET_GROUP_TREE));
        List<Map<String, Object>> rows = new ArrayList<>();
        flattenGroup(data, rows, null, dept == null ? null : dept.getDeptName());
        return rows;
    }
 
    /**
     * 目的:按组织 ID 查询中台组织详情,并适配为 SysDept 页面字段。
     */
    public static Object deptDetail(Object deptId) throws IOException {
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("id", deptId);
        return mapGroup(data(BaseDataApiUtil.request(BaseDataApiUtil.Api.GET_GROUP_GET, params)), null);
    }
 
    /**
     * 目的:查询中台人员分页数据,并转换为员工管理页面字段。
     */
    public static Object employeePage(SysEmployeePageRequest request) throws IOException {
        SysEmployeePageRequest query = request == null ? new SysEmployeePageRequest() : request;
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("current", defaultPageNum(query.getPageNum()));
        params.put("size", defaultPageSize(query.getPageSize()));
        // 中台人员列表只有 keyword 模糊查询,合并本地姓名、编号、电话、岗位等查询条件。
        putIfNotBlank(params, "keyword", firstNotBlank(query.getEmployeeName(), query.getEmployeeNo(),
                query.getContactPhone(), query.getPosition()));
        if (query.getDeptId() != null) {
            params.put("orgId", query.getDeptId());
        }
        return mapPage(data(BaseDataApiUtil.request(BaseDataApiUtil.Api.GET_PERSON_LIST, params)), true);
    }
 
    /**
     * 目的:按人员 ID 查询中台人员详情,保持员工列表和详情数据源一致。
     */
    public static Object employeeDetail(Object employeeId) throws IOException {
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("id", employeeId);
        return mapPerson(data(BaseDataApiUtil.request(BaseDataApiUtil.Api.GET_PERSON, params)));
    }
 
    /**
     * 目的:查询中台设备分页数据,并按访客机页面字段返回。
     */
    public static Object visitorMachinePage(SysVisitorMachinePageRequest request) throws IOException {
        SysVisitorMachinePageRequest query = request == null ? new SysVisitorMachinePageRequest() : request;
        return devicePage(query.getPageNum(), query.getPageSize(), query.getAreaId(), query.getDeviceName(),
                query.getDeviceId(), query.getOnlineStatus(), "machine");
    }
 
    /**
     * 目的:查询中台设备分页数据,并按门禁设备页面字段返回。
     */
    public static Object doorDevicePage(SysDoorDevicePageRequest request) throws IOException {
        SysDoorDevicePageRequest query = request == null ? new SysDoorDevicePageRequest() : request;
        return devicePage(query.getPageNum(), query.getPageSize(), query.getAreaId(), query.getDeviceName(),
                query.getDeviceId(), query.getOnlineStatus(), "door");
    }
 
    /**
     * 目的:按设备标识查询访客机详情,并补齐 machineId 字段。
     */
    public static Object visitorMachineDetail(Object machineId) throws IOException {
        return deviceDetail(machineId, "machine");
    }
 
    /**
     * 目的:按设备标识查询门禁设备详情,并补齐 doorId 字段。
     */
    public static Object doorDeviceDetail(Object doorId) throws IOException {
        return deviceDetail(doorId, "door");
    }
 
    /**
     * 目的:组装中台设备分页查询参数,并按设备类型映射主键字段名。
     */
    private static Object devicePage(Integer pageNum, Integer pageSize, Object areaId, String deviceName,
                                     String deviceId, String onlineStatus, String idName) throws IOException {
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("current", defaultPageNum(pageNum));
        params.put("size", defaultPageSize(pageSize));
        putIfNotBlank(params, "name", firstNotBlank(deviceName, deviceId));
        putIfNotBlank(params, "onlineStatus", toMiddleOnlineStatus(onlineStatus));
        if (areaId != null) {
            // 中台设备接口按 mapIds 过滤区域,本地页面只传单个 areaId。
            params.put("mapIds", Collections.singletonList(areaId));
        }
        return mapPage(data(BaseDataApiUtil.request(BaseDataApiUtil.Api.POST_HIK_DEVICE_LIST, params)), false, idName);
    }
 
    /**
     * 目的:调用中台设备详情接口,并按页面类型映射 machineId 或 doorId。
     */
    private static Object deviceDetail(Object deviceId, String idName) throws IOException {
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("id", deviceId);
        return mapDevice(data(BaseDataApiUtil.request(BaseDataApiUtil.Api.GET_HIK_DEVICE, params)), idName);
    }
 
    /**
     * 目的:分页转换入口,默认不需要设备主键字段名。
     */
    private static Map<String, Object> mapPage(Object data, boolean person) {
        return mapPage(data, person, null);
    }
 
    /**
     * 目的:把中台分页结构转换为前端 getPageRows/getPageTotal 可识别的结构。
     */
    private static Map<String, Object> mapPage(Object data, boolean person, String deviceIdName) {
        JSONObject page = asObject(data);
        JSONArray records = toArray(page.get("records"));
        List<Map<String, Object>> rows = new ArrayList<>();
        for (Object record : records) {
            rows.add(person ? mapPerson(record) : mapDevice(record, deviceIdName));
        }
 
        // 保持 records/total/current/size 结构,兼容前端 getPageRows/getPageTotal。
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("records", rows);
        result.put("total", firstValue(page, "total", "totalCount", "dataSize"));
        result.put("current", page.get("current"));
        result.put("size", page.get("size"));
        return result;
    }
 
    /**
     * 目的:递归转换中台区域树,并支持区域名称关键字过滤。
     */
    private static List<Map<String, Object>> mapRegionTree(Object data, String keyword) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (Object item : toArray(data)) {
            Map<String, Object> region = mapRegion(item, keyword);
            if (region != null) {
                result.add(region);
            }
        }
        return result;
    }
 
    /**
     * 目的:转换单个中台区域节点为本地访问区域节点。
     */
    private static Map<String, Object> mapRegion(Object source, String keyword) {
        JSONObject item = asObject(source);
        List<Map<String, Object>> children = mapRegionTree(item.get("children"), keyword);
        String areaName = item.getString("name");
        boolean matched = !StringUtils.isNotBlank(keyword) || StringUtils.contains(areaName, keyword);
        // 搜索区域时保留命中的父级节点,避免树结构断层。
        if (!matched && children.isEmpty()) {
            return null;
        }
 
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("areaId", item.get("id"));
        result.put("parentId", defaultParentId(item.get("parentId")));
        result.put("parentName", item.get("parentName"));
        result.put("areaCode", item.get("id"));
        result.put("areaName", areaName);
        result.put("areaDesc", firstValue(item, "remark", "type"));
        result.put("orderNum", item.get("level"));
        result.put("createTime", item.get("createTime"));
        result.put("children", children);
        return result;
    }
 
    /**
     * 目的:将中台组织树拍平,供若依部门 handleTree 重新组装层级。
     */
    private static void flattenGroup(Object data, List<Map<String, Object>> rows, Object parentId, String keyword) {
        for (Object item : toArray(data)) {
            JSONObject group = asObject(item);
            Map<String, Object> mapped = mapGroup(group, parentId);
            if (!StringUtils.isNotBlank(keyword) || StringUtils.contains(String.valueOf(mapped.get("deptName")), keyword)) {
                rows.add(mapped);
            }
            // 中台组织树可能没有 parentId,递归时用父节点 id 补齐,供前端 handleTree 还原层级。
            flattenGroup(group.get("children"), rows, mapped.get("deptId"), keyword);
        }
    }
 
    /**
     * 目的:转换单个中台组织节点为本地部门字段。
     */
    private static Map<String, Object> mapGroup(Object source, Object fallbackParentId) {
        JSONObject item = asObject(source);
        Map<String, Object> result = new LinkedHashMap<>();
        Object id = item.get("id");
        Object parentId = firstValue(item, "parentId", "pid", "parentID", "parent");
        if (parentId == null) {
            parentId = fallbackParentId;
        }
        result.put("deptId", id);
        result.put("parentId", defaultParentId(parentId));
        result.put("deptName", firstValue(item, "name", "orgName"));
        result.put("orderNum", firstValue(item, "sortNumber", "sort", "orderNum"));
        result.put("status", "0");
        result.put("children", Collections.emptyList());
        return result;
    }
 
    /**
     * 目的:转换单个中台人员记录为员工管理页面字段。
     */
    private static Map<String, Object> mapPerson(Object source) {
        JSONObject item = asObject(source);
        Map<String, Object> result = new LinkedHashMap<>();
        Object id = item.get("id");
        result.put("employeeId", id);
        result.put("deptId", item.get("orgId"));
        result.put("deptName", item.get("orgName"));
        result.put("employeeName", item.get("name"));
        result.put("employeeNo", firstValue(item, "personNumber", "jobCode"));
        result.put("contactPhone", item.get("phone"));
        result.put("position", item.get("position"));
        result.put("positionName", item.get("position"));
        result.put("cardNo", item.get("certNo"));
        result.put("faceImage", item.get("faceId"));
        result.put("enabled", !"1".equals(String.valueOf(item.get("isDelete"))));
        result.put("createTime", item.get("createTime"));
        return result;
    }
 
    /**
     * 目的:转换单个中台设备记录为访客机或门禁设备页面字段。
     */
    private static Map<String, Object> mapDevice(Object source, String idName) {
        JSONObject item = asObject(source);
        Map<String, Object> result = new LinkedHashMap<>();
        Object id = firstValue(item, "indexCode", "id");
        result.put(idName == null ? "deviceKey" : idName + "Id", id);
        result.put("areaId", item.get("mapId"));
        result.put("areaName", firstValue(item, "regionName", "mapName"));
        result.put("deviceName", item.get("name"));
        result.put("brand", item.get("deviceType"));
        result.put("deviceId", id);
        result.put("enabled", !"1".equals(String.valueOf(item.get("isDelete"))));
        result.put("onlineStatus", toLocalOnlineStatus(item.get("onlineStatus")));
        result.put("remark", firstValue(item, "remark", "ipAddress"));
        result.put("createTime", item.get("createTime"));
        return result;
    }
 
    /**
     * 目的:解析中台标准响应,失败时统一转成 IOException。
     */
    private static Object data(String content) throws IOException {
        JSONObject root = JSON.parseObject(content);
        if (root == null) {
            return null;
        }
        // 中台标准响应 success=false 时转成 IOException,由 controller 统一返回业务错误。
        if (Boolean.FALSE.equals(root.getBoolean("success"))) {
            throw new IOException(StringUtils.defaultIfEmpty(root.getString("description"), "中台接口返回失败"));
        }
        return root.get("data");
    }
 
    private static boolean isNotFoundResponse(JSONObject root) {
        Object code = firstValue(root, "errorCode", "code");
        String codeText = code == null ? "" : String.valueOf(code);
        String description = StringUtils.defaultString(root.getString("description")).toLowerCase();
        return "404".equals(codeText)
                || description.contains("not found")
                || description.contains("\u4e0d\u5b58\u5728")
                || description.contains("\u672a\u67e5\u8be2")
                || description.contains("\u672a\u627e\u5230");
    }
 
    /**
     * 目的:把任意响应片段安全转换为 JSONObject,减少空值判断分散。
     */
    private static JSONObject asObject(Object value) {
        if (value instanceof JSONObject) {
            return (JSONObject) value;
        }
        if (value == null) {
            return new JSONObject();
        }
        return JSON.parseObject(JSON.toJSONString(value));
    }
 
    /**
     * 目的:把中台可能返回的数组、集合或单对象统一转换为 JSONArray。
     */
    private static JSONArray toArray(Object value) {
        if (value instanceof JSONArray) {
            return (JSONArray) value;
        }
        JSONArray array = new JSONArray();
        if (value == null) {
            return array;
        }
        if (value instanceof Iterable) {
            for (Object item : (Iterable<?>) value) {
                array.add(item);
            }
            return array;
        }
        array.add(value);
        return array;
    }
 
    /**
     * 目的:规范页码默认值,避免中台接口收到无效页码。
     */
    private static int defaultPageNum(Integer pageNum) {
        return pageNum == null || pageNum < 1 ? 1 : pageNum;
    }
 
    /**
     * 目的:规范分页大小默认值,避免中台接口收到无效分页大小。
     */
    private static int defaultPageSize(Integer pageSize) {
        return pageSize == null || pageSize < 1 ? 10 : pageSize;
    }
 
    /**
     * 目的:将空父节点统一转换为 0,兼容若依根部门和根区域规则。
     */
    private static Object defaultParentId(Object parentId) {
        return parentId == null || "".equals(String.valueOf(parentId)) ? 0 : parentId;
    }
 
    /**
     * 目的:只写入非空查询参数,避免空字符串影响中台筛选。
     */
    private static void putIfNotBlank(Map<String, Object> params, String key, String value) {
        if (StringUtils.isNotBlank(value)) {
            params.put(key, value);
        }
    }
 
    /**
     * 目的:从多个本地查询条件中取第一个有效值映射到中台 keyword/name。
     */
    private static String firstNotBlank(String... values) {
        if (values == null) {
            return null;
        }
        for (String value : values) {
            if (StringUtils.isNotBlank(value)) {
                return value;
            }
        }
        return null;
    }
 
    /**
     * 目的:按候选字段顺序读取中台响应,兼容不同接口字段命名。
     */
    private static Object firstValue(JSONObject item, String... keys) {
        for (String key : keys) {
            Object value = item.get(key);
            if (value != null && !"".equals(String.valueOf(value))) {
                return value;
            }
        }
        return null;
    }
 
    /**
     * 目的:将本地 online/offline 查询值转换为中台 1/0 查询值。
     */
    private static String toMiddleOnlineStatus(String value) {
        if (!StringUtils.isNotBlank(value)) {
            return null;
        }
        if ("online".equalsIgnoreCase(value)) {
            return "1";
        }
        if ("offline".equalsIgnoreCase(value)) {
            return "0";
        }
        return value;
    }
 
    /**
     * 目的:将中台 1/0 在线状态转换为本地数据库使用的在线状态枚举。
     */
    private static String toLocalOnlineStatus(Object value) {
        return "1".equals(String.valueOf(value)) || "online".equalsIgnoreCase(String.valueOf(value))
                ? "ONLINE" : "OFFLINE";
    }
}