新火炬后端单体项目初始化代码
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
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
package org.jeecg.modules.feishu.service;
 
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.PasswordUtil;
import org.jeecg.common.util.RestUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.andon.dto.AndonButtonDTO;
import org.jeecg.modules.mes.entity.FeishuUser;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.system.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
 
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
 
@Service
@Slf4j
public class FeishuUserService {
    // 类中定义模板常量(便于维护)
    private static final String FIRST_LEVEL_TEMPLATE = "【工艺安灯】\n%s · %s 发起了工艺安灯,请%d分钟内在PDA上进行响应处理!!!";
    private static final String SECOND_LEVEL_TEMPLATE = "【工艺安灯】\n%s · %s 发起的工艺安灯,%s未在%d分钟内响应,请尽快督促进行响应处理!!!";
    @Resource
    private ISysUserService sysUserService;
 
    @Resource
    private RestTemplate restTemplate;
 
    @Value("${feishu.appId:}")
    private String appId;
 
    @Value("${feishu.appSecret:}")
    private String appSecret;
 
    @Value("${feishu.url:}")
    private String feishuUrl;
 
    // 定时任务线程池(支持多级响应调度)
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
    private final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
 
    /**
     * 同步飞书部门用户到系统用户表
     */
    public void syncFeishuDepartmentUsers(String departmentId) {
        try {
            log.info("开始同步飞书部门用户,部门ID: {}", departmentId);
 
            // 1. 获取飞书访问令牌
            String accessToken = getFeishuAccessToken();
            if (oConvertUtils.isEmpty(accessToken)) {
                log.error("获取飞书访问令牌失败,终止同步流程");
                return;
            }
 
            // 2. 获取部门下的用户列表
            List<FeishuUser> feishuUsers = getDepartmentUsers(accessToken, departmentId);
            log.info("获取到飞书部门用户数量: {}", feishuUsers.size());
 
            // 3. 同步到系统用户表
            int successCount = 0;
            int updateCount = 0;
            int addCount = 0;
 
            for (FeishuUser feishuUser : feishuUsers) {
                try {
                    boolean isUpdated = syncFeishuUserToSystem(feishuUser);
                    successCount++;
                    if (isUpdated) {
                        updateCount++;
                    } else {
                        addCount++;
                    }
                } catch (Exception e) {
                    log.error("同步飞书用户失败,用户ID: {}", feishuUser.getUserId(), e);
                }
            }
 
            log.info("飞书用户同步完成,总处理: {},新增: {},更新: {}", successCount, addCount, updateCount);
        } catch (Exception e) {
            log.error("同步飞书部门用户失败,部门ID: {}", departmentId, e);
            throw new RuntimeException("同步飞书用户失败: " + e.getMessage());
        }
    }
 
    public String getFeishuAccessToken() {
        try {
            String url = feishuUrl + "open-apis/auth/v3/tenant_access_token/internal";
 
            log.info("开始获取飞书访问令牌,AppId: {}",
                    appId != null ? appId.substring(0, Math.min(appId.length(), 10)) + "..." : "null");
 
            JSONObject params = new JSONObject();
            params.put("app_id", appId);
            params.put("app_secret", appSecret);
 
            log.debug("获取令牌请求参数: {}", params.toJSONString());
 
            JSONObject result = RestUtil.post(url, params);
 
            log.info("获取飞书访问令牌响应: {}", result != null ? result.toJSONString() : "null");
 
            if (result != null && result.getInteger("code") == 0) {
                String accessToken = result.getString("tenant_access_token");
                log.info("成功获取飞书访问令牌,令牌长度: {}", accessToken != null ? accessToken.length() : 0);
                return accessToken;
            } else {
                log.error("获取飞书访问令牌失败,响应: {}", result != null ? result.toJSONString() : "null");
                if (result != null) {
                    log.error("错误码: {}, 错误信息: {}", result.getInteger("code"), result.getString("msg"));
                }
                return null;
            }
        } catch (Exception e) {
            log.error("获取飞书访问令牌异常", e);
            return null;
        }
    }
 
    /**
     * 发送安灯通知消息(支持一、二、三级响应)
     * 流程:
     * 1. APP点击发送一级响应(初级响应)
     * 2. 一级响应时长后检查状态,未处理则发送二级
     * 3. 二级响应时长后检查状态,未处理则发送三级
     */
    public boolean sendAndonNotification(String accessToken, AndonButtonDTO andonOrde) {
        try {
            String currentTime = sdf.format(new Date());
            log.info("【{}】触发一级响应(初级响应),安灯ID: {}", currentTime, andonOrde.getId());
 
            // 1. 参数验证
            if (!validateNotificationParams(andonOrde)) {
                return false;
            }
            if (accessToken == null || accessToken.isEmpty()) {
                log.error("访问令牌为空,无法发送安灯通知");
                return false;
            }
 
            // 2. 发送一级响应通知(即时发送)
            boolean firstLevelResult = sendLevelNotification(accessToken, andonOrde, 1);
 
            // 3. 调度二级响应通知(延迟时间=一级响应时长)
            if (firstLevelResult) {
                int secondDelay = andonOrde.getUpgradeResponseDuration();
                String secondTime = sdf.format(new Date(System.currentTimeMillis() + secondDelay * 60 * 1000L));
                log.info("【{}】一级响应调度二级通知,延迟{}分钟,预计发送时间: {}",
                        currentTime, secondDelay, secondTime);
                scheduleNextLevelNotification(accessToken, andonOrde, 1, secondDelay);
            } else {
                log.warn("一级通知发送失败,终止后续通知流程,安灯ID: {}", andonOrde.getId());
            }
 
            return firstLevelResult;
        } catch (Exception e) {
            log.error("处理安灯通知异常,安灯ID: {}", andonOrde != null ? andonOrde.getId() : "未知", e);
            return false;
        }
    }
 
    /**
     * 发送指定级别的通知
     * @param level 1-一级,2-二级,3-三级
     */
    private boolean sendLevelNotification(String accessToken, AndonButtonDTO andonOrde, int level) {
        try {
            String levelDesc = getLevelDesc(level);
            String openId = getResponderOpenId(andonOrde, level);
            String currentTime = sdf.format(new Date());
            log.info("【{}】开始发送{}级安灯通知,接收人openId: {}, 安灯ID: {}",
                    currentTime, levelDesc, openId, andonOrde.getId());
 
            // 构建请求URL
            String url = feishuUrl + "open-apis/im/v1/messages?receive_id_type=open_id";
            log.info("{}级通知请求URL: {}", levelDesc, url);
 
            // 设置请求头
            HttpHeaders headers = new HttpHeaders();
            String authHeader = "Bearer " + accessToken;
            headers.add("Authorization", authHeader);
            headers.add("Content-Type", "application/json; charset=utf-8");
            log.info("设置{}级通知请求头: Authorization = {}", levelDesc,
                    authHeader.substring(0, Math.min(30, authHeader.length())) + "...");
 
            // 构建消息内容
            JSONObject content = new JSONObject();
            String notificationContent = buildNotificationContent(andonOrde, level);
            content.put("text", notificationContent);
            log.debug("【{}】{}级通知内容: {}", currentTime, levelDesc, notificationContent);
 
            // 构建请求体
            JSONObject requestBody = new JSONObject();
            requestBody.put("receive_id", openId);
            requestBody.put("msg_type", "text");
            requestBody.put("content", content.toJSONString());
            requestBody.put("uuid", "andon_" + andonOrde.getId() + "_level_" + level + "_" + System.currentTimeMillis());
 
            // 发送请求
            HttpEntity<String> requestEntity = new HttpEntity<>(requestBody.toJSONString(), headers);
            ResponseEntity<String> response = restTemplate.exchange(
                    url, HttpMethod.POST, requestEntity, String.class
            );
 
            // 解析响应
            JSONObject result = JSONObject.parseObject(response.getBody());
            log.debug("{}级通知API响应: {}", levelDesc, result != null ? result.toJSONString() : "null");
 
            if (result != null && result.getInteger("code") == 0) {
                JSONObject data = result.getJSONObject("data");
                if (data != null) {
                    log.info("【{}】{}级安灯通知发送成功,消息ID: {}, 安灯ID: {}",
                            currentTime, levelDesc, data.getString("message_id"), andonOrde.getId());
                    return true;
                } else {
                    log.warn("{}级通知响应data为空,安灯ID: {}", levelDesc, andonOrde.getId());
                    return false;
                }
            } else {
                log.error("发送{}级安灯通知失败,安灯ID: {}", levelDesc, andonOrde.getId());
                if (result != null) {
                    log.error("错误码: {}, 错误信息: {}", result.getInteger("code"), result.getString("msg"));
                }
                return false;
            }
 
        } catch (Exception e) {
            log.error("发送{}级安灯通知异常,安灯ID: {}", getLevelDesc(level), andonOrde.getId(), e);
            return false;
        }
    }
 
    /**
     * 调度下一级通知(1→2→3)
     * @param currentLevel 当前级别
     * @param delayMinutes 延迟分钟数(由当前级别响应时长决定)
     */
    private void scheduleNextLevelNotification(String accessToken, AndonButtonDTO andonOrde, int currentLevel, int delayMinutes) {
        int nextLevel = currentLevel + 1;
        if (nextLevel > 3) {
            log.info("已到达最高级别通知,无需继续调度,安灯ID: {}", andonOrde.getId());
            return;
        }
 
        String currentLevelDesc = getLevelDesc(currentLevel);
        String nextLevelDesc = getLevelDesc(nextLevel);
        String scheduleTime = sdf.format(new Date());
        String executeTime = sdf.format(new Date(System.currentTimeMillis() + delayMinutes * 60 * 1000L));
 
        log.info("【{}】调度{}通知,延迟{}分钟(当前级别{}时长),预计执行时间: {}",
                scheduleTime, nextLevelDesc, delayMinutes, currentLevelDesc, executeTime);
 
        scheduler.schedule(() -> {
            try {
                String currentExecuteTime = sdf.format(new Date());
                log.info("【{}】开始执行{}通知检查,安灯ID: {}", currentExecuteTime, nextLevelDesc, andonOrde.getId());
 
                // 检查订单状态:3表示已响应,无需发送下一级
                if (andonOrde.getOrderStatus() != null && "3".equals(andonOrde.getOrderStatus())) {
                    log.info("【{}】订单已响应(status=3),取消{}通知发送,安灯ID: {}",
                            currentExecuteTime, nextLevelDesc, andonOrde.getId());
                    return;
                }
 
                // 检查下一级响应人是否有效
                if (!hasValidResponder(andonOrde, nextLevel)) {
                    log.warn("【{}】{}响应人信息无效,取消通知发送,安灯ID: {}",
                            currentExecuteTime, nextLevelDesc, andonOrde.getId());
                    return;
                }
 
                // 发送下一级通知
                boolean nextLevelResult = sendLevelNotification(accessToken, andonOrde, nextLevel);
                if (nextLevelResult && nextLevel < 3) {
 
                    int nextDelay = getResponseDuration(andonOrde, nextLevel);
                    String nextExecuteTime = sdf.format(new Date(System.currentTimeMillis() + nextDelay * 60 * 1000L));
                    log.info("【{}】{}通知调度三级通知,延迟{}分钟,预计发送时间: {}",
                            currentExecuteTime, nextLevelDesc, nextDelay, nextExecuteTime);
                    scheduleNextLevelNotification(accessToken, andonOrde, nextLevel, nextDelay);
                } else if (!nextLevelResult) {
                    log.warn("【{}】{}通知发送失败,终止后续调度,安灯ID: {}",
                            currentExecuteTime, nextLevelDesc, andonOrde.getId());
                }
 
            } catch (Exception e) {
                log.error("执行{}调度任务异常,安灯ID: {}", nextLevelDesc, andonOrde.getId(), e);
            }
        }, delayMinutes, TimeUnit.MINUTES);
    }
    /**
     * 构建通知内容(优化分隔符,提升可读性)
     */
    private String buildNotificationContent(AndonButtonDTO andonOrde, int level) {
        // 获取工厂名称和产线名称,做空值保护
        String parentFactoryName = andonOrde.getParentFactoryName() != null ? andonOrde.getParentFactoryName() : "未知工厂";
        String factoryName = andonOrde.getFactoryName() != null ? andonOrde.getFactoryName() : "未知产线";
        String content;
 
        if (level == 1) {
            // 一级模板:使用"·"分隔工厂和产线
            int firstDuration = andonOrde.getUpgradeResponseDuration() != null ? andonOrde.getUpgradeResponseDuration() : 0;
            content = String.format(FIRST_LEVEL_TEMPLATE, parentFactoryName, factoryName, firstDuration);
        } else {
            // 二级模板:保持一致的分隔风格
            String prevResponder = getResponderName(andonOrde, level - 1);
            int prevDuration = getResponseDuration(andonOrde, level - 1);
 
            prevResponder = oConvertUtils.isEmpty(prevResponder) ? "未知人员" : prevResponder;
            prevDuration = Math.max(prevDuration, 0);
 
            content = String.format(SECOND_LEVEL_TEMPLATE, parentFactoryName, factoryName, prevResponder, prevDuration);
        }
 
        log.debug("构建{}级通知内容: {}", level, content);
        return content;
    }
//    /**
//     * 构建通知内容(按级别使用不同模板)
//     */
//    private String buildNotificationContent(AndonButtonDTO andonOrde, int level) {
//        String factoryName = andonOrde.getFactoryName() != null ? andonOrde.getFactoryName() : "未知产线";
//
//        if (level == 1) {
//            // 一级模板:【工艺安灯】XXX产线发起了工艺安灯,请xx分钟内在PDA上进行响应处理!!!
//            return String.format("【工艺安灯】\n%s产线发起了工艺安灯,请%d分钟内在PDA上进行响应处理!!!",
//                    factoryName,
//                    andonOrde.getUpgradeResponseDuration());
//        } else {
//            // 二/三级模板:【工艺安灯】XXX(产线)发起的工艺安灯,xxx(上一级响应人)未在xx分钟内响应,请尽快督促进行响应处理!!!
//            String prevLevelDesc = getLevelDesc(level - 1);
//            String prevResponder = getResponderName(andonOrde, level - 1);
//            int prevDuration = getResponseDuration(andonOrde, level - 1);
//
//            return String.format("【工艺安灯】\n%s(产线)发起的工艺安灯,%s(%s响应人)未在%d分钟内响应,请尽快督促进行响应处理!!!",
//                    factoryName,
//                    prevResponder,
//                    prevLevelDesc,
//                    prevDuration);
//        }
//    }
 
    /**
     * 验证通知相关参数
     */
    private boolean validateNotificationParams(AndonButtonDTO andonOrde) {
        if (andonOrde == null) {
            log.error("安灯订单信息为空");
            return false;
        }
        if (andonOrde.getId() == null) {
            log.error("安灯ID为空");
            return false;
        }
        // 一级响应校验(必选)
        if (andonOrde.getResponderOpenId() == null || andonOrde.getResponderOpenId().isEmpty()
                || andonOrde.getUpgradeResponseDuration() == null || andonOrde.getUpgradeResponseDuration() <= 0) {
            log.error("一级响应人信息无效(openId或时长缺失),安灯ID: {}", andonOrde.getId());
            return false;
        }
        // 二级响应校验(若存在则必选完整)
        if (andonOrde.getSecondResponderOpenId() != null && !andonOrde.getSecondResponderOpenId().isEmpty()) {
            if (andonOrde.getSecondUpgradeResponseDuration() == null || andonOrde.getSecondUpgradeResponseDuration() <= 0) {
                log.error("二级响应人存在但时长无效,安灯ID: {}", andonOrde.getId());
                return false;
            }
        }
        // 三级响应校验(若存在则必选完整)
        if (andonOrde.getThirdResponderOpenId() != null && !andonOrde.getThirdResponderOpenId().isEmpty()) {
            if (andonOrde.getSecondUpgradeResponseDuration() == null || andonOrde.getSecondUpgradeResponseDuration() <= 0) {
                log.error("三级响应人存在但二级时长无效(三级依赖二级时长),安灯ID: {}", andonOrde.getId());
                return false;
            }
        }
        return true;
    }
 
    // 工具方法:获取级别描述
    private String getLevelDesc(int level) {
        switch (level) {
            case 1: return "一级";
            case 2: return "二级";
            case 3: return "三级";
            default: return "未知级别";
        }
    }
 
    // 工具方法:获取响应人openId
    private String getResponderOpenId(AndonButtonDTO andonOrde, int level) {
        switch (level) {
            case 1: return andonOrde.getResponderOpenId();
            case 2: return andonOrde.getSecondResponderOpenId();
            case 3: return andonOrde.getThirdResponderOpenId();
            default: return null;
        }
    }
 
    // 工具方法:获取响应人名称
    private String getResponderName(AndonButtonDTO andonOrde, int level) {
        String name = null;
        switch (level) {
            case 1:
                name = andonOrde.getResponder();
                break;
            case 2:
                name = andonOrde.getSecondResponder();
                break;
            case 3:
                name = andonOrde.getThirdResponder();
                break;
            default:
                name = null;
        }
        return name != null ? name : "未知响应人";
    }
 
    // 工具方法:获取响应时长(一级=二级延迟,二级=三级延迟)
    private int getResponseDuration(AndonButtonDTO andonOrde, int level) {
        int duration = 0;
        switch (level) {
            case 1:
                duration = andonOrde.getUpgradeResponseDuration() != null ? andonOrde.getUpgradeResponseDuration() : 0;
                break;
            case 2:
                duration = andonOrde.getSecondUpgradeResponseDuration() != null ? andonOrde.getSecondUpgradeResponseDuration() : 0;
                break;
            case 3:
                duration = 0; // 三级是最后一级,无需延迟
                break;
            default:
                duration = 0;
        }
        return duration;
    }
 
    // 工具方法:判断指定级别响应人是否有效
    private boolean hasValidResponder(AndonButtonDTO andonOrde, int level) {
        String openId = getResponderOpenId(andonOrde, level);
        return openId != null && !openId.isEmpty();
    }
 
    /**
     * 获取部门用户列表(直属员工)
     */
    private List<FeishuUser> getDepartmentUsers(String accessToken, String departmentId) {
        List<FeishuUser> userList = new ArrayList<>();
        try {
            log.info("开始获取飞书部门用户列表,部门ID: {}", departmentId);
 
            if (accessToken == null || accessToken.isEmpty()) {
                log.error("访问令牌为空,无法继续执行");
                return userList;
            }
            log.info("使用的访问令牌前缀: Bearer {}", accessToken.substring(0, Math.min(20, accessToken.length())) + "...");
 
            String url = feishuUrl + "open-apis/contact/v3/users/find_by_department";
            String pageToken = null;
            int pageNumber = 1;
 
            do {
                StringBuilder urlBuilder = new StringBuilder(url);
                urlBuilder.append("?department_id=").append(departmentId);
                urlBuilder.append("&department_id_type=open_department_id");
                urlBuilder.append("&page_size=50");
                urlBuilder.append("&user_id_type=open_id");
 
                if (pageToken != null && !pageToken.isEmpty()) {
                    urlBuilder.append("&page_token=").append(pageToken);
                }
 
                log.info("请求第{}页数据,URL: {}", pageNumber, urlBuilder.toString());
 
                HttpHeaders headers = new HttpHeaders();
                String authHeader = "Bearer " + accessToken;
                headers.add("Authorization", authHeader);
                headers.add("Content-Type", "application/json; charset=utf-8");
                log.info("设置请求头: Authorization = {}", authHeader.substring(0, Math.min(30, authHeader.length())) + "...");
 
                HttpEntity<String> requestEntity = new HttpEntity<>(headers);
                ResponseEntity<String> response = restTemplate.exchange(
                        urlBuilder.toString(), HttpMethod.GET, requestEntity, String.class
                );
 
                JSONObject result = JSONObject.parseObject(response.getBody());
                log.debug("第{}页API响应: {}", pageNumber, result != null ? result.toJSONString() : "null");
 
                if (result != null && result.getInteger("code") == 0) {
                    JSONObject data = result.getJSONObject("data");
                    if (data != null) {
                        Object items = data.get("items");
                        log.info("第{}页原始用户数据数量: {}", pageNumber, items instanceof List ? ((List<?>) items).size() : 0);
 
                        if (items != null) {
                            List<FeishuUser> pageUsers = parseUsers(items);
                            userList.addAll(pageUsers);
                            log.info("第{}页解析后用户数量: {}", pageNumber, pageUsers.size());
                        }
 
                        pageToken = data.getString("page_token");
                        boolean hasMore = data.getBooleanValue("has_more");
                        log.info("第{}页has_more: {}, page_token: {}", pageNumber, hasMore, pageToken);
                        pageNumber++;
                    } else {
                        log.warn("第{}页data为空", pageNumber);
                        break;
                    }
                } else {
                    log.error("获取飞书部门用户列表失败,响应: {}", result != null ? result.toJSONString() : "null");
                    if (result != null) {
                        log.error("错误码: {}, 错误信息: {}", result.getInteger("code"), result.getString("msg"));
                    }
                    break;
                }
            } while (pageToken != null && !pageToken.isEmpty());
 
            log.info("获取飞书部门用户完成,总用户数量: {}", userList.size());
 
        } catch (Exception e) {
            log.error("获取飞书部门用户列表异常", e);
        }
        return userList;
    }
 
    /**
     * 解析用户数据
     */
    @SuppressWarnings("unchecked")
    private List<FeishuUser> parseUsers(Object items) {
        Logger log = LoggerFactory.getLogger(FeishuUserService.class);
        List<FeishuUser> userList = new ArrayList<>();
        if (!(items instanceof List)) {
            log.warn("解析用户数据失败,items不是列表类型: {}", items);
            return userList;
        }
        List<?> userItems = (List<?>) items;
        log.info("开始解析飞书用户列表,共{}条记录", userItems.size());
        for (Object item : userItems) {
            if (item == null || !(item instanceof Map)) {
                log.warn("跳过无效用户数据项,类型不匹配: {}", item != null ? item.getClass() : "null");
                continue;
            }
            Map<String, Object> userMap = (Map<String, Object>) item;
            FeishuUser user = new FeishuUser();
            user.setOpenId((String) userMap.getOrDefault("open_id", ""));
 
            String name = (String) userMap.getOrDefault("name", "");
            if (name.contains(" ")) {
                String[] parts = name.split(" ");
                if (parts.length == 2) {
                    user.setUsername(parts[0].trim());
                    user.setRealname(parts[1].trim());
                    user.setWorkNo(parts[0].trim());
                } else {
                    user.setUsername(name);
                    user.setRealname(name);
                    user.setWorkNo("");
                    log.warn("飞书用户name字段格式异常,无法拆分,原始值:{}", name);
                }
            } else {
                user.setUsername(name);
                user.setRealname(name);
                user.setWorkNo("");
                log.warn("飞书用户name字段无空格分隔,无法拆分,原始值:{}", name);
            }
 
            user.setEnName((String) userMap.getOrDefault("en_name", ""));
            user.setDescription((String) userMap.getOrDefault("description", ""));
            Object mobileVisibleObj = userMap.get("mobile_visible");
            user.setMobileVisible(mobileVisibleObj instanceof Boolean ? (Boolean) mobileVisibleObj : false);
 
            Object avatarObj = userMap.get("avatar");
            if (avatarObj instanceof Map) {
                Map<String, Object> avatarMap = (Map<String, Object>) avatarObj;
                user.setAvatar240((String) avatarMap.getOrDefault("avatar_240", ""));
                user.setAvatar640((String) avatarMap.getOrDefault("avatar_640", ""));
                user.setAvatar72((String) avatarMap.getOrDefault("avatar_72", ""));
                user.setAvatarOrigin((String) avatarMap.getOrDefault("avatar_origin", ""));
            } else {
                log.info("用户[{}]未设置头像或头像格式异常", user.getRealname());
            }
 
            user.setPassword("123456");
            user.setOpenId((String) userMap.getOrDefault("open_id", ""));
            userList.add(user);
        }
 
        return userList;
    }
 
    /**
     * 同步单个飞书用户到系统
     */
    private boolean syncFeishuUserToSystem(FeishuUser feishuUser) {
        String username = feishuUser.getUsername();
        SysUser existUser = sysUserService.getUserByName(username);
        SysUser sysUser = new SysUser();
        sysUser.setUsername(username);
        sysUser.setStatus(1);
        sysUser.setDelFlag(0);
        sysUser.setAvatar(feishuUser.getAvatar640());
        sysUser.setRealname(feishuUser.getRealname());
        sysUser.setWorkNo(feishuUser.getWorkNo());
        String password = "123456", salt = oConvertUtils.randomGen(8);
        String passwordEncode = PasswordUtil.encrypt(sysUser.getUsername(), password, salt);
        sysUser.setSalt(salt);
        sysUser.setPassword(passwordEncode);
        sysUser.setOpenId(feishuUser.getOpenId());
 
        if (existUser == null) {
            sysUserService.addUserWithRole(sysUser, "5");
            log.info("新增用户: {}", username);
            return false;
        } else {
            sysUser.setId(existUser.getId());
            sysUserService.editUser(sysUser);
            log.info("更新用户: {}", username);
            return true;
        }
    }
}