liu
2026-09-22 0837c3ff473499d8a267924125c776b34cc8b517
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
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
package com.ruoyi.system.controller;
 
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.domain.CjStudent;
import com.ruoyi.system.domain.CjClass;
import com.ruoyi.system.domain.CjCollege;
import com.ruoyi.system.domain.CjMajor;
import com.ruoyi.system.mapper.CjClassMapper;
import com.ruoyi.system.mapper.CjCollegeMapper;
import com.ruoyi.system.mapper.CjMajorMapper;
import com.ruoyi.system.mapper.CjStudentBindMapper;
import com.ruoyi.system.mapper.CjStudentMapper;
import com.ruoyi.system.service.ICjStudentService;
import com.ruoyi.system.service.MiniappTokenService;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
 
/**
 * 学生花名册Controller
 *
 * 约定:学号全校不唯一、仅班级内唯一,学生唯一性 = 所属班级 + 学号;
 * Excel 导入/导出用“学院名称/专业名称/班级名称”引用班级,无需数字班级ID。
 *
 * @author ruoyi
 * @date 2026-09-03
 */
@RestController
@RequestMapping("/system/student")
public class CjStudentController extends BaseController
{
    @Autowired
    private ICjStudentService cjStudentService;
    @Autowired
    private CjClassMapper cjClassMapper;
    @Autowired
    private CjCollegeMapper cjCollegeMapper;
    @Autowired
    private CjMajorMapper cjMajorMapper;
    @Autowired
    private CjStudentBindMapper cjStudentBindMapper;
    @Autowired
    private CjStudentMapper cjStudentMapper;
    @Autowired
    private MiniappTokenService miniappTokenService;
    @Autowired
    private RedisCache redisCache;
 
    /**
     * 全校视野:超管(约定 admin 的 userId 恒为 1,或 admin 角色)或「教务处」角色。
     * 教务处账号(jwc01)不是 userId=1、也没有 manager_user_id 班级,
     * 若仍按 userId 判定会看到空花名册,故按角色放行。
     */
    private boolean isSchoolWide()
    {
        Long userId = SecurityUtils.getUserId();
        if (userId != null && userId == 1L)
        {
            return true;
        }
        try
        {
            return SecurityUtils.hasRole("admin") || SecurityUtils.hasRole("jwc");
        }
        catch (Exception e)
        {
            return false;
        }
    }
 
    /** 当前用户(教师)管理的班级ID集合;超管返回 null 表示不限 */
    private List<Long> myClassIds()
    {
        CjClass q = new CjClass();
        q.setManagerUserId(SecurityUtils.getUserId());
        List<CjClass> myClasses = cjClassMapper.selectCjClassList(q);
        List<Long> ids = new ArrayList<>();
        if (myClasses != null)
        {
            for (CjClass c : myClasses)
            {
                ids.add(c.getClassId());
            }
        }
        return ids;
    }
 
    /** 当前用户能否管理该学生(超管可管理一切;教师须在本班) */
    private boolean canManage(CjStudent stu)
    {
        if (stu == null || stu.getClassId() == null)
        {
            return false;
        }
        if (isSchoolWide())
        {
            return true;
        }
        return myClassIds().contains(stu.getClassId());
    }
 
    /** 班级+学号 查已有学生(判重依据:班级内学号唯一) */
    private CjStudent findByClassAndNo(Long classId, String studentNo)
    {
        if (classId == null || studentNo == null)
        {
            return null;
        }
        CjStudent q = new CjStudent();
        q.setClassId(classId);
        q.setStudentNo(studentNo);
        List<CjStudent> list = cjStudentService.selectCjStudentList(q);
        return list.isEmpty() ? null : list.get(0);
    }
 
    /** 性别码转文本(导出展示列) */
    private String genderToText(String gender)
    {
        if (gender == null)
        {
            return "";
        }
        if ("0".equals(gender))
        {
            return "男";
        }
        if ("1".equals(gender))
        {
            return "女";
        }
        if ("2".equals(gender))
        {
            return "未知";
        }
        return "";
    }
 
    /** 非法性别标记 */
    private static final String GENDER_INVALID = "GENDER_INVALID";
 
    /** 解析导入的性别列(兼容 男/女/未知 与 0/1/2);空白返回 null;非法返回 GENDER_INVALID */
    private String parseGender(String text)
    {
        if (text == null)
        {
            return null;
        }
        String t = text.trim();
        if (t.isEmpty())
        {
            return null;
        }
        if ("男".equals(t) || "0".equals(t))
        {
            return "0";
        }
        if ("女".equals(t) || "1".equals(t))
        {
            return "1";
        }
        if ("未知".equals(t) || "2".equals(t))
        {
            return "2";
        }
        return GENDER_INVALID;
    }
 
    /**
     * 导出/展示前回填 学院/专业/班级名称 与 性别中文(transient Excel 列)
     */
    private void fillStudentForExcel(List<CjStudent> list)
    {
        if (list == null || list.isEmpty())
        {
            return;
        }
        Map<Long, CjClass> classMap = new HashMap<>();
        List<CjClass> classes = cjClassMapper.selectCjClassList(new CjClass());
        if (classes != null)
        {
            for (CjClass c : classes)
            {
                classMap.put(c.getClassId(), c);
            }
        }
        Map<Long, CjMajor> majorMap = new HashMap<>();
        List<CjMajor> majors = cjMajorMapper.selectCjMajorList(new CjMajor());
        if (majors != null)
        {
            for (CjMajor m : majors)
            {
                majorMap.put(m.getMajorId(), m);
            }
        }
        Map<Long, String> collegeMap = new HashMap<>();
        List<CjCollege> colleges = cjCollegeMapper.selectCjCollegeList(new CjCollege());
        if (colleges != null)
        {
            for (CjCollege c : colleges)
            {
                collegeMap.put(c.getCollegeId(), c.getCollegeName());
            }
        }
        for (CjStudent s : list)
        {
            s.setGenderText(genderToText(s.getGender()));
            if (s.getClassId() == null)
            {
                continue;
            }
            CjClass cl = classMap.get(s.getClassId());
            if (cl == null)
            {
                continue;
            }
            s.setClassName(cl.getClassName());
            if (cl.getMajorId() != null)
            {
                CjMajor m = majorMap.get(cl.getMajorId());
                if (m != null)
                {
                    s.setMajorName(m.getMajorName());
                    if (m.getCollegeId() != null)
                    {
                        s.setCollegeName(collegeMap.get(m.getCollegeId()));
                    }
                }
            }
        }
    }
 
    /**
     * 查询学生花名册列表(数据权限:教师仅见本班)
     */
    @PreAuthorize("@ss.hasPermi('system:student:list')")
    @GetMapping("/list")
    public TableDataInfo list(CjStudent cjStudent)
    {
        // 非超管(admin=1)时,仅返回其管理班级的学生
        if (!isSchoolWide())
        {
            List<Long> classIds = myClassIds();
            if (classIds.isEmpty())
            {
                return getDataTable(new ArrayList<>());
            }
            cjStudent.setClassIds(classIds);
        }
 
        startPage();
        List<CjStudent> list = cjStudentService.selectCjStudentList(cjStudent);
        // 手机号脱敏(列表展示 138****1234)
        list.forEach(s -> {
            if (s.getPhone() != null && s.getPhone().length() == 11) {
                s.setPhone(s.getPhone().substring(0, 3) + "****" + s.getPhone().substring(7));
            }
        });
        return getDataTable(list);
    }
 
    /**
     * 查询当前教师可管理的班级(新增/查询“我的班”下拉源;超管返回空,走全校级联)
     */
    @PreAuthorize("@ss.hasPermi('system:student:list')")
    @GetMapping("/myClasses")
    public AjaxResult myClasses()
    {
        if (isSchoolWide())
        {
            return success(new ArrayList<>());
        }
        return success(myClassIdsForSelect());
    }
 
    private List<CjClass> myClassIdsForSelect()
    {
        CjClass q = new CjClass();
        q.setManagerUserId(SecurityUtils.getUserId());
        return cjClassMapper.selectCjClassList(q);
    }
 
    /**
     * 导出学生花名册列表(教师仅导出本班;导出列含学院/专业/班级名称与性别文本)
     */
    @PreAuthorize("@ss.hasPermi('system:student:export')")
    @Log(title = "学生花名册", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, CjStudent cjStudent)
    {
        List<CjStudent> list;
        if (!isSchoolWide())
        {
            List<Long> classIds = myClassIds();
            if (classIds.isEmpty())
            {
                ExcelUtil<CjStudent> emptyUtil = new ExcelUtil<CjStudent>(CjStudent.class);
                emptyUtil.exportExcel(response, new ArrayList<>(), "学生花名册数据");
                return;
            }
            cjStudent.setClassIds(classIds);
        }
        list = cjStudentService.selectCjStudentList(cjStudent);
        fillStudentForExcel(list);
        ExcelUtil<CjStudent> util = new ExcelUtil<CjStudent>(CjStudent.class);
        util.exportExcel(response, list, "学生花名册数据");
    }
 
    /**
     * 获取学生花名册详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:student:query')")
    @GetMapping(value = "/{studentId}")
    public AjaxResult getInfo(@PathVariable("studentId") Long studentId)
    {
        CjStudent stu = cjStudentService.selectCjStudentByStudentId(studentId);
        if (!isSchoolWide() && !canManage(stu))
        {
            return error("无权访问其他班级学生数据");
        }
        return success(stu);
    }
 
    /**
     * 新增学生花名册(教师仅可在本班新增;同班学号不得重复)
     */
    @PreAuthorize("@ss.hasPermi('system:student:add')")
    @Log(title = "学生花名册", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody CjStudent cjStudent)
    {
        if (!isSchoolWide() && !myClassIds().contains(cjStudent.getClassId()))
        {
            return error("只能在本班新增学生");
        }
        if (findByClassAndNo(cjStudent.getClassId(), cjStudent.getStudentNo()) != null)
        {
            return error("该班级已存在相同学号的学生(学号仅班级内唯一),请核对");
        }
        return toAjax(cjStudentService.insertCjStudent(cjStudent));
    }
 
    /**
     * 修改学生花名册(教师仅可维护本班学生,且不能改到他人班级;班级+学号不可重复)
     */
    @PreAuthorize("@ss.hasPermi('system:student:edit')")
    @Log(title = "学生花名册", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody CjStudent cjStudent)
    {
        if (!isSchoolWide())
        {
            CjStudent old = cjStudentService.selectCjStudentByStudentId(cjStudent.getStudentId());
            if (!canManage(old))
            {
                return error("无权操作其他班级学生");
            }
            if (cjStudent.getClassId() != null && !myClassIds().contains(cjStudent.getClassId()))
            {
                return error("只能将学生保留在本人管理的班级");
            }
        }
        CjStudent same = findByClassAndNo(cjStudent.getClassId(), cjStudent.getStudentNo());
        if (same != null && !same.getStudentId().equals(cjStudent.getStudentId()))
        {
            return error("该班级已存在相同学号的学生(学号仅班级内唯一),请核对");
        }
        int rows = cjStudentService.updateCjStudent(cjStudent);
        // 修正学号/姓名/手机号后清掉自助通道的失败锁,避免学生被历史尝试锁在门外
        if (rows > 0 && cjStudent.getStudentNo() != null && cjStudent.getName() != null)
        {
            redisCache.deleteObject(MiniappTokenService.pwdErrKey(cjStudent.getStudentNo(), cjStudent.getName()));
            redisCache.deleteObject(MiniappTokenService.resetErrKey(cjStudent.getStudentNo(), cjStudent.getName()));
        }
        return toAjax(rows);
    }
 
    /**
     * 删除学生花名册(引用保护:已微信绑定的学生禁止删除,请改用停用)
     */
    @PreAuthorize("@ss.hasPermi('system:student:remove')")
    @Log(title = "学生花名册", businessType = BusinessType.DELETE)
    @DeleteMapping("/{studentIds}")
    public AjaxResult remove(@PathVariable Long[] studentIds)
    {
        if (studentIds == null || studentIds.length == 0)
        {
            return toAjax(0);
        }
        if (!isSchoolWide())
        {
            for (Long id : studentIds)
            {
                CjStudent s = cjStudentService.selectCjStudentByStudentId(id);
                if (!canManage(s))
                {
                    return error("无权操作其他班级学生");
                }
            }
        }
        if (cjStudentBindMapper.countByStudentIds(studentIds) > 0)
        {
            return error("所选学生中已存在微信绑定记录,禁止删除;请改用“停用”");
        }
        return toAjax(cjStudentService.deleteCjStudentByStudentIds(studentIds));
    }
 
    /**
     * 学生停用/启用(0正常 1停用)
     */
    @PreAuthorize("@ss.hasPermi('system:student:status')")
    @Log(title = "学生花名册", businessType = BusinessType.UPDATE)
    @PutMapping("/changeStatus")
    public AjaxResult changeStatus(@RequestBody CjStudent cjStudent)
    {
        if (cjStudent.getStudentId() == null)
        {
            return error("缺少学生ID");
        }
        if (!isSchoolWide())
        {
            CjStudent old = cjStudentService.selectCjStudentByStudentId(cjStudent.getStudentId());
            if (!canManage(old))
            {
                return error("无权操作其他班级学生");
            }
        }
        CjStudent update = new CjStudent();
        update.setStudentId(cjStudent.getStudentId());
        update.setStatus(cjStudent.getStatus());
        update.setUpdateBy(getUsername());
        return toAjax(cjStudentService.updateCjStudent(update));
    }
 
    /**
     * 重置学生登录密码为初始密码(AA+学号),并置“下次登录强制改密”;
     * 同时作废该生全部在线会话与登录失败计数。教师仅限本班学生。
     */
    @PreAuthorize("@ss.hasPermi('system:student:resetPwd')")
    @Log(title = "学生账号", businessType = BusinessType.UPDATE)
    @PutMapping("/resetPwd/{studentId}")
    public AjaxResult resetPwd(@PathVariable("studentId") Long studentId)
    {
        CjStudent stu = cjStudentService.selectCjStudentByStudentId(studentId);
        if (stu == null)
        {
            return error("学生不存在");
        }
        if (!canManage(stu))
        {
            return error("无权操作其他班级学生");
        }
        String initPwd = initialPassword(stu.getStudentNo());
        // 从未激活过的学生被重置:视为激活,补 register_time,避免"已激活但激活时间为空"
        boolean firstActivate = cjStudentMapper.selectPasswordByStudentId(studentId) == null;
        cjStudentMapper.updateStudentPassword(studentId, SecurityUtils.encryptPassword(initPwd), "1",
                firstActivate ? new java.util.Date() : null);
        miniappTokenService.invalidateAll(studentId);
        // 解除该生的登录失败锁与自助重置锁,否则学生拿到初始密码也可能被锁在门外
        redisCache.deleteObject(MiniappTokenService.pwdErrKey(stu.getStudentNo(), stu.getName()));
        redisCache.deleteObject(MiniappTokenService.resetErrKey(stu.getStudentNo(), stu.getName()));
        return success("密码已重置为初始密码(AA+学号),学生下次登录须修改密码");
    }
 
    /**
     * 强制下线:作废该学生全部小程序/H5 登录会话(下次请求即需重新登录)。教师仅限本班学生。
     */
    @PreAuthorize("@ss.hasPermi('system:student:forceLogout')")
    @Log(title = "学生账号", businessType = BusinessType.UPDATE)
    @DeleteMapping("/forceLogout/{studentId}")
    public AjaxResult forceLogout(@PathVariable("studentId") Long studentId)
    {
        CjStudent stu = cjStudentService.selectCjStudentByStudentId(studentId);
        if (stu == null)
        {
            return error("学生不存在");
        }
        if (!canManage(stu))
        {
            return error("无权操作其他班级学生");
        }
        miniappTokenService.invalidateAll(studentId);
        return success("已强制下线");
    }
 
    /** 初始密码规则:AA + 完整学号(满足“8-20位且含字母和数字”的密码策略) */
    private String initialPassword(String studentNo)
    {
        return "AA" + (studentNo == null ? "" : studentNo.trim());
    }
 
    /**
     * 导入学生花名册
     * 模板列:所属学院 / 所属专业 / 班级名称 / 学号 / 姓名 / 性别(男/女) / 联系电话
     * 判重键:所属班级 + 学号(班级内学号唯一);班级通过 学院→专业→班级名称 三级解析。
     * @param file 上传的 Excel
     * @param updateSupport 是否覆盖更新已存在数据(true=覆盖)
     * @return {新增X 更新Y 失败Z + 失败名单}
     */
    @PreAuthorize("@ss.hasPermi('system:student:import')")
    @Log(title = "学生花名册", businessType = BusinessType.IMPORT)
    @PostMapping("/importData")
    public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
    {
        ExcelUtil<CjStudent> util = new ExcelUtil<CjStudent>(CjStudent.class);
        List<CjStudent> studentList = util.importExcel(file.getInputStream());
        String message;
        int insertCount = 0, updateCount = 0, failCount = 0;
        StringBuilder failMsg = new StringBuilder();
        String operName = getUsername();
 
        for (int i = 0; i < studentList.size(); i++)
        {
            CjStudent stu = studentList.get(i);
            int rowNo = i + 1;
            try
            {
                String collegeName = trim(stu.getCollegeName());
                String majorName = trim(stu.getMajorName());
                String className = trim(stu.getClassName());
                String studentNo = trim(stu.getStudentNo());
                String stuName = trim(stu.getName());
                if (collegeName == null)
                {
                    failCount++; failMsg.append("第" + rowNo + "条: 所属学院不能为空; "); continue;
                }
                if (majorName == null)
                {
                    failCount++; failMsg.append("第" + rowNo + "条: 所属专业不能为空; "); continue;
                }
                if (className == null)
                {
                    failCount++; failMsg.append("第" + rowNo + "条: 班级名称不能为空; "); continue;
                }
                if (studentNo == null)
                {
                    failCount++; failMsg.append("第" + rowNo + "条: 学号不能为空; "); continue;
                }
                if (stuName == null)
                {
                    failCount++; failMsg.append("第" + rowNo + "条: 姓名不能为空; "); continue;
                }
 
                // 三级名称解析到班级(父级须启用中;班级须已存在且启用)
                List<CjCollege> colleges = cjCollegeMapper.selectEnabledByCollegeName(collegeName);
                if (colleges.isEmpty())
                {
                    failCount++;
                    failMsg.append("第" + rowNo + "条: 所属学院“" + collegeName + "”不存在或已停用; ");
                    continue;
                }
                if (colleges.size() > 1)
                {
                    failCount++;
                    failMsg.append("第" + rowNo + "条: 所属学院“" + collegeName + "”在系统中重复,请先清理; ");
                    continue;
                }
                Long collegeId = colleges.get(0).getCollegeId();
                List<CjMajor> majors = cjMajorMapper.selectEnabledByCollegeIdAndName(collegeId, majorName);
                if (majors.isEmpty())
                {
                    failCount++;
                    failMsg.append("第" + rowNo + "条: 所属专业“" + majorName + "”在学院“" + collegeName + "”下不存在或已停用; ");
                    continue;
                }
                if (majors.size() > 1)
                {
                    failCount++;
                    failMsg.append("第" + rowNo + "条: 专业“" + majorName + "”在该学院下重复,请先清理; ");
                    continue;
                }
                Long majorId = majors.get(0).getMajorId();
                List<CjClass> classes = cjClassMapper.selectByMajorIdAndName(majorId, className);
                if (classes.isEmpty())
                {
                    failCount++;
                    failMsg.append("第" + rowNo + "条: 班级“" + className + "”在专业“" + majorName + "”下不存在,请先在班级管理中建立; ");
                    continue;
                }
                if (classes.size() > 1)
                {
                    failCount++;
                    failMsg.append("第" + rowNo + "条: 班级“" + className + "”在该专业下重复,无法定位; ");
                    continue;
                }
                CjClass cl = classes.get(0);
                if (cl.getStatus() != null && !"0".equals(cl.getStatus()))
                {
                    failCount++;
                    failMsg.append("第" + rowNo + "条: 班级“" + className + "”已停用,无法导入学生; ");
                    continue;
                }
 
                // 性别兼容 男/女/未知 与 0/1/2
                String gender = parseGender(stu.getGenderText());
                if (GENDER_INVALID.equals(gender))
                {
                    failCount++;
                    failMsg.append("第" + rowNo + "条: 学号" + studentNo + " 性别“" + stu.getGenderText() + "”无法识别(请填男/女/未知); ");
                    continue;
                }
 
                stu.setCollegeName(collegeName);
                stu.setMajorName(majorName);
                stu.setClassName(className);
                stu.setClassId(cl.getClassId());
                stu.setStudentNo(studentNo);
                stu.setName(stuName);
                stu.setGender(gender);
                if (stu.getPhone() != null)
                {
                    stu.setPhone(stu.getPhone().trim());
                }
 
                // 判重:班级 + 学号
                CjStudent exists = findByClassAndNo(cl.getClassId(), studentNo);
                if (exists == null)
                {
                    if (stu.getStatus() == null)
                    {
                        stu.setStatus("0");
                    }
                    stu.setCreateBy(operName);
                    cjStudentService.insertCjStudent(stu);
                    insertCount++;
                }
                else if (updateSupport)
                {
                    // 覆盖导入时电话列为空 = 保留原登记号码(写成空串会静默废掉学生的三要素自助找回密码)
                    if (stu.getPhone() != null && stu.getPhone().trim().isEmpty())
                    {
                        stu.setPhone(null);
                    }
                    stu.setStudentId(exists.getStudentId());
                    stu.setUpdateBy(operName);
                    cjStudentService.updateCjStudent(stu);
                    updateCount++;
                }
                else
                {
                    failCount++;
                    failMsg.append("第" + rowNo + "条: 学号" + studentNo + "在班级“" + className + "”中已存在; ");
                }
            }
            catch (Exception e)
            {
                failCount++;
                failMsg.append("第" + rowNo + "条" + (stu.getStudentNo() != null ? "(学号" + stu.getStudentNo() + ")" : "")
                    + "(导入失败:" + e.getMessage() + "); ");
            }
        }
 
        if (updateSupport)
            message = String.format("导入完成!新增 %d 条,更新 %d 条", insertCount, updateCount);
        else
            message = String.format("导入完成!新增 %d 条,由于数据重复导致导入失败 %d 条", insertCount, failCount);
 
        Map<String, Object> data = new HashMap<>();
        data.put("message", message);
        data.put("insertCount", insertCount);
        data.put("updateCount", updateCount);
        data.put("failCount", failCount);
        data.put("failMsg", failMsg.toString());
        return AjaxResult.success(data);
    }
 
    /** 字符串去首尾空白;null/空返回 null */
    private String trim(String s)
    {
        return (s == null || s.trim().isEmpty()) ? null : s.trim();
    }
 
    /**
     * 下载学生花名册导入模板
     */
    @PreAuthorize("@ss.hasPermi('system:student:import')")
    @PostMapping("/importTemplate")
    public void importTemplate(HttpServletResponse response) throws Exception
    {
        ExcelUtil<CjStudent> util = new ExcelUtil<CjStudent>(CjStudent.class);
        util.importTemplateExcel(response, "学生花名册导入模板");
    }
}