yxh
6 天以前 68ff33beff11310e830b0615b5a81f83fad0f683
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
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
package cn.lihu.jh.module.ecg.controller.admin.room;
 
import java.util.*;
import java.io.IOException;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
 
import cn.hutool.extra.servlet.ServletUtil;
import cn.lihu.jh.module.ecg.utils.IpUtils;
import cn.lihu.jh.module.ecg.controller.admin.room.vo.*;
import cn.lihu.jh.module.ecg.dal.dataobject.queue.QueueDO;
import cn.lihu.jh.module.ecg.dal.dataobject.room.RoomDO;
import cn.lihu.jh.module.ecg.dal.dataobject.room.RoomProfile;
import cn.lihu.jh.module.ecg.service.queue.QueueService;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
 
import cn.lihu.jh.module.ecg.service.room.RoomService;
import cn.lihu.jh.module.system.api.dept.DeptApi;
import cn.lihu.jh.module.system.api.dept.dto.DeptRespDTO;
import cn.lihu.jh.framework.common.pojo.PageParam;
import cn.lihu.jh.framework.common.pojo.PageResult;
import cn.lihu.jh.framework.common.pojo.CommonResult;
import cn.lihu.jh.framework.common.exception.ErrorCode;
import cn.lihu.jh.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.lihu.jh.framework.common.util.object.BeanUtils;
import cn.lihu.jh.framework.excel.core.util.ExcelUtils;
import cn.lihu.jh.framework.apilog.core.annotation.ApiAccessLog;
import cn.lihu.jh.framework.security.core.util.SecurityFrameworkUtils;
import lombok.extern.slf4j.Slf4j;
 
import static cn.lihu.jh.framework.common.pojo.CommonResult.success;
import static cn.lihu.jh.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.lihu.jh.framework.apilog.core.enums.OperateTypeEnum.*;
 
 
@Tag(name = "管理后台 - 诊室和诊疗床")
@RestController
@RequestMapping("/clinic/room")
@Validated
@Slf4j
public class RoomController {
 
    @Resource
    private RoomService roomService;
 
    @Resource
    private QueueService queueService;
 
    @Resource
    private DeptApi deptApi;
 
    @PostMapping("/create")
    @Operation(summary = "创建诊室和诊疗床")
    @PreAuthorize("@ss.hasPermission('clinic:room:create')")
    public CommonResult<Integer> createRoom(@Valid @RequestBody RoomSaveReqVO createReqVO) {
        return success(roomService.createRoom(createReqVO));
    }
 
    @PutMapping("/update")
    @Operation(summary = "更新诊室和诊疗床")
    @PreAuthorize("@ss.hasPermission('clinic:room:update')")
    public CommonResult<Boolean> updateRoom(@Valid @RequestBody RoomSaveReqVO updateReqVO) {
        roomService.updateRoom(updateReqVO);
        return success(true);
    }
 
    @DeleteMapping("/delete")
    @Operation(summary = "删除诊室和诊疗床")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('clinic:room:delete')")
    public CommonResult<Boolean> deleteRoom(@RequestParam("id") Integer id) {
        roomService.deleteRoom(id);
        return success(true);
    }
 
    @GetMapping("/get")
    @Operation(summary = "获得诊室和诊疗床")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('clinic:room:query')")
    public CommonResult<RoomRespVO> getRoom(@RequestParam("id") Integer id) {
        RoomDO room = roomService.getRoom(id);
        return success(BeanUtils.toBean(room, RoomRespVO.class));
    }
 
    @GetMapping("/get-room-by-ip")
    @Operation(summary = "获得诊室和诊疗床")
    @PermitAll
    public CommonResult<RoomProfileRespVO> getRoomByIP(HttpServletRequest request, @RequestParam(value = "roomId", required = false) Long roomId) {
        String reqIp = ServletUtil.getClientIP(request);
        RoomProfile roomProfile = roomService.getRoomByIP(reqIp, roomId);
        return success(BeanUtils.toBean(roomProfile, RoomProfileRespVO.class));
    }
 
    /* ==================== 选择工位模式(需求:诊室工位选择模式 0/1/2/3) ==================== */
 
    /**
     * 按本机IP匹配工位(模式 1/3 用)。
     *
     * <p>只读接口:
     * <ul>
     *   <li>把后端实际识别到的来访IP一起返回,便于排查"前置代理未透传 X-Forwarded-For";</li>
     *   <li>候选工位已按「本人已占用 → 空闲 → 被他人占用」排好序,前端取第一个可用的自动入座;</li>
     *   <li>本接口不改任何状态,入座仍走原有的 bedDoctorOn 流程。</li>
     * </ul>
     * 需要登录(未登录时返回空结果),但不做菜单权限校验:能登录就能查自己的工位。</p>
     */
    @GetMapping("/get-ip-bed")
    @Operation(summary = "按本机IP匹配工位(选择工位模式 1/3)")
    public CommonResult<RoomSelectBedRespVO> getIpBed(HttpServletRequest request) {
        // 规范化:去括号/区域号、统一小写、IPv4-mapped IPv6 还原成 IPv4(支持 IPv4 与 IPv6)
        String clientIp = IpUtils.normalize(ServletUtil.getClientIP(request));
        Long docId = SecurityFrameworkUtils.getLoginUserId();
        RoomSelectBedRespVO resp = new RoomSelectBedRespVO();
        resp.setClientIp(clientIp);
        resp.setIpValid(IpUtils.isBindableIp(clientIp));
        resp.setIpLoopback(IpUtils.isLoopback(clientIp));
        resp.setIpv6(IpUtils.isIpv6Address(clientIp));
        List<RoomSelectBedRespVO.RoomSelectBedItem> beds = roomService.matchBedsByIp(clientIp, docId);
        resp.setBeds(beds);
        resp.setIpBound(!beds.isEmpty());
        // 有"本人已占用"或"空闲"的工位,就能自动入座
        resp.setMatched(beds.stream()
                .anyMatch(b -> Boolean.TRUE.equals(b.getMine()) || !Boolean.TRUE.equals(b.getOccupied())));
        log.info("[getIpBed][按IP匹配工位] clientIp={} docId={} ipValid={} loopback={} 候选数={} 可自动入座={}",
                clientIp, docId, resp.getIpValid(), resp.getIpLoopback(), beds.size(), resp.getMatched());
        return success(resp);
    }
 
    /**
     * 把本机IP写入指定工位(模式 2/3 用)。
     *
     * <p>只更新 {@code clinic_room.ip} 一个字段,不走 {@code clinic:room:update}(否则为了选工位还得给医生开诊室管理权限),
     * 也不会碰 status/doc_id/check_types 等其它字段。
     * 若该IP已绑在别的工位上,默认返回冲突工位让前端确认;确认后带 {@code force=true} 再调一次即覆盖。</p>
     */
    @PostMapping("/bind-ip")
    @Operation(summary = "把本机IP绑定到工位(选择工位模式 2/3)")
    public CommonResult<Map<String, Object>> bindIp(HttpServletRequest request,
                                                    @RequestParam("id") Integer id,
                                                    @RequestParam(value = "force", required = false, defaultValue = "false") Boolean force) {
        Map<String, Object> data = new HashMap<>();
        String rawIp = ServletUtil.getClientIP(request);
        String clientIp = IpUtils.normalize(rawIp);
        data.put("clientIp", clientIp);
        if (!IpUtils.isBindableIp(clientIp)) {
            data.put("code", 400);
            if (IpUtils.isLoopback(clientIp)) {
                // 浏览器和服务在同一台机器上访问时必然出现:IPv4 是 127.0.0.1,IPv6 是 ::1
                data.put("msg", "无法绑定工位:检测到当前访问来自本机回环地址(" + clientIp
                        + ",即本机访问),请在医生实际使用的电脑上访问后再绑定");
            } else if (IpUtils.isUnspecified(clientIp)) {
                data.put("msg", "无法绑定工位:当前识别到的地址是未指定地址(" + clientIp + ")");
            } else if (IpUtils.isLocalAddress(clientIp)) {
                // 本次新增:现场最常见的"IP 不对"就是这种 —— 在服务器上(或远程桌面里)访问系统,
                // 客户端本来就是服务器自己;若把它写进工位,该工位以后永远匹配不到医生电脑。
                data.put("msg", "无法绑定工位:识别到的是**服务器自身地址**(" + clientIp
                        + ")—— 说明当前是在服务器上访问,或经远程桌面/本机代理访问,这不是医生电脑的 IP。"
                        + "请在医生实际使用的电脑上、用服务器的 IPv4 地址访问后再绑定"
                        + "(可用「IP 诊断」查看后端到底把谁当成了客户端)");
                log.warn("[bindIp][识别到服务器自身地址,拒绝绑定] clientIp={} bedId={} 服务器网卡IPv4={}",
                        clientIp, id, IpUtils.localIpv4Addresses());
            } else if (IpUtils.isIpv6Address(clientIp)) {
                // 本系统工位绑定统一采用 IPv4;客户端走 IPv6 时无法从代码"退化成"IPv4
                data.put("msg", "无法绑定工位:当前访问使用的是 IPv6 地址(" + clientIp
                        + "),本系统工位绑定仅采用 IPv4,请改用服务器的 IPv4 地址访问"
                        + "(或让服务只监听 IPv4:JVM 参数 -Djava.net.preferIPv4Stack=true)");
            } else {
                data.put("msg", "无法绑定工位:未能识别到合法的 IPv4 来访地址(" + clientIp
                        + "),请检查是否通过代理访问、以及代理是否透传客户端地址(X-Forwarded-For)");
            }
            log.warn("[bindIp][IP不可用,拒绝绑定] raw={} normalized={} ipv6={} bedId={}",
                    rawIp, clientIp, IpUtils.isIpv6Address(clientIp), id);
            return success(data);
        }
        // 冲突检测:该IP已绑在别的工位 → 先让前端确认
        List<RoomDO> conflicts = roomService.listIpBindings(clientIp, id);
        if (!conflicts.isEmpty() && !Boolean.TRUE.equals(force)) {
            RoomDO c = conflicts.get(0);
            data.put("code", 409);
            data.put("conflict", true);
            data.put("conflictBedId", c.getId());
            data.put("conflictRoomName", c.getRoomName());
            data.put("conflictBedNo", c.getBedNo());
            data.put("msg", "该IP已绑定「" + c.getRoomName() + " / " + c.getBedNo() + "」");
            return success(data);
        }
        Integer rows = roomService.bindIpToBed(id, clientIp);
        data.put("code", rows != null && rows > 0 ? 200 : 500);
        data.put("msg", rows != null && rows > 0 ? "已绑定本机IP到该工位" : "绑定失败(工位不存在?)");
        data.put("rows", rows);
        log.info("[bindIp][绑定结果] raw={} clientIp={} bedId={} force={} rows={}", rawIp, clientIp, id, force, rows);
        return success(data);
    }
 
    /* ==================== 自动入座 / 我的工位(本次需求) ==================== */
 
    /**
     * 一键自动入座(选择工位模式 1/3 用)。
     *
     * <p>为什么合并成一个接口:前端原来把「匹配 → 绑 IP → 入座」拆成三个请求自己拼,
     * 任一步失败都只能回落"手选工位",现场表现就是"登录后没有自动入座"且看不出原因。
     * 现在一次调用完成,并返回**识别到的 IP** 与**未入座原因**
     * ({@code ip_invalid / nomatch / multi / occupied / error}),排查一眼可辨。
     *
     * <p>需要登录,但不做菜单权限校验:能登录就能坐自己的工位。
     */
    @PostMapping("/auto-seat")
    @Operation(summary = "按本机IP自动匹配工位并自动入座(选择工位模式 1/3)")
    public CommonResult<RoomSeatRespVO> autoSeat(HttpServletRequest request) {
        String clientIp = IpUtils.normalize(ServletUtil.getClientIP(request));
        Long docId = SecurityFrameworkUtils.getLoginUserId();
        String docName = SecurityFrameworkUtils.getLoginUserNickname();
        RoomSeatRespVO resp = roomService.autoSeatByIp(clientIp, docId, docName);
        log.info("[autoSeat][自动入座] clientIp={} docId={} seated={} reason={} 工位={}/{}",
                clientIp, docId, resp.getSeated(), resp.getReason(), resp.getRoomName(), resp.getBedNo());
        return success(resp);
    }
 
    /**
     * 查询当前登录人**已入座**的工位(系统顶栏"当前工位"标签用)。
     *
     * <p>未入座时返回 {@code seated=false},前端据此显示灰色「未入座」并可点击去选工位。
     */
    @GetMapping("/my-seat")
    @Operation(summary = "查询当前登录人已入座的工位(系统顶栏展示)")
    public CommonResult<RoomSeatRespVO> mySeat() {
        Long docId = SecurityFrameworkUtils.getLoginUserId();
        RoomSeatRespVO resp = roomService.getMySeat(docId);
        log.info("[mySeat][查询我的工位] docId={} seated={} 工位={}/{}",
                docId, resp.getSeated(), resp.getRoomName(), resp.getBedNo());
        return success(resp);
    }
 
    /**
     * 【本需求·一人多工位】查询当前登录人占据的**全部**工位(节假日值班多岗位)。
     *
     * <p>返回「医生在座 / 医生暂停」的工位列表,按 roomId + bedNo 升序;未占工位返回空数组。
     * 前端用它渲染顶栏「我的工位(N 个)」并在工位之间切换。
     */
    @GetMapping("/my-seats")
    @Operation(summary = "查询当前登录人已入座的全部工位(一人多岗位)")
    public CommonResult<List<RoomSeatRespVO>> mySeats() {
        Long docId = SecurityFrameworkUtils.getLoginUserId();
        List<RoomSeatRespVO> seats = roomService.getMySeats(docId);
        log.info("[mySeats][查询我的全部工位] docId={} 数量={}", docId, null == seats ? 0 : seats.size());
        return success(seats);
    }
 
    /**
     * 【本需求·接班自助接管】把工位从"别人占用"改为"当前登录人占用"。
     *
     * <p>场景:上一班医生下班未点「离座」,下一班医生在同一台电脑/同一工位接班。
     * 原占用人会被强制离座(其页面 5s 轮询会提示「请重新就坐」并转只读)。
     * 权限沿用医生工作台已有的 {@code ecg:doctor:task},无需新增权限点。
     */
    @PostMapping("/take-over-bed")
    @Operation(summary = "接管工位(接班:把他人占用的工位转给我)")
    @PreAuthorize("@ss.hasPermission('ecg:doctor:task')")
    public CommonResult<Boolean> takeOverBed(@RequestParam("roomId") Long roomId,
                                             @RequestParam("bedNo") String bedNo) {
        roomService.takeOverBed(roomId, bedNo);
        return success(true);
    }
 
    /**
     * 客户端 IP 识别诊断(现场排查"工位绑定的 IP 不对"用)。
     *
     * <p>一次把所有相关线索摊开:各个代理头、TCP 直连地址、规范化结果、是否回环/IPv6、
     * **是否是服务器自身地址**、以及服务器所有 IPv4 网卡地址。界面上的「IP 诊断」按钮就调它。
     *
     * <p>典型结论:
     * <ul>
     *   <li>取到的是服务器自身地址 → 当前是在服务器上/远程桌面里访问,或代理把客户端地址覆盖成了服务器地址;</li>
     *   <li>取到 127.x / ::1 → 浏览器与服务在同一台机器;</li>
     *   <li>取到 IPv6 → 客户端走 IPv6(本系统工位绑定只用 IPv4);</li>
     *   <li>{@code X-Forwarded-For} 为空且直连地址=网关 → 前面的代理没透传客户端地址。</li>
     * </ul>
     */
    @GetMapping("/ip-debug")
    @Operation(summary = "客户端IP识别诊断(排查工位绑定IP不对)")
    public CommonResult<Map<String, Object>> ipDebug(HttpServletRequest request) {
        Map<String, Object> data = new LinkedHashMap<>();
        String xff = request.getHeader("X-Forwarded-For");
        String realIp = request.getHeader("X-Real-IP");
        String remoteAddr = request.getRemoteAddr();
        String raw = ServletUtil.getClientIP(request);
        String clientIp = IpUtils.normalize(raw);
 
        data.put("访问Host", request.getHeader("Host"));
        data.put("X-Forwarded-For", xff);
        data.put("X-Real-IP", realIp);
        data.put("Proxy-Client-IP", request.getHeader("Proxy-Client-IP"));
        data.put("WL-Proxy-Client-IP", request.getHeader("WL-Proxy-Client-IP"));
        data.put("HTTP_CLIENT_IP", request.getHeader("HTTP_CLIENT_IP"));
        data.put("直连地址(remoteAddr)", remoteAddr);
        data.put("取到的原始值", raw);
        data.put("规范化后(工位按此匹配)", clientIp);
        data.put("可用于工位绑定", IpUtils.isBindableIp(clientIp));
        data.put("是回环(浏览器与服务同机)", IpUtils.isLoopback(clientIp));
        data.put("是IPv6", IpUtils.isIpv6Address(clientIp));
        data.put("是服务器自身地址", IpUtils.isLocalAddress(clientIp));
        data.put("服务器网卡IPv4", IpUtils.localIpv4Addresses());
 
        String verdict;
        if (IpUtils.isLoopback(clientIp)) {
            verdict = "客户端就是服务器本机(回环 " + clientIp + "):属【浏览器与服务在同一台机器】的正常现象,"
                    + "不能作为工位标识,请在医生电脑上访问。";
        } else if (IpUtils.isLocalAddress(clientIp)) {
            verdict = "⚠ 识别到的是【服务器自身地址】(" + clientIp + "):当前是在服务器上/远程桌面里访问,"
                    + "或前置代理把客户端地址覆盖成了服务器地址 → 不能作为工位标识。"
                    + "请在医生电脑上、用服务器的 IPv4 地址访问;若确认是在医生电脑上访问,请检查代理是否透传 "
                    + "X-Forwarded-For(nginx: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;)。";
        } else if (IpUtils.isIpv6Address(clientIp)) {
            verdict = "客户端走的是 IPv6(" + clientIp + "):本系统工位绑定只采用 IPv4,"
                    + "请让服务只监听 IPv4(JVM: -Djava.net.preferIPv4Stack=true)或改用 IPv4 地址访问。";
        } else if (IpUtils.isBindableIp(clientIp)) {
            verdict = "识别正常,可用于工位绑定:" + clientIp;
        } else {
            verdict = "未能识别到可用的 IPv4 客户端地址(原始值:" + raw + ",直连地址:" + remoteAddr
                    + "):请检查代理是否透传客户端地址(X-Forwarded-For)。";
        }
        data.put("诊断结论", verdict);
 
        log.info("[ipDebug][客户端IP诊断] Host={} XFF={} X-Real-IP={} remoteAddr={} → clientIp={} 服务器地址={}",
                request.getHeader("Host"), xff, realIp, remoteAddr, clientIp, IpUtils.isLocalAddress(clientIp));
        return success(data);
    }
 
    /**
     * 工位状态诊断(排查"入座报状态异常 / 明明空闲却坐不上")。
     *
     * <p>把库里**原始行**摊开:`id / room_id / room_name / bed_no(含长度与 HEX,用于发现结尾空格、全角) /
     * ip / status / doc_id / doc_name / op_type / deleted / update_time`,并给出一句结论。
     *
     * <p>用法:`GET /clinic/room/bed-state?id=3` 或 `?roomId=118&bedNo=C1`。
     */
    @GetMapping("/bed-state")
    @Operation(summary = "工位状态诊断(排查入座报状态异常)")
    public CommonResult<Map<String, Object>> bedState(
            @RequestParam(value = "id", required = false) Integer id,
            @RequestParam(value = "roomId", required = false) Long roomId,
            @RequestParam(value = "bedNo", required = false) String bedNo) {
        return success(roomService.diagnoseBed(id, roomId, bedNo));
    }
 
    @GetMapping("/page")
    @Operation(summary = "获得诊室和诊疗床分页")
    @PreAuthorize("@ss.hasPermission('clinic:room:query')")
    public CommonResult<PageResult<RoomRespVO>> getRoomPage(@Valid RoomPageReqVO pageReqVO) {
        PageResult<RoomDO> pageResult = roomService.getRoomPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, RoomRespVO.class));
    }
 
    @GetMapping("/export-excel")
    @Operation(summary = "导出诊室和诊疗床 Excel")
    @PreAuthorize("@ss.hasPermission('clinic:room:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportRoomExcel(@Valid RoomPageReqVO pageReqVO,
              HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<RoomDO> list = roomService.getRoomPage(pageReqVO).getList();
        // 导出 Excel
        ExcelUtils.write(response, "诊室和诊疗床.xls", "数据", RoomRespVO.class,
                        BeanUtils.toBean(list, RoomRespVO.class));
    }
 
    @GetMapping(value = {"/list-all-simple", "/simple-list"})
    @Operation(summary = "获取部门精简信息列表", description = "只包含被开启的部门,主要用于前端的下拉选项")
    public CommonResult<List<RoomRespVO>> getSimpleDeptList() {
 
        List<DeptRespDTO> deptRespDTOList = deptApi.getSimpleDeptList();
        Optional<DeptRespDTO> ecgDept = deptRespDTOList.stream()
                .filter(a -> a.getName().equals("ECG"))
                .findAny();
 
        List<DeptRespDTO> list2 = new ArrayList<DeptRespDTO>();
        if (ecgDept.isPresent()){
            System.out.println(ecgDept.get().getName());
 
            list2 = deptRespDTOList.stream()
                    .filter(a -> Objects.equals(a.getParentId(), ecgDept.get().getId()))
                    .toList();
            list2.forEach(System.out::println);
            list2.size();
        }
 
        List<RoomRespVO> list3 = list2.stream().map(item -> {
            RoomRespVO roomRespVO = new RoomRespVO();
            roomRespVO.setRoomId( item.getId() );
            roomRespVO.setRoomName( item.getName() );
            return roomRespVO;
        }).toList();
 
        //return success(BeanUtils.toBean(list2, RoomRespVO.class));
        return success(list3);
    }
 
    @GetMapping(value = {"/list-all-bed"})
    @Operation(summary = "获取诊床列表", description = "主要用于前端的开诊管理")
    public CommonResult<Map<String, List<RoomRespVO>>> getAllBedMap() {
 
        RoomPageReqVO pageReqVO = new RoomPageReqVO();
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<RoomDO> list = roomService.getRoomPage(pageReqVO).getList();
 
        List<RoomRespVO> list2 = BeanUtils.toBean(list, RoomRespVO.class);
 
        Map<String, List<RoomRespVO>> map = list2.stream().collect(Collectors.groupingBy(RoomRespVO::getRoomName));
        return success(map);
    }
 
    @GetMapping(value = {"/list-simple-room"})
    @Operation(summary = "获取门诊室列表", description = "主要用于前端的诊室选择")
    public CommonResult<Map<Long, List<RoomRespVO>>> getSimpleRoomList() {
        List<RoomDO> list = roomService.simpleRoomList();
        List<RoomRespVO> list2 = BeanUtils.toBean(list, RoomRespVO.class);
 
        // 显式标注「是否已被医生占用」:选工位页据此显示「已入座:张三」并**禁用入座按钮**,
        // 不依赖 BeanUtils 是否把 docId 复制过来(双保险;本次现场就有"看不出来谁占了工位"的反馈)。
        list2.forEach(roomRespVO -> roomRespVO.setOccupied(null != roomRespVO.getDocId()));
 
        Map<Long, List<RoomRespVO>> map = list2.stream().collect(Collectors.groupingBy(RoomRespVO::getRoomId));
        return success(map);
    }
 
    @GetMapping(value = {"/reset-room"})
    @Operation(summary = "重置门诊室", description = "主要用于每天关闭工位")
    public CommonResult<String> resetRoom() {
        roomService.resetRoom(true);
        return success("reset room ok");
    }
 
    @GetMapping(value = {"/monitor"})
    @Operation(summary = "监控状态获取", description = "监控状态获取 监控用")
    public CommonResult<MonitorInfoVO> getMonitorInfo() {
        MonitorInfoVO monitorInfoVO = roomService.getMonitorInfo();
        return success( monitorInfoVO );
    }
 
    @GetMapping("/start-biz")
    @Operation(summary = "手动开诊")
    @PreAuthorize("@ss.hasPermission('ecg:room:setting')")
    public CommonResult<Integer> startBiz() {
        queueService.startBiz();
        return success(0);
    }
 
    @GetMapping("/close-biz")
    @Operation(summary = "手动闭诊")
    @PreAuthorize("@ss.hasPermission('ecg:room:setting')")
    public CommonResult<Integer> stopBiz() {
        queueService.closeBiz();
        return success(0);
    }
 
    /**
     * 【本次需求】管理员强制清空某个工位(不要求是占用者本人)。
     * <p>
     * 使用场景:医生下班忘记「离座」,或换人使用同一工位。
     * <p>
     * 与医生自己的「离座」接口({@code /ecg/doctor/bed-doctor-off})的区别:
     * · 该接口只能清**自己**占用的工位(SQL 带 {@code doc_id = 当前用户});
     * · 本接口面向管理端,可以清**任何人**占用的工位(仅要求该工位当前是「医生在岗 / 暂停」)。
     * <p>
     * 工位空闲时会返回「当前工位状态不允许该操作」——这是预期行为。
     */
    @PostMapping("/force-clear-bed")
    @Operation(summary = "管理员强制清空工位", description = "医生下班忘离座 / 换人使用同一工位时使用")
    @PreAuthorize("@ss.hasPermission('clinic:room:update')")
    public CommonResult<Boolean> forceClearBed(@RequestParam("roomId") Long roomId,
                                               @RequestParam("bedNo") String bedNo) {
        ErrorCode ec = queueService.startBedDoctorOffByAdmin(roomId, bedNo);
        if (!GlobalErrorCodeConstants.SUCCESS.getCode().equals(ec.getCode())) {
            log.warn("[forceClearBed] 清空工位失败 roomId={} bedNo={} code={} msg={}",
                    roomId, bedNo, ec.getCode(), ec.getMsg());
            throw exception(ec);
        }
        return success(true);
    }
 
}