liuyu
21 小时以前 6eef0b6730d940ef44e0f66dd8e7d178f1cd2d1f
feat(tms): 实现培训管理业务模块

Co-authored-by: Cursor <cursoragent@cursor.com>
183个文件已添加
2个文件已修改
12833 ■■■■■ 已修改文件
jnpf-tms/jnpf-tms-biz/pom.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanItemMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsCourseMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsEvalModeMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamItemMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsMaterialMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperQuestionMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperSectionMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonFileMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTaskMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTrainMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonWorkMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPlaceMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanItemMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionBankMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionOptionMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsRecordMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestItemMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSignModeMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskFileMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainModeMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainerMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsCourseService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsEvalModeService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamGradeService.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamScoreService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsOnlineExamService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPaperService.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPersonTaskService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPlaceService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsQuestionService.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsRecordService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSelfTestService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSignModeService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTaskService.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainArchiveService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainModeService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainRecordService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsCourseServiceImpl.java 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsEvalModeServiceImpl.java 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamGradeServiceImpl.java 421 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamScoreServiceImpl.java 376 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsOnlineExamServiceImpl.java 1030 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPaperServiceImpl.java 558 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPersonTaskServiceImpl.java 543 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPlaceServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsQuestionServiceImpl.java 378 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsRecordServiceImpl.java 388 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSelfTestServiceImpl.java 484 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSignModeServiceImpl.java 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTaskServiceImpl.java 1069 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainArchiveServiceImpl.java 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainModeServiceImpl.java 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainRecordServiceImpl.java 446 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsCourseController.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsEvalModeController.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamGradeController.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamScoreController.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsOnlineExamController.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPaperController.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPersonTaskController.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPlaceController.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsQuestionController.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsRecordController.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSelfTestController.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSignModeController.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTaskController.java 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainArchiveController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainModeController.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainRecordController.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanEntity.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanItemEntity.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsCourseEntity.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsEvalModeEntity.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamEntity.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamItemEntity.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsMaterialEntity.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperEntity.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperQuestionEntity.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperSectionEntity.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonFileEntity.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTaskEntity.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTrainEntity.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonWorkEntity.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPlaceEntity.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanEntity.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanItemEntity.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionBankEntity.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionEntity.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionOptionEntity.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsRecordEntity.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestEntity.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestItemEntity.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSignModeEntity.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskEntity.java 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskFileEntity.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainModeEntity.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainerEntity.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveCatalogVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveDetailVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveProfileVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveWorkVO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseForm.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseQuery.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsPaperOption.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeForm.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeQuery.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamAttemptVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamDetailVO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamGradeItemVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamListVO.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamOptionVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamPaperVO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuery.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuestionVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamStartForm.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitForm.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitResultVO.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeDetailVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeExamVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeItemVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeOptionVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeQuery.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSessionVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitForm.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitResultVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreDetailVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreExportForm.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreOptionVO.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperItemVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreQuery.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreRowVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreSummaryVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperForm.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuery.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuestionForm.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperSectionForm.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskFileForm.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnForm.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnResult.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskMineForm.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskQuery.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskSignResult.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/place/TmsPlaceOption.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionBankOption.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionForm.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionOptionForm.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionQuery.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordAttachmentForm.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordForm.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordParticipantForm.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordQuery.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordSignForm.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestOptionVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestPaperVO.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuery.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuestionVO.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestRecordVO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestStartForm.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitForm.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitResultVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestTypeSetting.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeForm.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeQuery.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsFromPlanResult.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsPersonTaskForm.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskFileForm.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskForm.java 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskQuery.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeForm.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeQuery.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogItemVO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordListVO.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordQuery.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-server/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-server/src/main/resources/tms-datasource.yaml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jnpf-tms/jnpf-tms-biz/pom.xml
@@ -32,5 +32,10 @@
            <version>${project.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.jnpf</groupId>
            <artifactId>jnpf-permission-api</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
</project>
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanItemMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsAnnualPlanItemEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsAnnualPlanItemMapper extends BaseMapper<TmsAnnualPlanItemEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsAnnualPlanEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsAnnualPlanMapper extends BaseMapper<TmsAnnualPlanEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsCourseMapper.java
New file
@@ -0,0 +1,14 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsCourseEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TmsCourseMapper extends BaseMapper<TmsCourseEntity> {
    @Select("SELECT MAX(course_no) FROM tms_course WHERE course_no LIKE CONCAT(#{prefix}, '%') AND (f_delete_mark = 0 OR f_delete_mark IS NULL)")
    String selectMaxCourseNo(@Param("prefix") String prefix);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsEvalModeMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsEvalModeEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsEvalModeMapper extends BaseMapper<TmsEvalModeEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamItemMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsExamItemEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsExamItemMapper extends BaseMapper<TmsExamItemEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsExamEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsExamMapper extends BaseMapper<TmsExamEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsMaterialMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsMaterialEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsMaterialMapper extends BaseMapper<TmsMaterialEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperMapper.java
New file
@@ -0,0 +1,18 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsPaperEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TmsPaperMapper extends BaseMapper<TmsPaperEntity> {
    /**
     * 当年最大试卷编号(含已逻辑删除,避免流水号回退复用)。
     * 格式:yy + 4 位流水,如 260001。
     */
    @Select("SELECT MAX(paper_no) FROM tms_paper WHERE paper_no LIKE CONCAT(#{prefix}, '%') AND LENGTH(paper_no) = 6")
    String selectMaxPaperNo(@Param("prefix") String prefix);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperQuestionMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsPaperQuestionEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsPaperQuestionMapper extends BaseMapper<TmsPaperQuestionEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperSectionMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsPaperSectionEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsPaperSectionMapper extends BaseMapper<TmsPaperSectionEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonFileMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsPersonFileEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsPersonFileMapper extends BaseMapper<TmsPersonFileEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTaskMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsPersonTaskEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsPersonTaskMapper extends BaseMapper<TmsPersonTaskEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTrainMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsPersonTrainEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsPersonTrainMapper extends BaseMapper<TmsPersonTrainEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonWorkMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsPersonWorkEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsPersonWorkMapper extends BaseMapper<TmsPersonWorkEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPlaceMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsPlaceEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsPlaceMapper extends BaseMapper<TmsPlaceEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanItemMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsPostPlanItemEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsPostPlanItemMapper extends BaseMapper<TmsPostPlanItemEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsPostPlanEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsPostPlanMapper extends BaseMapper<TmsPostPlanEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionBankMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsQuestionBankEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsQuestionBankMapper extends BaseMapper<TmsQuestionBankEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionMapper.java
New file
@@ -0,0 +1,18 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsQuestionEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TmsQuestionMapper extends BaseMapper<TmsQuestionEntity> {
    /**
     * 当年最大试题编号(含已逻辑删除,避免流水号回退复用)。
     * 格式:yy + 4 位流水,如 260001。
     */
    @Select("SELECT MAX(question_no) FROM tms_question WHERE question_no LIKE CONCAT(#{prefix}, '%') AND LENGTH(question_no) = 6")
    String selectMaxQuestionNo(@Param("prefix") String prefix);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionOptionMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsQuestionOptionEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsQuestionOptionMapper extends BaseMapper<TmsQuestionOptionEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsRecordMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsRecordEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsRecordMapper extends BaseMapper<TmsRecordEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestItemMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsSelfTestItemEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsSelfTestItemMapper extends BaseMapper<TmsSelfTestItemEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsSelfTestEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsSelfTestMapper extends BaseMapper<TmsSelfTestEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSignModeMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsSignModeEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsSignModeMapper extends BaseMapper<TmsSignModeEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskFileMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsTaskFileEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsTaskFileMapper extends BaseMapper<TmsTaskFileEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskMapper.java
New file
@@ -0,0 +1,18 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsTaskEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TmsTaskMapper extends BaseMapper<TmsTaskEntity> {
    /**
     * 当月最大任务编号(含已逻辑删除,避免流水回退)。
     * 格式:前缀(2) + yyyyMM(6) + 4位 = 12位,如 TT2026090001 / TD2026090001。
     */
    @Select("SELECT MAX(task_no) FROM tms_task WHERE task_no LIKE CONCAT(#{prefix}, '%') AND LENGTH(task_no) = 12")
    String selectMaxTaskNo(@Param("prefix") String prefix);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainModeMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsTrainModeEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsTrainModeMapper extends BaseMapper<TmsTrainModeEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainerMapper.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import jnpf.tmsEntity.TmsTrainerEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TmsTrainerMapper extends BaseMapper<TmsTrainerEntity> {
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsCourseService.java
New file
@@ -0,0 +1,24 @@
package jnpf.tmsService;
import jnpf.tmsEntity.course.TmsCourseForm;
import jnpf.tmsEntity.course.TmsCourseQuery;
import jnpf.tmsEntity.course.TmsPaperOption;
import java.util.List;
public interface TmsCourseService {
    List<TmsCourseForm> getList(TmsCourseQuery query);
    TmsCourseForm getInfo(String id);
    String create(TmsCourseForm form);
    void update(String id, TmsCourseForm form);
    void setEnabled(String id, String enabled);
    void delete(String id);
    List<TmsPaperOption> paperOptions();
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsEvalModeService.java
New file
@@ -0,0 +1,21 @@
package jnpf.tmsService;
import jnpf.tmsEntity.evalmode.TmsEvalModeForm;
import jnpf.tmsEntity.evalmode.TmsEvalModeQuery;
import java.util.List;
public interface TmsEvalModeService {
    List<TmsEvalModeForm> getList(TmsEvalModeQuery query);
    TmsEvalModeForm getInfo(String id);
    String create(TmsEvalModeForm form);
    void update(String id, TmsEvalModeForm form);
    void setEnabled(String id, String enabled);
    void delete(String id);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamGradeService.java
New file
@@ -0,0 +1,23 @@
package jnpf.tmsService;
import jnpf.tmsEntity.examgrade.TmsExamGradeDetailVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeExamVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeQuery;
import jnpf.tmsEntity.examgrade.TmsExamGradeSessionVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeSubmitForm;
import jnpf.tmsEntity.examgrade.TmsExamGradeSubmitResultVO;
import java.util.List;
public interface TmsExamGradeService {
    List<TmsExamGradeSessionVO> getSessionList(TmsExamGradeQuery query);
    TmsExamGradeSessionVO getSession(String sessionId);
    List<TmsExamGradeExamVO> getExamList(String sessionId);
    TmsExamGradeDetailVO getExamDetail(String examId);
    TmsExamGradeSubmitResultVO submitGrade(TmsExamGradeSubmitForm form);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamScoreService.java
New file
@@ -0,0 +1,21 @@
package jnpf.tmsService;
import jnpf.tmsEntity.examscore.TmsExamScoreDetailVO;
import jnpf.tmsEntity.examscore.TmsExamScorePaperVO;
import jnpf.tmsEntity.examscore.TmsExamScoreQuery;
import jnpf.tmsEntity.examscore.TmsExamScoreRowVO;
import jnpf.tmsEntity.examscore.TmsExamScoreSummaryVO;
import java.util.List;
public interface TmsExamScoreService {
    List<TmsExamScoreSummaryVO> getList(TmsExamScoreQuery query);
    TmsExamScoreDetailVO getDetail(String summaryId);
    TmsExamScorePaperVO getPaperView(String examId);
    /** 按考核对象导出已勾选试卷的成绩 */
    List<TmsExamScoreRowVO> exportRows(List<String> summaryIds);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsOnlineExamService.java
New file
@@ -0,0 +1,26 @@
package jnpf.tmsService;
import jnpf.tmsEntity.exam.TmsOnlineExamDetailVO;
import jnpf.tmsEntity.exam.TmsOnlineExamListVO;
import jnpf.tmsEntity.exam.TmsOnlineExamPaperVO;
import jnpf.tmsEntity.exam.TmsOnlineExamQuery;
import jnpf.tmsEntity.exam.TmsOnlineExamSubmitForm;
import jnpf.tmsEntity.exam.TmsOnlineExamSubmitResultVO;
import java.util.List;
public interface TmsOnlineExamService {
    List<TmsOnlineExamListVO> getMyPapers(TmsOnlineExamQuery query);
    TmsOnlineExamDetailVO getDetail(String paperId);
    /** 按答卷ID查看已交卷回顾 */
    TmsOnlineExamDetailVO getExamReview(String examId);
    TmsOnlineExamPaperVO start(String paperId);
    void saveAnswers(String examId, TmsOnlineExamSubmitForm form);
    TmsOnlineExamSubmitResultVO submit(String examId, TmsOnlineExamSubmitForm form);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPaperService.java
New file
@@ -0,0 +1,23 @@
package jnpf.tmsService;
import jnpf.tmsEntity.paper.TmsPaperForm;
import jnpf.tmsEntity.paper.TmsPaperQuery;
import java.util.List;
public interface TmsPaperService {
    List<TmsPaperForm> getList(TmsPaperQuery query);
    TmsPaperForm getInfo(String id);
    String create(TmsPaperForm form);
    void update(String id, TmsPaperForm form);
    /** 废弃:biz_status=invalid */
    void invalidate(String id);
    /** 启用/停用:open ↔ closed(废弃卷不可) */
    void setBizStatus(String id, String bizStatus);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPersonTaskService.java
New file
@@ -0,0 +1,20 @@
package jnpf.tmsService;
import jnpf.tmsEntity.person.TmsPersonTaskLearnResult;
import jnpf.tmsEntity.person.TmsPersonTaskMineForm;
import jnpf.tmsEntity.person.TmsPersonTaskQuery;
import jnpf.tmsEntity.person.TmsPersonTaskSignResult;
import java.util.List;
public interface TmsPersonTaskService {
    List<TmsPersonTaskMineForm> getMine(TmsPersonTaskQuery query);
    TmsPersonTaskMineForm getInfo(String id);
    TmsPersonTaskLearnResult markLearned(String id, Integer seconds);
    /** 在线签到:校验培训方式是否允许、是否在培训时间窗内,写入签到时间 */
    TmsPersonTaskSignResult sign(String id);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPlaceService.java
New file
@@ -0,0 +1,14 @@
package jnpf.tmsService;
import jnpf.tmsEntity.place.TmsPlaceOption;
import java.util.List;
/**
 * 培训地点
 */
public interface TmsPlaceService {
    /** 启用中的地点下拉(供任务发布、培训方式「地点必填」联动选用) */
    List<TmsPlaceOption> listOptions();
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsQuestionService.java
New file
@@ -0,0 +1,25 @@
package jnpf.tmsService;
import jnpf.tmsEntity.question.TmsQuestionBankOption;
import jnpf.tmsEntity.question.TmsQuestionForm;
import jnpf.tmsEntity.question.TmsQuestionQuery;
import java.util.List;
import java.util.Map;
public interface TmsQuestionService {
    List<TmsQuestionBankOption> listBanks();
    List<Map<String, String>> listAdmins();
    List<TmsQuestionForm> getList(TmsQuestionQuery query);
    TmsQuestionForm getInfo(String id);
    String create(TmsQuestionForm form);
    void update(String id, TmsQuestionForm form);
    void invalidate(String id);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsRecordService.java
New file
@@ -0,0 +1,19 @@
package jnpf.tmsService;
import jnpf.tmsEntity.record.TmsRecordForm;
import jnpf.tmsEntity.record.TmsRecordQuery;
import java.util.List;
public interface TmsRecordService {
    List<TmsRecordForm> getList(TmsRecordQuery query);
    TmsRecordForm getInfo(String id);
    void update(String id, TmsRecordForm form);
    int archive(List<String> ids);
    void delete(List<String> ids);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSelfTestService.java
New file
@@ -0,0 +1,24 @@
package jnpf.tmsService;
import jnpf.tmsEntity.selftest.TmsSelfTestPaperVO;
import jnpf.tmsEntity.selftest.TmsSelfTestQuery;
import jnpf.tmsEntity.selftest.TmsSelfTestRecordVO;
import jnpf.tmsEntity.selftest.TmsSelfTestStartForm;
import jnpf.tmsEntity.selftest.TmsSelfTestSubmitForm;
import jnpf.tmsEntity.selftest.TmsSelfTestSubmitResultVO;
import java.util.List;
public interface TmsSelfTestService {
    TmsSelfTestPaperVO start(TmsSelfTestStartForm form);
    /** 暂存作答(不交卷、不计分) */
    void saveAnswers(String id, TmsSelfTestSubmitForm form);
    TmsSelfTestSubmitResultVO submit(String id, TmsSelfTestSubmitForm form);
    List<TmsSelfTestRecordVO> getList(TmsSelfTestQuery query);
    TmsSelfTestPaperVO getInfo(String id);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSignModeService.java
New file
@@ -0,0 +1,21 @@
package jnpf.tmsService;
import jnpf.tmsEntity.signmode.TmsSignModeForm;
import jnpf.tmsEntity.signmode.TmsSignModeQuery;
import java.util.List;
public interface TmsSignModeService {
    List<TmsSignModeForm> getList(TmsSignModeQuery query);
    TmsSignModeForm getInfo(String id);
    String create(TmsSignModeForm form);
    void update(String id, TmsSignModeForm form);
    void setEnabled(String id, String enabled);
    void delete(String id);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTaskService.java
New file
@@ -0,0 +1,44 @@
package jnpf.tmsService;
import jnpf.tmsEntity.task.TmsFromPlanResult;
import jnpf.tmsEntity.task.TmsTaskForm;
import jnpf.tmsEntity.task.TmsTaskQuery;
import java.util.List;
public interface TmsTaskService {
    List<TmsTaskForm> getList(TmsTaskQuery query);
    TmsTaskForm getInfo(String id);
    String create(TmsTaskForm form);
    void update(String id, TmsTaskForm form);
    void delete(String id);
    /** 发布:生成 task_no、个人任务、培训记录 */
    void publish(String id);
    /** 取消/终止(有人已签到则不可) */
    void cancel(String id);
    /** 批量取消 */
    void cancelBatch(List<String> ids);
    /** 恢复为草稿 */
    void restore(String id);
    /**
     * 年计划审批通过:按子表生成未发布培训任务。
     * 已存在 source_item_id 的行跳过,可重复调用。
     */
    TmsFromPlanResult createFromAnnual(String planId);
    /**
     * 岗位培训计划审批通过:按子表生成未发布培训任务。
     * 培训对象取计划主表;已存在 source_item_id 的行跳过。
     */
    TmsFromPlanResult createFromPost(String planId);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainArchiveService.java
New file
@@ -0,0 +1,12 @@
package jnpf.tmsService;
import jnpf.tmsEntity.archive.TmsTrainArchiveDetailVO;
/**
 * 个人培训档案
 */
public interface TmsTrainArchiveService {
    /** 当前登录用户的个人培训档案(无档则自动建档) */
    TmsTrainArchiveDetailVO getMine();
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainModeService.java
New file
@@ -0,0 +1,24 @@
package jnpf.tmsService;
import jnpf.tmsEntity.trainmode.TmsTrainModeForm;
import jnpf.tmsEntity.trainmode.TmsTrainModeQuery;
import java.util.List;
/**
 * 培训方式配置
 */
public interface TmsTrainModeService {
    List<TmsTrainModeForm> getList(TmsTrainModeQuery query);
    TmsTrainModeForm getInfo(String id);
    String create(TmsTrainModeForm form);
    void update(String id, TmsTrainModeForm form);
    void setEnabled(String id, String enabled);
    void delete(String id);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainRecordService.java
New file
@@ -0,0 +1,14 @@
package jnpf.tmsService;
import jnpf.tmsEntity.trainrecord.TmsTrainRecordCatalogVO;
import jnpf.tmsEntity.trainrecord.TmsTrainRecordListVO;
import jnpf.tmsEntity.trainrecord.TmsTrainRecordQuery;
import java.util.List;
public interface TmsTrainRecordService {
    List<TmsTrainRecordListVO> getList(TmsTrainRecordQuery query);
    TmsTrainRecordCatalogVO getCatalog(String archiveId, String startDate, String endDate);
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsCourseServiceImpl.java
New file
@@ -0,0 +1,245 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jnpf.exception.DataException;
import jnpf.tmsEntity.TmsCourseEntity;
import jnpf.tmsEntity.TmsPaperEntity;
import jnpf.tmsEntity.course.TmsCourseForm;
import jnpf.tmsEntity.course.TmsCourseQuery;
import jnpf.tmsEntity.course.TmsPaperOption;
import jnpf.tmsMapper.TmsCourseMapper;
import jnpf.tmsMapper.TmsPaperMapper;
import jnpf.tmsService.TmsCourseService;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
@RequiredArgsConstructor
public class TmsCourseServiceImpl implements TmsCourseService {
    private final TmsCourseMapper courseMapper;
    private final TmsPaperMapper paperMapper;
    @Override
    public List<TmsCourseForm> getList(TmsCourseQuery query) {
        LambdaQueryWrapper<TmsCourseEntity> qw = new LambdaQueryWrapper<>();
        if (StringUtil.isNotEmpty(query.getEnabled())) {
            qw.eq(TmsCourseEntity::getEnabled, query.getEnabled());
        }
        if (StringUtil.isNotEmpty(query.getCategory())) {
            qw.eq(TmsCourseEntity::getCategory, query.getCategory());
        }
        if (StringUtil.isNotEmpty(query.getTrainMode())) {
            qw.eq(TmsCourseEntity::getTrainMode, query.getTrainMode());
        }
        if (StringUtil.isNotEmpty(query.getEvalMode())) {
            qw.eq(TmsCourseEntity::getEvalMode, query.getEvalMode());
        }
        if (StringUtil.isNotEmpty(query.getKeyword())) {
            String kw = query.getKeyword().trim();
            qw.and(w -> w.like(TmsCourseEntity::getCourseNo, kw)
                    .or()
                    .like(TmsCourseEntity::getCourseName, kw));
        }
        qw.orderByDesc(TmsCourseEntity::getCourseNo);
        qw.orderByDesc(TmsCourseEntity::getCreatorTime);
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        Page<TmsCourseEntity> page = courseMapper.selectPage(new Page<>(current, size), qw);
        Map<String, String> paperNames = loadPaperNames(page.getRecords());
        List<TmsCourseForm> list = new ArrayList<>(page.getRecords().size());
        for (TmsCourseEntity row : page.getRecords()) {
            list.add(toForm(row, paperNames));
        }
        query.setData(list, page.getTotal());
        return list;
    }
    @Override
    public TmsCourseForm getInfo(String id) {
        TmsCourseEntity entity = requireOne(id);
        Map<String, String> paperNames = loadPaperNames(Collections.singletonList(entity));
        return toForm(entity, paperNames);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String create(TmsCourseForm form) {
        validateForm(form);
        TmsCourseEntity entity = new TmsCourseEntity();
        entity.setId(RandomUtil.uuId());
        entity.setCourseNo(nextCourseNo());
        applyForm(entity, form);
        entity.setCreatorUserId(UserProvider.getLoginUserId());
        entity.setCreatorTime(new Date());
        courseMapper.insert(entity);
        return entity.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(String id, TmsCourseForm form) {
        TmsCourseEntity entity = requireOne(id);
        validateForm(form);
        applyForm(entity, form);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        courseMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void setEnabled(String id, String enabled) {
        if (!"0".equals(enabled) && !"1".equals(enabled)) {
            throw new DataException("启用状态无效");
        }
        TmsCourseEntity entity = requireOne(id);
        entity.setEnabled(enabled);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        courseMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete(String id) {
        TmsCourseEntity entity = requireOne(id);
        if (!"0".equals(entity.getEnabled())) {
            throw new DataException("请先停用后再删除");
        }
        courseMapper.deleteById(id);
    }
    @Override
    public List<TmsPaperOption> paperOptions() {
        LambdaQueryWrapper<TmsPaperEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsPaperEntity::getBizStatus, "open");
        qw.orderByDesc(TmsPaperEntity::getCreatorTime);
        List<TmsPaperEntity> rows = paperMapper.selectList(qw);
        List<TmsPaperOption> list = new ArrayList<>(rows.size());
        for (TmsPaperEntity row : rows) {
            TmsPaperOption opt = new TmsPaperOption();
            opt.setId(row.getId());
            opt.setFullName(row.getPaperName());
            list.add(opt);
        }
        return list;
    }
    private String nextCourseNo() {
        String prefix = "KC" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
        String maxNo = courseMapper.selectMaxCourseNo(prefix);
        int next = 1;
        if (StringUtil.isNotEmpty(maxNo) && maxNo.length() >= prefix.length() + 4 && maxNo.startsWith(prefix)) {
            try {
                next = Integer.parseInt(maxNo.substring(prefix.length())) + 1;
            } catch (NumberFormatException ignored) {
                next = 1;
            }
        }
        if (next > 9999) {
            throw new DataException("当月课程编号已用尽(" + prefix + "9999)");
        }
        return prefix + String.format("%04d", next);
    }
    private Map<String, String> loadPaperNames(List<TmsCourseEntity> rows) {
        Set<String> ids = new HashSet<>();
        for (TmsCourseEntity row : rows) {
            if (StringUtil.isNotEmpty(row.getPaperId())) {
                ids.add(row.getPaperId());
            }
        }
        if (ids.isEmpty()) {
            return Collections.emptyMap();
        }
        List<TmsPaperEntity> papers = paperMapper.selectBatchIds(ids);
        Map<String, String> map = new HashMap<>(papers.size());
        for (TmsPaperEntity paper : papers) {
            map.put(paper.getId(), paper.getPaperName());
        }
        return map;
    }
    private TmsCourseEntity requireOne(String id) {
        if (StringUtil.isEmpty(id)) {
            throw new DataException("培训课程不存在");
        }
        TmsCourseEntity entity = courseMapper.selectById(id);
        if (entity == null) {
            throw new DataException("培训课程不存在");
        }
        return entity;
    }
    private void validateForm(TmsCourseForm form) {
        if (form == null || StringUtil.isEmpty(form.getCourseName())) {
            throw new DataException("请填写课程名称");
        }
        if ("exam".equals(form.getEvalMode()) && StringUtil.isEmpty(form.getPaperId())) {
            throw new DataException("在线考试须关联考核试卷");
        }
        if (StringUtil.isNotEmpty(form.getPaperId())) {
            TmsPaperEntity paper = paperMapper.selectById(form.getPaperId());
            if (paper == null) {
                throw new DataException("关联试卷不存在");
            }
        }
    }
    private void applyForm(TmsCourseEntity entity, TmsCourseForm form) {
        entity.setCourseName(form.getCourseName().trim());
        entity.setCategory(StringUtil.isEmpty(form.getCategory()) ? null : form.getCategory().trim());
        entity.setTrainMode(StringUtil.isEmpty(form.getTrainMode()) ? null : form.getTrainMode().trim());
        entity.setEvalMode(StringUtil.isEmpty(form.getEvalMode()) ? null : form.getEvalMode().trim());
        if ("exam".equals(entity.getEvalMode())) {
            entity.setPaperId(form.getPaperId());
        } else {
            entity.setPaperId(StringUtil.isEmpty(form.getPaperId()) ? null : form.getPaperId());
        }
        entity.setHours(form.getHours());
        entity.setEnabled(yn(form.getEnabled(), "1"));
        entity.setRemark(form.getRemark());
    }
    private static String yn(String v, String def) {
        if ("0".equals(v) || "1".equals(v)) {
            return v;
        }
        return def;
    }
    private TmsCourseForm toForm(TmsCourseEntity entity, Map<String, String> paperNames) {
        TmsCourseForm form = new TmsCourseForm();
        form.setId(entity.getId());
        form.setCourseNo(entity.getCourseNo());
        form.setCourseName(entity.getCourseName());
        form.setCategory(entity.getCategory());
        form.setTrainMode(entity.getTrainMode());
        form.setEvalMode(entity.getEvalMode());
        form.setPaperId(entity.getPaperId());
        form.setPaperName(StringUtil.isEmpty(entity.getPaperId()) ? null : paperNames.get(entity.getPaperId()));
        form.setHours(entity.getHours());
        form.setEnabled(entity.getEnabled());
        form.setRemark(entity.getRemark());
        return form;
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsEvalModeServiceImpl.java
New file
@@ -0,0 +1,166 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jnpf.exception.DataException;
import jnpf.tmsEntity.TmsEvalModeEntity;
import jnpf.tmsEntity.evalmode.TmsEvalModeForm;
import jnpf.tmsEntity.evalmode.TmsEvalModeQuery;
import jnpf.tmsMapper.TmsEvalModeMapper;
import jnpf.tmsService.TmsEvalModeService;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TmsEvalModeServiceImpl implements TmsEvalModeService {
    private final TmsEvalModeMapper evalModeMapper;
    @Override
    public List<TmsEvalModeForm> getList(TmsEvalModeQuery query) {
        LambdaQueryWrapper<TmsEvalModeEntity> qw = new LambdaQueryWrapper<>();
        if (StringUtil.isNotEmpty(query.getEnabled())) {
            qw.eq(TmsEvalModeEntity::getEnabled, query.getEnabled());
        }
        if (StringUtil.isNotEmpty(query.getKeyword())) {
            String kw = query.getKeyword().trim();
            qw.and(w -> w.like(TmsEvalModeEntity::getModeCode, kw)
                    .or()
                    .like(TmsEvalModeEntity::getModeName, kw));
        }
        qw.orderByDesc(TmsEvalModeEntity::getCreatorTime);
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        Page<TmsEvalModeEntity> page = evalModeMapper.selectPage(new Page<>(current, size), qw);
        List<TmsEvalModeForm> list = new ArrayList<>(page.getRecords().size());
        for (TmsEvalModeEntity row : page.getRecords()) {
            list.add(toForm(row));
        }
        query.setData(list, page.getTotal());
        return list;
    }
    @Override
    public TmsEvalModeForm getInfo(String id) {
        return toForm(requireOne(id));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String create(TmsEvalModeForm form) {
        validateForm(form, null);
        TmsEvalModeEntity entity = new TmsEvalModeEntity();
        entity.setId(RandomUtil.uuId());
        applyForm(entity, form);
        entity.setCreatorUserId(UserProvider.getLoginUserId());
        entity.setCreatorTime(new Date());
        evalModeMapper.insert(entity);
        return entity.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(String id, TmsEvalModeForm form) {
        TmsEvalModeEntity entity = requireOne(id);
        validateForm(form, id);
        form.setModeCode(entity.getModeCode());
        applyForm(entity, form);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        evalModeMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void setEnabled(String id, String enabled) {
        if (!"0".equals(enabled) && !"1".equals(enabled)) {
            throw new DataException("启用状态无效");
        }
        TmsEvalModeEntity entity = requireOne(id);
        entity.setEnabled(enabled);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        evalModeMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete(String id) {
        TmsEvalModeEntity entity = requireOne(id);
        if (!"0".equals(entity.getEnabled())) {
            throw new DataException("请先停用后再删除");
        }
        evalModeMapper.deleteById(id);
    }
    private TmsEvalModeEntity requireOne(String id) {
        if (StringUtil.isEmpty(id)) {
            throw new DataException("考核方式不存在");
        }
        TmsEvalModeEntity entity = evalModeMapper.selectById(id);
        if (entity == null) {
            throw new DataException("考核方式不存在");
        }
        return entity;
    }
    private void validateForm(TmsEvalModeForm form, String excludeId) {
        if (form == null || StringUtil.isEmpty(form.getModeCode())) {
            throw new DataException("请填写方式编码");
        }
        if (StringUtil.isEmpty(form.getModeName())) {
            throw new DataException("请填写方式名称");
        }
        String code = form.getModeCode().trim();
        form.setModeCode(code);
        LambdaQueryWrapper<TmsEvalModeEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsEvalModeEntity::getModeCode, code);
        if (StringUtil.isNotEmpty(excludeId)) {
            qw.ne(TmsEvalModeEntity::getId, excludeId);
        }
        if (evalModeMapper.selectCount(qw) > 0) {
            throw new DataException("方式编码已存在");
        }
    }
    private void applyForm(TmsEvalModeEntity entity, TmsEvalModeForm form) {
        entity.setModeCode(form.getModeCode().trim());
        entity.setModeName(form.getModeName().trim());
        entity.setNeedPaper(yn(form.getNeedPaper(), "0"));
        entity.setNeedQuiz(yn(form.getNeedQuiz(), "0"));
        entity.setNeedPractice(yn(form.getNeedPractice(), "0"));
        entity.setEnabled(yn(form.getEnabled(), "1"));
        entity.setRemark(form.getRemark());
    }
    private static String yn(String v, String def) {
        if ("0".equals(v) || "1".equals(v)) {
            return v;
        }
        return def;
    }
    private TmsEvalModeForm toForm(TmsEvalModeEntity entity) {
        TmsEvalModeForm form = new TmsEvalModeForm();
        form.setId(entity.getId());
        form.setModeCode(entity.getModeCode());
        form.setModeName(entity.getModeName());
        form.setNeedPaper(entity.getNeedPaper());
        form.setNeedQuiz(entity.getNeedQuiz());
        form.setNeedPractice(entity.getNeedPractice());
        form.setEnabled(entity.getEnabled());
        form.setRemark(entity.getRemark());
        return form;
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamGradeServiceImpl.java
New file
@@ -0,0 +1,421 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jnpf.exception.DataException;
import jnpf.permission.UserApi;
import jnpf.permission.entity.UserEntity;
import jnpf.tmsEntity.TmsExamEntity;
import jnpf.tmsEntity.TmsExamItemEntity;
import jnpf.tmsEntity.TmsPaperEntity;
import jnpf.tmsEntity.examgrade.TmsExamGradeDetailVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeExamVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeItemVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeOptionVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeQuery;
import jnpf.tmsEntity.examgrade.TmsExamGradeSessionVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeSubmitForm;
import jnpf.tmsEntity.examgrade.TmsExamGradeSubmitResultVO;
import jnpf.tmsMapper.TmsExamItemMapper;
import jnpf.tmsMapper.TmsExamMapper;
import jnpf.tmsMapper.TmsPaperMapper;
import jnpf.tmsService.TmsExamGradeService;
import jnpf.util.DateUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class TmsExamGradeServiceImpl implements TmsExamGradeService {
    private static final String EXAM_SUBMITTED = "submitted";
    private static final String GRADE_PENDING = "pending";
    private static final String GRADE_GRADED = "graded";
    private static final String FMT = "yyyy-MM-dd HH:mm:ss";
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private final TmsExamMapper examMapper;
    private final TmsExamItemMapper examItemMapper;
    private final TmsPaperMapper paperMapper;
    private final UserApi userApi;
    @Override
    public List<TmsExamGradeSessionVO> getSessionList(TmsExamGradeQuery query) {
        List<TmsExamEntity> exams = loadGradeExams(null);
        Map<String, List<TmsExamEntity>> byPaper = groupByPaper(exams);
        Map<String, TmsPaperEntity> papers = loadPapers(new ArrayList<>(byPaper.keySet()));
        List<TmsExamGradeSessionVO> all = new ArrayList<>();
        for (Map.Entry<String, List<TmsExamEntity>> entry : byPaper.entrySet()) {
            all.add(toSession(entry.getKey(), papers.get(entry.getKey()), entry.getValue()));
        }
        all.sort(Comparator
                .comparing(TmsExamGradeSessionVO::getPendingCount, Comparator.nullsLast(Integer::compareTo)).reversed()
                .thenComparing(TmsExamGradeSessionVO::getExamEnd, Comparator.nullsLast(String::compareTo)).reversed());
        String keyword = trim(query.getKeyword());
        String taskNo = trim(query.getTaskNo());
        if (StringUtil.isNotEmpty(keyword)) {
            String kw = keyword.toLowerCase();
            all = all.stream()
                    .filter(x -> contains(x.getTaskSubject(), kw)
                            || contains(x.getPaperName(), kw)
                            || contains(x.getTaskNo(), kw))
                    .collect(Collectors.toList());
        }
        if (StringUtil.isNotEmpty(taskNo)) {
            String tn = taskNo.toLowerCase();
            all = all.stream().filter(x -> contains(x.getTaskNo(), tn)).collect(Collectors.toList());
        }
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        int from = (int) ((current - 1) * size);
        int to = Math.min(from + (int) size, all.size());
        List<TmsExamGradeSessionVO> page = from >= all.size() ? new ArrayList<>() : new ArrayList<>(all.subList(from, to));
        query.setData(page, all.size());
        return page;
    }
    @Override
    public TmsExamGradeSessionVO getSession(String sessionId) {
        if (StringUtil.isEmpty(sessionId)) {
            throw new DataException("阅卷场次不存在");
        }
        List<TmsExamEntity> exams = loadGradeExams(sessionId);
        if (exams.isEmpty()) {
            throw new DataException("阅卷场次不存在");
        }
        return toSession(sessionId, paperMapper.selectById(sessionId), exams);
    }
    @Override
    public List<TmsExamGradeExamVO> getExamList(String sessionId) {
        if (StringUtil.isEmpty(sessionId)) {
            return Collections.emptyList();
        }
        List<TmsExamEntity> exams = loadGradeExams(sessionId);
        exams.sort(Comparator.comparing(TmsExamEntity::getSubmitTime, Comparator.nullsLast(Date::compareTo)).reversed());
        Map<String, String> names = loadUserNames(exams.stream().map(TmsExamEntity::getUserId).collect(Collectors.toList()));
        List<TmsExamGradeExamVO> rows = new ArrayList<>();
        for (TmsExamEntity exam : exams) {
            TmsExamGradeExamVO vo = new TmsExamGradeExamVO();
            vo.setId(exam.getId());
            vo.setSessionId(sessionId);
            vo.setUserId(exam.getUserId());
            vo.setUserName(names.getOrDefault(exam.getUserId(), exam.getUserId()));
            vo.setSubmitTime(fmt(exam.getSubmitTime()));
            vo.setObjectiveScore(exam.getObjectiveScore());
            vo.setSubjectiveScore(exam.getSubjectiveScore());
            vo.setTotalScore(GRADE_PENDING.equals(exam.getGradeStatus()) ? null : exam.getTotalScore());
            vo.setPassScore(exam.getPassScore());
            vo.setGradeStatus(exam.getGradeStatus());
            vo.setPassFlag(exam.getPassFlag());
            rows.add(vo);
        }
        return rows;
    }
    @Override
    public TmsExamGradeDetailVO getExamDetail(String examId) {
        TmsExamEntity exam = requireGradeExam(examId);
        List<TmsExamItemEntity> items = loadItems(exam.getId());
        Map<String, String> names = loadUserNames(Collections.singletonList(exam.getUserId()));
        TmsExamGradeDetailVO vo = new TmsExamGradeDetailVO();
        vo.setExamId(exam.getId());
        vo.setSessionId(exam.getPaperId());
        vo.setPaperName(exam.getPaperName());
        vo.setUserName(names.getOrDefault(exam.getUserId(), exam.getUserId()));
        vo.setSubmitTime(fmt(exam.getSubmitTime()));
        vo.setTotalScore(resolvePaperTotal(exam));
        vo.setPassScore(exam.getPassScore());
        vo.setObjectiveScore(nvl(exam.getObjectiveScore()));
        vo.setSubjectiveScore(nvl(exam.getSubjectiveScore()));
        vo.setGradeStatus(exam.getGradeStatus());
        vo.setItems(items.stream().map(this::toItemVo).collect(Collectors.toList()));
        return vo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public TmsExamGradeSubmitResultVO submitGrade(TmsExamGradeSubmitForm form) {
        if (form == null || StringUtil.isEmpty(form.getExamId())) {
            throw new DataException("答卷不存在");
        }
        TmsExamEntity exam = requireGradeExam(form.getExamId());
        if (!GRADE_PENDING.equals(exam.getGradeStatus())) {
            throw new DataException("该答卷已批改,无需重复提交");
        }
        List<TmsExamItemEntity> items = loadItems(exam.getId());
        Map<String, BigDecimal> scoreMap = new HashMap<>();
        if (form.getItems() != null) {
            for (TmsExamGradeSubmitForm.ItemScore row : form.getItems()) {
                if (row == null || StringUtil.isEmpty(row.getId())) {
                    continue;
                }
                scoreMap.put(row.getId(), row.getGotScore());
            }
        }
        BigDecimal subjective = BigDecimal.ZERO;
        Date now = new Date();
        String graderId = UserProvider.getLoginUserId();
        for (TmsExamItemEntity item : items) {
            if (!"1".equals(item.getIsSubjective())) {
                continue;
            }
            if (!scoreMap.containsKey(item.getId()) || scoreMap.get(item.getId()) == null) {
                throw new DataException("请为所有主观题打分");
            }
            BigDecimal got = scoreMap.get(item.getId());
            BigDecimal max = nvl(item.getScore());
            if (got.compareTo(BigDecimal.ZERO) < 0 || got.compareTo(max) > 0) {
                throw new DataException("主观题得分需在 0 ~ " + max.stripTrailingZeros().toPlainString() + " 之间");
            }
            item.setGotScore(got);
            item.setLastModifyUserId(graderId);
            item.setLastModifyTime(now);
            examItemMapper.updateById(item);
            subjective = subjective.add(got);
        }
        BigDecimal objective = nvl(exam.getObjectiveScore());
        BigDecimal total = objective.add(subjective);
        exam.setSubjectiveScore(subjective);
        exam.setTotalScore(total);
        exam.setPassFlag(passFlag(total, exam.getPassScore()));
        exam.setGradeStatus(GRADE_GRADED);
        exam.setGraderId(graderId);
        exam.setGradeTime(now);
        exam.setLastModifyUserId(graderId);
        exam.setLastModifyTime(now);
        examMapper.updateById(exam);
        TmsExamGradeSubmitResultVO result = new TmsExamGradeSubmitResultVO();
        result.setExamId(exam.getId());
        result.setSubjectiveScore(subjective);
        result.setTotalScore(total);
        result.setPassFlag(exam.getPassFlag());
        result.setGradeStatus(GRADE_GRADED);
        result.setNextExamId(findNextPending(exam.getPaperId(), exam.getId()));
        return result;
    }
    private String findNextPending(String paperId, String excludeExamId) {
        List<TmsExamEntity> exams = loadGradeExams(paperId);
        return exams.stream()
                .filter(x -> GRADE_PENDING.equals(x.getGradeStatus()))
                .filter(x -> !Objects.equals(x.getId(), excludeExamId))
                .sorted(Comparator.comparing(TmsExamEntity::getSubmitTime, Comparator.nullsLast(Date::compareTo)))
                .map(TmsExamEntity::getId)
                .findFirst()
                .orElse(null);
    }
    private TmsExamEntity requireGradeExam(String examId) {
        if (StringUtil.isEmpty(examId)) {
            throw new DataException("答卷不存在");
        }
        TmsExamEntity exam = examMapper.selectById(examId);
        if (exam == null || !EXAM_SUBMITTED.equals(exam.getExamStatus())) {
            throw new DataException("答卷不存在");
        }
        if (!GRADE_PENDING.equals(exam.getGradeStatus()) && !GRADE_GRADED.equals(exam.getGradeStatus())) {
            throw new DataException("该答卷无需阅卷");
        }
        return exam;
    }
    private List<TmsExamEntity> loadGradeExams(String paperId) {
        LambdaQueryWrapper<TmsExamEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamEntity::getExamStatus, EXAM_SUBMITTED)
                .in(TmsExamEntity::getGradeStatus, GRADE_PENDING, GRADE_GRADED)
                .eq(StringUtil.isNotEmpty(paperId), TmsExamEntity::getPaperId, paperId)
                .orderByDesc(TmsExamEntity::getSubmitTime);
        return examMapper.selectList(qw);
    }
    private Map<String, List<TmsExamEntity>> groupByPaper(List<TmsExamEntity> exams) {
        Map<String, List<TmsExamEntity>> map = new LinkedHashMap<>();
        for (TmsExamEntity exam : exams) {
            if (StringUtil.isEmpty(exam.getPaperId())) {
                continue;
            }
            map.computeIfAbsent(exam.getPaperId(), k -> new ArrayList<>()).add(exam);
        }
        return map;
    }
    private Map<String, TmsPaperEntity> loadPapers(List<String> ids) {
        if (ids == null || ids.isEmpty()) {
            return Collections.emptyMap();
        }
        List<TmsPaperEntity> list = paperMapper.selectBatchIds(ids);
        Map<String, TmsPaperEntity> map = new HashMap<>();
        for (TmsPaperEntity paper : list) {
            map.put(paper.getId(), paper);
        }
        return map;
    }
    private TmsExamGradeSessionVO toSession(String paperId, TmsPaperEntity paper, List<TmsExamEntity> exams) {
        TmsExamGradeSessionVO vo = new TmsExamGradeSessionVO();
        vo.setId(paperId);
        vo.setPaperId(paperId);
        String paperName = paper != null && StringUtil.isNotEmpty(paper.getPaperName())
                ? paper.getPaperName()
                : (exams.isEmpty() ? paperId : exams.get(0).getPaperName());
        vo.setPaperName(paperName);
        vo.setTaskSubject(paperName);
        String paperNo = paper != null && StringUtil.isNotEmpty(paper.getPaperNo()) ? paper.getPaperNo() : paperId;
        vo.setTaskNo(paperNo);
        vo.setDurationMin(paper != null && paper.getDurationMin() != null
                ? paper.getDurationMin()
                : (exams.isEmpty() ? null : exams.get(0).getDurationMin()));
        vo.setTotalScore(paper != null && paper.getTotalScore() != null
                ? paper.getTotalScore()
                : resolvePaperTotal(exams.isEmpty() ? null : exams.get(0)));
        vo.setPassScore(paper != null && paper.getPassScore() != null
                ? paper.getPassScore()
                : (exams.isEmpty() ? null : exams.get(0).getPassScore()));
        Date start = paper != null ? paper.getExamStart() : null;
        Date end = paper != null ? paper.getExamEnd() : null;
        if (start == null) {
            start = exams.stream().map(TmsExamEntity::getStartTime).filter(Objects::nonNull).min(Date::compareTo).orElse(null);
        }
        if (end == null) {
            end = exams.stream().map(TmsExamEntity::getSubmitTime).filter(Objects::nonNull).max(Date::compareTo).orElse(null);
        }
        vo.setExamStart(fmt(start));
        vo.setExamEnd(fmt(end));
        vo.setSubmittedCount(exams.size());
        vo.setPendingCount((int) exams.stream().filter(x -> GRADE_PENDING.equals(x.getGradeStatus())).count());
        return vo;
    }
    private BigDecimal resolvePaperTotal(TmsExamEntity exam) {
        if (exam == null) {
            return BigDecimal.ZERO;
        }
        if (exam.getPaperId() != null) {
            TmsPaperEntity paper = paperMapper.selectById(exam.getPaperId());
            if (paper != null && paper.getTotalScore() != null) {
                return paper.getTotalScore();
            }
        }
        return nvl(exam.getTotalScore());
    }
    private List<TmsExamItemEntity> loadItems(String examId) {
        LambdaQueryWrapper<TmsExamItemEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamItemEntity::getForeignId, examId).orderByAsc(TmsExamItemEntity::getSortNo);
        return examItemMapper.selectList(qw);
    }
    private TmsExamGradeItemVO toItemVo(TmsExamItemEntity item) {
        TmsExamGradeItemVO vo = new TmsExamGradeItemVO();
        vo.setId(item.getId());
        vo.setQuestionType(item.getQuestionType());
        vo.setStem(item.getStem());
        vo.setOptions(parseOptions(item.getOptionsJson()));
        vo.setCorrectAnswer(item.getCorrectAnswer());
        vo.setUserAnswer(item.getUserAnswer());
        vo.setScore(item.getScore());
        vo.setGotScore(item.getGotScore());
        vo.setIsSubjective(item.getIsSubjective());
        vo.setAnalysis(item.getAnalysis());
        return vo;
    }
    private List<TmsExamGradeOptionVO> parseOptions(String json) {
        if (StringUtil.isEmpty(json)) {
            return Collections.emptyList();
        }
        try {
            List<OptSnap> list = OBJECT_MAPPER.readValue(json, new TypeReference<List<OptSnap>>() {});
            List<TmsExamGradeOptionVO> options = new ArrayList<>();
            for (OptSnap snap : list) {
                TmsExamGradeOptionVO opt = new TmsExamGradeOptionVO();
                opt.setOptionLabel(snap.getOptionLabel());
                opt.setOptionContent(snap.getOptionContent());
                options.add(opt);
            }
            return options;
        } catch (Exception e) {
            return Collections.emptyList();
        }
    }
    private Map<String, String> loadUserNames(List<String> userIds) {
        Map<String, String> map = new HashMap<>();
        List<String> ids = userIds == null ? Collections.emptyList()
                : userIds.stream().filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList());
        if (ids.isEmpty()) {
            return map;
        }
        try {
            List<UserEntity> users = userApi.getUserName(ids);
            if (users != null) {
                for (UserEntity user : users) {
                    if (user == null || StringUtil.isEmpty(user.getId())) {
                        continue;
                    }
                    map.put(user.getId(), StringUtil.isNotEmpty(user.getRealName()) ? user.getRealName() : user.getId());
                }
            }
        } catch (Exception ignored) {
            // 用户服务不可用时回退 userId
        }
        return map;
    }
    private String passFlag(BigDecimal score, BigDecimal passScore) {
        if (passScore == null) {
            return null;
        }
        return nvl(score).compareTo(passScore) >= 0 ? "1" : "0";
    }
    private String fmt(Date date) {
        return date == null ? null : DateUtil.dateToString(date, FMT);
    }
    private BigDecimal nvl(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private String trim(String value) {
        return value == null || "null".equalsIgnoreCase(value) ? "" : value.trim();
    }
    private boolean contains(String text, String kw) {
        return text != null && text.toLowerCase().contains(kw);
    }
    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    private static class OptSnap {
        private String optionLabel;
        private String optionContent;
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamScoreServiceImpl.java
New file
@@ -0,0 +1,376 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jnpf.exception.DataException;
import jnpf.permission.UserApi;
import jnpf.permission.entity.UserEntity;
import jnpf.tmsEntity.TmsExamEntity;
import jnpf.tmsEntity.TmsExamItemEntity;
import jnpf.tmsEntity.TmsPaperEntity;
import jnpf.tmsEntity.examscore.TmsExamScoreDetailVO;
import jnpf.tmsEntity.examscore.TmsExamScoreOptionVO;
import jnpf.tmsEntity.examscore.TmsExamScorePaperItemVO;
import jnpf.tmsEntity.examscore.TmsExamScorePaperVO;
import jnpf.tmsEntity.examscore.TmsExamScoreQuery;
import jnpf.tmsEntity.examscore.TmsExamScoreRowVO;
import jnpf.tmsEntity.examscore.TmsExamScoreSummaryVO;
import jnpf.tmsMapper.TmsExamItemMapper;
import jnpf.tmsMapper.TmsExamMapper;
import jnpf.tmsMapper.TmsPaperMapper;
import jnpf.tmsService.TmsExamScoreService;
import jnpf.util.DateUtil;
import jnpf.util.StringUtil;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class TmsExamScoreServiceImpl implements TmsExamScoreService {
    private static final String EXAM_SUBMITTED = "submitted";
    private static final String STATUS_INVALID = "invalid";
    private static final String FMT = "yyyy-MM-dd HH:mm:ss";
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private final TmsExamMapper examMapper;
    private final TmsExamItemMapper examItemMapper;
    private final TmsPaperMapper paperMapper;
    private final UserApi userApi;
    @Override
    public List<TmsExamScoreSummaryVO> getList(TmsExamScoreQuery query) {
        List<TmsExamEntity> exams = loadSubmittedExams();
        Map<String, List<TmsExamEntity>> byPaper = groupByPaper(exams);
        Map<String, TmsPaperEntity> papers = loadPapers(new ArrayList<>(byPaper.keySet()));
        List<TmsExamScoreSummaryVO> all = new ArrayList<>();
        for (Map.Entry<String, List<TmsExamEntity>> entry : byPaper.entrySet()) {
            TmsPaperEntity paper = papers.get(entry.getKey());
            all.add(toSummary(entry.getKey(), paper, entry.getValue()));
        }
        all.sort(Comparator.comparing(TmsExamScoreSummaryVO::getPaperName, Comparator.nullsLast(String::compareTo)).reversed());
        String keyword = trim(query.getKeyword());
        String taskNo = trim(query.getTaskNo());
        if (StringUtil.isNotEmpty(keyword)) {
            String kw = keyword.toLowerCase();
            all = all.stream()
                    .filter(x -> contains(x.getTaskSubject(), kw)
                            || contains(x.getPaperName(), kw)
                            || contains(x.getTaskNo(), kw))
                    .collect(Collectors.toList());
        }
        if (StringUtil.isNotEmpty(taskNo)) {
            String tn = taskNo.toLowerCase();
            all = all.stream().filter(x -> contains(x.getTaskNo(), tn)).collect(Collectors.toList());
        }
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        int from = (int) ((current - 1) * size);
        int to = Math.min(from + (int) size, all.size());
        List<TmsExamScoreSummaryVO> page = from >= all.size() ? new ArrayList<>() : new ArrayList<>(all.subList(from, to));
        query.setData(page, all.size());
        return page;
    }
    @Override
    public TmsExamScoreDetailVO getDetail(String summaryId) {
        if (StringUtil.isEmpty(summaryId)) {
            throw new DataException("成绩汇总不存在");
        }
        List<TmsExamEntity> exams = loadSubmittedByPaper(summaryId);
        if (exams.isEmpty()) {
            throw new DataException("成绩汇总不存在");
        }
        TmsPaperEntity paper = paperMapper.selectById(summaryId);
        TmsExamScoreSummaryVO summary = toSummary(summaryId, paper, exams);
        Map<String, String> names = loadUserNames(exams.stream().map(TmsExamEntity::getUserId).collect(Collectors.toList()));
        List<TmsExamScoreRowVO> rows = new ArrayList<>();
        for (TmsExamEntity exam : exams) {
            TmsExamScoreRowVO row = new TmsExamScoreRowVO();
            row.setId(exam.getId());
            row.setSummaryId(summaryId);
            row.setTaskSubject(summary.getTaskSubject());
            row.setTaskNo(summary.getTaskNo());
            row.setUserId(exam.getUserId());
            row.setUserName(names.getOrDefault(exam.getUserId(), exam.getUserId()));
            row.setExamStart(fmt(exam.getStartTime()));
            row.setExamEnd(fmt(exam.getSubmitTime()));
            row.setScore(exam.getTotalScore());
            row.setPassScore(exam.getPassScore() != null ? exam.getPassScore() : summary.getPassScore());
            row.setPassFlag(exam.getPassFlag());
            rows.add(row);
        }
        rows.sort(Comparator.comparing(TmsExamScoreRowVO::getExamEnd, Comparator.nullsLast(String::compareTo)).reversed());
        TmsExamScoreDetailVO detail = new TmsExamScoreDetailVO();
        BeanUtils.copyProperties(summary, detail);
        detail.setRows(rows);
        return detail;
    }
    @Override
    public TmsExamScorePaperVO getPaperView(String examId) {
        TmsExamEntity exam = examMapper.selectById(examId);
        if (exam == null || !EXAM_SUBMITTED.equals(exam.getExamStatus())) {
            throw new DataException("答卷不存在或尚未交卷");
        }
        TmsPaperEntity paper = paperMapper.selectById(exam.getPaperId());
        Map<String, String> names = loadUserNames(Collections.singletonList(exam.getUserId()));
        LambdaQueryWrapper<TmsExamItemEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamItemEntity::getForeignId, examId);
        qw.orderByAsc(TmsExamItemEntity::getSortNo);
        List<TmsExamItemEntity> items = examItemMapper.selectList(qw);
        BigDecimal paperTotal = BigDecimal.ZERO;
        List<TmsExamScorePaperItemVO> itemVos = new ArrayList<>();
        for (TmsExamItemEntity item : items) {
            paperTotal = paperTotal.add(nvl(item.getScore()));
            TmsExamScorePaperItemVO vo = new TmsExamScorePaperItemVO();
            vo.setId(item.getId());
            vo.setQuestionType(item.getQuestionType());
            vo.setStem(item.getStem());
            vo.setOptions(toOptions(item.getOptionsJson()));
            vo.setCorrectAnswer(item.getCorrectAnswer());
            vo.setUserAnswer(item.getUserAnswer());
            vo.setScore(item.getScore());
            vo.setGotScore(item.getGotScore());
            vo.setIsSubjective(item.getIsSubjective());
            if ("1".equals(item.getIsSubjective())) {
                vo.setIsCorrect(null);
            } else if (item.getScore() != null && item.getScore().compareTo(BigDecimal.ZERO) > 0) {
                vo.setIsCorrect(nvl(item.getGotScore()).compareTo(item.getScore()) >= 0);
            } else {
                vo.setIsCorrect(Objects.equals(normalize(item.getCorrectAnswer()), normalize(item.getUserAnswer())));
            }
            itemVos.add(vo);
        }
        TmsExamScorePaperVO vo = new TmsExamScorePaperVO();
        vo.setExamId(exam.getId());
        vo.setSummaryId(exam.getPaperId());
        vo.setTaskSubject(paper != null ? paper.getPaperName() : exam.getPaperName());
        vo.setTaskNo(paper != null && StringUtil.isNotEmpty(paper.getPaperNo()) ? paper.getPaperNo() : exam.getPaperId());
        vo.setPaperName(exam.getPaperName());
        vo.setUserId(exam.getUserId());
        vo.setUserName(names.getOrDefault(exam.getUserId(), exam.getUserId()));
        vo.setExamStart(fmt(exam.getStartTime()));
        vo.setExamEnd(fmt(exam.getSubmitTime()));
        vo.setScore(exam.getTotalScore());
        vo.setPassScore(exam.getPassScore());
        vo.setTotalScore(paper != null && paper.getTotalScore() != null ? paper.getTotalScore() : paperTotal);
        vo.setPassFlag(exam.getPassFlag());
        vo.setItems(itemVos);
        return vo;
    }
    @Override
    public List<TmsExamScoreRowVO> exportRows(List<String> summaryIds) {
        if (summaryIds == null || summaryIds.isEmpty()) {
            throw new DataException("请选择要导出的考试");
        }
        List<TmsExamScoreRowVO> rows = new ArrayList<>();
        for (String id : summaryIds) {
            if (StringUtil.isEmpty(id)) {
                continue;
            }
            List<TmsExamEntity> exams = loadSubmittedByPaper(id);
            if (exams.isEmpty()) {
                continue;
            }
            TmsExamScoreDetailVO detail = getDetail(id);
            if (detail.getRows() != null) {
                rows.addAll(detail.getRows());
            }
        }
        if (rows.isEmpty()) {
            throw new DataException("所选试卷暂无已交卷成绩");
        }
        return rows;
    }
    private TmsExamScoreSummaryVO toSummary(String paperId, TmsPaperEntity paper, List<TmsExamEntity> exams) {
        TmsExamScoreSummaryVO vo = new TmsExamScoreSummaryVO();
        vo.setId(paperId);
        vo.setPaperId(paperId);
        String paperName = paper != null ? paper.getPaperName() : (exams.isEmpty() ? paperId : exams.get(0).getPaperName());
        vo.setPaperName(paperName);
        vo.setTaskSubject(paperName);
        String paperNo = paper != null && StringUtil.isNotEmpty(paper.getPaperNo()) ? paper.getPaperNo() : paperId;
        vo.setTaskNo(paperNo);
        vo.setPaperInvalid(paper != null && STATUS_INVALID.equals(paper.getBizStatus()));
        BigDecimal passScore = paper != null && paper.getPassScore() != null
                ? paper.getPassScore()
                : exams.stream().map(TmsExamEntity::getPassScore).filter(Objects::nonNull).findFirst().orElse(BigDecimal.ZERO);
        BigDecimal totalScore = paper != null && paper.getTotalScore() != null
                ? paper.getTotalScore()
                : BigDecimal.ZERO;
        vo.setPassScore(passScore);
        vo.setTotalScore(totalScore);
        int fail = 0;
        BigDecimal max = null;
        BigDecimal min = null;
        BigDecimal sum = BigDecimal.ZERO;
        int scored = 0;
        for (TmsExamEntity exam : exams) {
            if ("0".equals(exam.getPassFlag())) {
                fail++;
            }
            BigDecimal score = exam.getTotalScore();
            if (score == null) {
                continue;
            }
            scored++;
            sum = sum.add(score);
            if (max == null || score.compareTo(max) > 0) {
                max = score;
            }
            if (min == null || score.compareTo(min) < 0) {
                min = score;
            }
        }
        vo.setParticipantCount(exams.size());
        vo.setFailCount(fail);
        vo.setMaxScore(max == null ? BigDecimal.ZERO : max);
        vo.setMinScore(min == null ? BigDecimal.ZERO : min);
        vo.setAvgScore(scored == 0
                ? BigDecimal.ZERO
                : sum.divide(BigDecimal.valueOf(scored), 1, RoundingMode.HALF_UP));
        return vo;
    }
    private List<TmsExamEntity> loadSubmittedExams() {
        LambdaQueryWrapper<TmsExamEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamEntity::getExamStatus, EXAM_SUBMITTED);
        qw.isNotNull(TmsExamEntity::getPaperId);
        qw.orderByDesc(TmsExamEntity::getSubmitTime);
        return examMapper.selectList(qw);
    }
    private List<TmsExamEntity> loadSubmittedByPaper(String paperId) {
        LambdaQueryWrapper<TmsExamEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamEntity::getExamStatus, EXAM_SUBMITTED);
        qw.eq(TmsExamEntity::getPaperId, paperId);
        qw.orderByDesc(TmsExamEntity::getSubmitTime);
        return examMapper.selectList(qw);
    }
    private Map<String, List<TmsExamEntity>> groupByPaper(List<TmsExamEntity> exams) {
        Map<String, List<TmsExamEntity>> map = new LinkedHashMap<>();
        for (TmsExamEntity exam : exams) {
            if (StringUtil.isEmpty(exam.getPaperId())) {
                continue;
            }
            map.computeIfAbsent(exam.getPaperId(), k -> new ArrayList<>()).add(exam);
        }
        return map;
    }
    private Map<String, TmsPaperEntity> loadPapers(List<String> paperIds) {
        if (paperIds == null || paperIds.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<String, TmsPaperEntity> map = new HashMap<>();
        for (TmsPaperEntity paper : paperMapper.selectBatchIds(paperIds)) {
            map.put(paper.getId(), paper);
        }
        return map;
    }
    private Map<String, String> loadUserNames(List<String> userIds) {
        if (userIds == null || userIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<String> ids = userIds.stream().filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList());
        if (ids.isEmpty()) {
            return Collections.emptyMap();
        }
        try {
            List<UserEntity> users = userApi.getUserName(ids);
            if (users == null || users.isEmpty()) {
                return Collections.emptyMap();
            }
            return users.stream()
                    .filter(Objects::nonNull)
                    .collect(Collectors.toMap(UserEntity::getId, UserEntity::getRealName, (a, b) -> a));
        } catch (Exception ex) {
            return Collections.emptyMap();
        }
    }
    private List<TmsExamScoreOptionVO> toOptions(String json) {
        if (StringUtil.isEmpty(json)) {
            return Collections.emptyList();
        }
        try {
            List<StoredOption> stored = OBJECT_MAPPER.readValue(json, new TypeReference<List<StoredOption>>() {
            });
            List<TmsExamScoreOptionVO> list = new ArrayList<>();
            for (StoredOption opt : stored) {
                TmsExamScoreOptionVO vo = new TmsExamScoreOptionVO();
                vo.setOptionLabel(opt.getOptionLabel());
                vo.setOptionContent(opt.getOptionContent());
                list.add(vo);
            }
            return list;
        } catch (Exception e) {
            return Collections.emptyList();
        }
    }
    private String normalize(String text) {
        if (StringUtil.isEmpty(text)) {
            return "";
        }
        return text.trim();
    }
    private String trim(String text) {
        if (StringUtil.isEmpty(text) || "null".equalsIgnoreCase(text)) {
            return null;
        }
        return text.trim();
    }
    private boolean contains(String text, String kw) {
        return text != null && text.toLowerCase().contains(kw);
    }
    private String fmt(java.util.Date date) {
        return date == null ? null : DateUtil.dateToString(date, FMT);
    }
    private BigDecimal nvl(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    private static class StoredOption {
        private String optionLabel;
        private String optionContent;
        private String isCorrect;
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsOnlineExamServiceImpl.java
New file
@@ -0,0 +1,1030 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import jnpf.exception.DataException;
import jnpf.tmsEntity.TmsExamEntity;
import jnpf.tmsEntity.TmsExamItemEntity;
import jnpf.tmsEntity.TmsPaperEntity;
import jnpf.tmsEntity.TmsPaperQuestionEntity;
import jnpf.tmsEntity.TmsPaperSectionEntity;
import jnpf.tmsEntity.TmsPersonTaskEntity;
import jnpf.tmsEntity.TmsQuestionEntity;
import jnpf.tmsEntity.TmsQuestionOptionEntity;
import jnpf.tmsEntity.TmsTaskEntity;
import jnpf.tmsEntity.exam.TmsOnlineExamAttemptVO;
import jnpf.tmsEntity.exam.TmsOnlineExamDetailVO;
import jnpf.tmsEntity.exam.TmsOnlineExamGradeItemVO;
import jnpf.tmsEntity.exam.TmsOnlineExamListVO;
import jnpf.tmsEntity.exam.TmsOnlineExamOptionVO;
import jnpf.tmsEntity.exam.TmsOnlineExamPaperVO;
import jnpf.tmsEntity.exam.TmsOnlineExamQuery;
import jnpf.tmsEntity.exam.TmsOnlineExamQuestionVO;
import jnpf.tmsEntity.exam.TmsOnlineExamSubmitForm;
import jnpf.tmsEntity.exam.TmsOnlineExamSubmitResultVO;
import jnpf.tmsMapper.TmsExamItemMapper;
import jnpf.tmsMapper.TmsExamMapper;
import jnpf.tmsMapper.TmsPaperMapper;
import jnpf.tmsMapper.TmsPaperQuestionMapper;
import jnpf.tmsMapper.TmsPaperSectionMapper;
import jnpf.tmsMapper.TmsPersonTaskMapper;
import jnpf.tmsMapper.TmsQuestionMapper;
import jnpf.tmsMapper.TmsQuestionOptionMapper;
import jnpf.tmsMapper.TmsTaskMapper;
import jnpf.tmsService.TmsOnlineExamService;
import jnpf.util.DateUtil;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class TmsOnlineExamServiceImpl implements TmsOnlineExamService {
    private static final String STATUS_OPEN = "open";
    private static final String EXAM_DOING = "doing";
    private static final String EXAM_SUBMITTED = "submitted";
    private static final String NOT_STARTED = "notStarted";
    private static final String GRADE_AUTO = "auto";
    private static final String GRADE_PENDING = "pending";
    private static final String SORT_RANDOM = "random";
    private static final String TYPE_ESSAY = "essay";
    private static final String TYPE_BLANK = "blank";
    private static final String TYPE_MULTI = "multi";
    private static final String FMT = "yyyy-MM-dd HH:mm:ss";
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private final TmsPaperMapper paperMapper;
    private final TmsPaperSectionMapper sectionMapper;
    private final TmsPaperQuestionMapper paperQuestionMapper;
    private final TmsQuestionMapper questionMapper;
    private final TmsQuestionOptionMapper optionMapper;
    private final TmsExamMapper examMapper;
    private final TmsExamItemMapper examItemMapper;
    private final TmsTaskMapper taskMapper;
    private final TmsPersonTaskMapper personTaskMapper;
    @Override
    public List<TmsOnlineExamListVO> getMyPapers(TmsOnlineExamQuery query) {
        String userId = UserProvider.getLoginUserId();
        LambdaQueryWrapper<TmsPaperEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsPaperEntity::getBizStatus, STATUS_OPEN);
        if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) {
            qw.like(TmsPaperEntity::getPaperName, query.getKeyword().trim());
        }
        qw.orderByDesc(TmsPaperEntity::getCreatorTime);
        List<TmsPaperEntity> papers = this.paperMapper.selectList(qw);
        Map<String, TmsExamEntity> examMap = this.loadLatestExams(userId, papers);
        ArrayList<TmsOnlineExamListVO> all = new ArrayList<TmsOnlineExamListVO>();
        for (TmsPaperEntity paper : papers) {
            TmsExamEntity exam = examMap.get(paper.getId());
            String status = this.statusOf(exam);
            if (StringUtil.isNotEmpty(query.getStatus()) && !"null".equalsIgnoreCase(query.getStatus()) && !query.getStatus().equals(status)) continue;
            all.add(this.toListVo(paper, exam, status));
        }
        long current = query.getCurrentPage() > 0L ? query.getCurrentPage() : 1L;
        long size = query.getPageSize() > 0L ? query.getPageSize() : 20L;
        int from = (int)((current - 1L) * size);
        int to = Math.min(from + (int)size, all.size());
        ArrayList<TmsOnlineExamListVO> page = from >= all.size() ? new ArrayList<TmsOnlineExamListVO>() : new ArrayList(all.subList(from, to));
        query.setData(page, (long)all.size());
        return page;
    }
    @Override
    public TmsOnlineExamDetailVO getDetail(String paperId) {
        if (StringUtil.isEmpty(paperId)) {
            throw new DataException("请选择试卷");
        }
        TmsPaperEntity paper = this.paperMapper.selectById(paperId);
        if (paper == null) {
            throw new DataException("试卷不存在");
        }
        TmsExamEntity exam = this.latestExam(UserProvider.getLoginUserId(), paperId);
        if (exam == null && !STATUS_OPEN.equals(paper.getBizStatus())) {
            throw new DataException("试卷未开放");
        }
        TmsOnlineExamDetailVO vo = new TmsOnlineExamDetailVO();
        vo.setId(paper.getId());
        vo.setPaperId(paper.getId());
        vo.setPaperName(paper.getPaperName());
        vo.setStatus(this.statusOf(exam));
        vo.setExamTimeText(this.timeText(paper.getExamStart(), paper.getExamEnd()));
        vo.setTotalScore(paper.getTotalScore());
        vo.setPassScore(paper.getPassScore());
        vo.setDurationMin(paper.getDurationMin());
        if (exam != null) {
            vo.setExamId(exam.getId());
            vo.setGotScore(exam.getTotalScore());
            vo.setStartTime(this.fmt(exam.getStartTime()));
            vo.setSubmitTime(this.fmt(exam.getSubmitTime()));
            vo.setPassFlag(exam.getPassFlag());
            vo.setGradeStatus(exam.getGradeStatus());
            if (EXAM_SUBMITTED.equals(exam.getExamStatus())) {
                vo.setQuestions(this.toReviewQuestions(this.loadItems(exam.getId())));
                if (vo.getTotalScore() == null) {
                    BigDecimal total = BigDecimal.ZERO;
                    for (TmsOnlineExamQuestionVO q : vo.getQuestions()) {
                        total = total.add(this.nvl(q.getScore()));
                    }
                    vo.setTotalScore(total);
                }
            }
        }
        this.fillRetakeMeta(vo, paper.getId(), exam, UserProvider.getLoginUserId());
        vo.setAttempts(this.listAttempts(UserProvider.getLoginUserId(), paper.getId()));
        return vo;
    }
    @Override
    public TmsOnlineExamDetailVO getExamReview(String examId) {
        if (StringUtil.isEmpty(examId)) {
            throw new DataException("请选择答卷");
        }
        TmsExamEntity exam = this.examMapper.selectById(examId);
        if (exam == null) {
            throw new DataException("答卷不存在");
        }
        if (!Objects.equals(UserProvider.getLoginUserId(), exam.getUserId())) {
            throw new DataException("无权查看该答卷");
        }
        if (!EXAM_SUBMITTED.equals(exam.getExamStatus())) {
            throw new DataException("交卷后才能进行考试回顾");
        }
        TmsPaperEntity paper = this.paperMapper.selectById((exam.getPaperId()));
        if (paper == null) {
            throw new DataException("试卷不存在");
        }
        TmsOnlineExamDetailVO vo = new TmsOnlineExamDetailVO();
        vo.setId(paper.getId());
        vo.setPaperId(paper.getId());
        vo.setPaperName(paper.getPaperName());
        vo.setStatus(this.statusOf(exam));
        vo.setExamTimeText(this.timeText(paper.getExamStart(), paper.getExamEnd()));
        vo.setTotalScore(paper.getTotalScore());
        vo.setPassScore(paper.getPassScore() != null ? paper.getPassScore() : exam.getPassScore());
        vo.setDurationMin(paper.getDurationMin());
        vo.setExamId(exam.getId());
        vo.setGotScore(exam.getTotalScore());
        vo.setStartTime(this.fmt(exam.getStartTime()));
        vo.setSubmitTime(this.fmt(exam.getSubmitTime()));
        vo.setPassFlag(exam.getPassFlag());
        vo.setGradeStatus(exam.getGradeStatus());
        vo.setAttemptNo(Integer.valueOf(exam.getAttemptNo() == null ? 1 : exam.getAttemptNo()));
        vo.setAttemptLabel(this.attemptLabel(vo.getAttemptNo()));
        vo.setQuestions(this.toReviewQuestions(this.loadItems(exam.getId())));
        if (vo.getTotalScore() == null) {
            BigDecimal total = BigDecimal.ZERO;
            for (TmsOnlineExamQuestionVO q : vo.getQuestions()) {
                total = total.add(this.nvl(q.getScore()));
            }
            vo.setTotalScore(total);
        }
        return vo;
    }
    private List<TmsOnlineExamAttemptVO> listAttempts(String userId, String paperId) {
        if (StringUtil.isEmpty(userId) || StringUtil.isEmpty(paperId)) {
            return Collections.emptyList();
        }
        LambdaQueryWrapper<TmsExamEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamEntity::getUserId, userId);
        qw.eq(TmsExamEntity::getPaperId, paperId);
        qw.orderByAsc(TmsExamEntity::getAttemptNo);
        qw.orderByAsc(TmsExamEntity::getCreatorTime);
        List<TmsExamEntity> exams = this.examMapper.selectList(qw);
        ArrayList<TmsOnlineExamAttemptVO> list = new ArrayList<TmsOnlineExamAttemptVO>(exams.size());
        for (TmsExamEntity exam : exams) {
            TmsOnlineExamAttemptVO row = new TmsOnlineExamAttemptVO();
            row.setExamId(exam.getId());
            int attemptNo = exam.getAttemptNo() == null ? 1 : exam.getAttemptNo();
            row.setAttemptNo(Integer.valueOf(attemptNo));
            row.setAttemptLabel(this.attemptLabel(attemptNo));
            row.setStatus(this.statusOf(exam));
            row.setGradeStatus(exam.getGradeStatus());
            row.setStartTime(this.fmt(exam.getStartTime()));
            row.setSubmitTime(this.fmt(exam.getSubmitTime()));
            row.setGotScore(exam.getTotalScore());
            row.setPassFlag(exam.getPassFlag());
            list.add(row);
        }
        return list;
    }
    @Override
    @Transactional(rollbackFor = Exception.class, noRollbackFor = ExamTimedOutException.class)
    public TmsOnlineExamPaperVO start(String paperId) {
        if (StringUtil.isEmpty(paperId)) {
            throw new DataException("请选择试卷");
        }
        String userId = UserProvider.getLoginUserId();
        TmsExamEntity doing = this.findDoing(userId, paperId);
        if (doing != null) {
            if (this.isTimedOut(doing)) {
                // 超时须先交卷再返回;抛异常会导致事务回滚,答卷一直停在考试中
                this.submitInternal(doing, null);
                TmsOnlineExamPaperVO vo = this.toPaperVo(doing, this.loadItems(doing.getId()));
                vo.setAutoSubmitted(Boolean.TRUE);
                vo.setRemainSeconds(Integer.valueOf(0));
                return vo;
            }
            return this.toPaperVo(doing, this.loadItems(doing.getId()));
        }
        TmsPaperEntity paper = this.requireOpenPaper(paperId);
        this.assertExamWindow(paper);
        RetakeContext retake = this.resolveRetake(userId, paperId);
        if (!retake.allowStart) {
            throw new DataException(retake.message);
        }
        TmsExamEntity exam = this.createExam(paper, userId, retake);
        return this.toPaperVo(exam, this.loadItems(exam.getId()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class, noRollbackFor = ExamTimedOutException.class)
    public void saveAnswers(String examId, TmsOnlineExamSubmitForm form) {
        TmsExamEntity exam = this.requireDoingExam(examId);
        if (this.isTimedOut(exam)) {
            this.submitInternal(exam, form == null ? null : form.getAnswers());
            throw new ExamTimedOutException();
        }
        this.writeAnswers(exam.getId(), form == null ? null : form.getAnswers());
    }
    @Override
    @Transactional(rollbackFor={Exception.class})
    public TmsOnlineExamSubmitResultVO submit(String examId, TmsOnlineExamSubmitForm form) {
        TmsExamEntity exam = this.requireDoingExam(examId);
        return this.submitInternal(exam, form == null ? null : form.getAnswers());
    }
    private TmsOnlineExamSubmitResultVO submitInternal(TmsExamEntity exam, Map<String, Object> answers) {
        List<TmsExamItemEntity> items = this.loadItems(exam.getId());
        HashMap<String, String> normalized = new HashMap<String, String>();
        for (TmsExamItemEntity item : items) {
            if (answers != null && answers.containsKey(item.getId())) {
                normalized.put(item.getId(), this.normalizeAnswer(answers.get(item.getId()), item.getQuestionType()));
                continue;
            }
            normalized.put(item.getId(), item.getUserAnswer() == null ? "" : item.getUserAnswer());
        }
        BigDecimal objective = BigDecimal.ZERO;
        boolean pending = false;
        ArrayList<TmsOnlineExamGradeItemVO> grades = new ArrayList<TmsOnlineExamGradeItemVO>();
        Date now = new Date();
        for (TmsExamItemEntity item : items) {
            String userAnswer = normalized.getOrDefault(item.getId(), "");
            item.setUserAnswer(userAnswer);
            TmsOnlineExamGradeItemVO grade = new TmsOnlineExamGradeItemVO();
            grade.setId(item.getId());
            grade.setCorrectAnswer(item.getCorrectAnswer());
            boolean subjective = "1".equals(item.getIsSubjective());
            grade.setSubjective(Boolean.valueOf(subjective));
            if (subjective) {
                pending = true;
                item.setGotScore(null);
                grade.setGotScore(null);
                grade.setRight(null);
            } else {
                boolean right = this.answerRight(item, userAnswer);
                BigDecimal got = right ? this.nvl(item.getScore()) : BigDecimal.ZERO;
                item.setGotScore(got);
                objective = objective.add(got);
                grade.setGotScore(got);
                grade.setRight(Boolean.valueOf(right));
            }
            item.setLastModifyTime(now);
            this.examItemMapper.updateById(item);
            grades.add(grade);
        }
        objective = objective.setScale(1, RoundingMode.HALF_UP);
        exam.setObjectiveScore(objective);
        exam.setSubmitTime(now);
        exam.setExamStatus(EXAM_SUBMITTED);
        exam.setLastModifyTime(now);
        if (pending) {
            exam.setGradeStatus(GRADE_PENDING);
            exam.setSubjectiveScore(null);
            exam.setTotalScore(objective);
            exam.setPassFlag(null);
        } else {
            exam.setGradeStatus(GRADE_AUTO);
            exam.setSubjectiveScore(BigDecimal.ZERO);
            exam.setTotalScore(objective);
            exam.setPassFlag(this.passFlag(objective, exam.getPassScore()));
        }
        this.examMapper.updateById(exam);
        TmsOnlineExamSubmitResultVO result = new TmsOnlineExamSubmitResultVO();
        result.setObjectiveScore(exam.getObjectiveScore());
        result.setTotalScore(exam.getTotalScore());
        result.setPassFlag(exam.getPassFlag());
        result.setGradeStatus(exam.getGradeStatus());
        result.setPendingGrade(Boolean.valueOf(pending));
        result.setItems(grades);
        return result;
    }
    private TmsExamEntity createExam(TmsPaperEntity paper, String userId, RetakeContext retake) {
        List<QuestionSnap> snaps = this.loadPaperQuestions(paper.getId());
        if (snaps.isEmpty()) {
            throw new DataException("试卷没有试题,无法开考");
        }
        if (SORT_RANDOM.equals(paper.getSortMode())) {
            Collections.shuffle(snaps);
        }
        Date now = new Date();
        TmsExamEntity exam = new TmsExamEntity();
        exam.setId(RandomUtil.uuId());
        exam.setTaskId(retake == null ? null : retake.taskId);
        exam.setPersonTaskId(retake == null ? null : retake.personTaskId);
        exam.setPaperId(paper.getId());
        exam.setPaperName(paper.getPaperName());
        exam.setUserId(userId);
        exam.setAttemptNo(Integer.valueOf(retake == null || retake.nextAttemptNo == null ? 1 : retake.nextAttemptNo));
        exam.setStartTime(now);
        exam.setDurationMin(paper.getDurationMin());
        BigDecimal passScore = paper.getPassScore();
        if (retake != null && retake.passScore != null) {
            passScore = retake.passScore;
        }
        exam.setPassScore(passScore);
        exam.setGradeStatus(GRADE_AUTO);
        exam.setExamStatus(EXAM_DOING);
        exam.setCreatorUserId(userId);
        exam.setCreatorTime(now);
        this.examMapper.insert(exam);
        int sort = 1;
        for (QuestionSnap snap : snaps) {
            TmsExamItemEntity item = new TmsExamItemEntity();
            item.setId(RandomUtil.uuId());
            item.setForeignId(exam.getId());
            item.setQuestionId(snap.question.getId());
            item.setQuestionType(snap.question.getQuestionType());
            item.setStem(snap.question.getStem());
            item.setOptionsJson(this.toJson(snap.options));
            item.setCorrectAnswer(this.correctOf(snap));
            item.setScore(snap.score);
            item.setIsSubjective(TYPE_ESSAY.equals(snap.question.getQuestionType()) ? "1" : "0");
            item.setAnalysis(snap.question.getAnalysis());
            item.setSortNo(Integer.valueOf(sort++));
            item.setCreatorUserId(userId);
            item.setCreatorTime(now);
            this.examItemMapper.insert(item);
        }
        return exam;
    }
    private List<QuestionSnap> loadPaperQuestions(String paperId) {
        LambdaQueryWrapper<TmsPaperSectionEntity> secQw = new LambdaQueryWrapper<>();
        secQw.eq(TmsPaperSectionEntity::getForeignId, paperId);
        secQw.orderByAsc(TmsPaperSectionEntity::getSortNo);
        List<TmsPaperSectionEntity> sections = this.sectionMapper.selectList(secQw);
        LambdaQueryWrapper<TmsPaperQuestionEntity> qQw = new LambdaQueryWrapper<>();
        qQw.eq(TmsPaperQuestionEntity::getForeignId, paperId);
        qQw.orderByAsc(TmsPaperQuestionEntity::getSortNo);
        List<TmsPaperQuestionEntity> links = this.paperQuestionMapper.selectList(qQw);
        HashMap<String, List<TmsPaperQuestionEntity>> bySection = new HashMap<>();
        for (TmsPaperQuestionEntity link : links) {
            bySection.computeIfAbsent(link.getSectionId(), k -> new ArrayList<>()).add(link);
        }
        List<String> questionIds = links.stream().map(TmsPaperQuestionEntity::getQuestionId).collect(Collectors.toList());
        HashMap<String, TmsQuestionEntity> questions = new HashMap<String, TmsQuestionEntity>();
        if (!questionIds.isEmpty()) {
            for (TmsQuestionEntity q : this.questionMapper.selectBatchIds(questionIds)) {
                questions.put(q.getId(), q);
            }
        }
        Map<String, List<TmsQuestionOptionEntity>> optionMap = this.loadOptions(questionIds);
        ArrayList<QuestionSnap> snaps = new ArrayList<QuestionSnap>();
        for (TmsPaperSectionEntity section : sections) {
            List<TmsPaperQuestionEntity> rows = bySection.getOrDefault(section.getId(), Collections.emptyList());
            rows.sort(Comparator.comparing(r -> r.getSortNo() == null ? 0 : r.getSortNo()));
            for (TmsPaperQuestionEntity row : rows) {
                TmsQuestionEntity question = questions.get(row.getQuestionId());
                if (question == null) {
                    throw new DataException("试卷中的试题已不存在,无法开考");
                }
                QuestionSnap snap = new QuestionSnap();
                snap.question = question;
                snap.score = row.getScore() == null ? BigDecimal.ZERO : row.getScore();
                snap.options = this.toStoredOptions(optionMap.getOrDefault(question.getId(), Collections.emptyList()));
                snaps.add(snap);
            }
        }
        return snaps;
    }
    private void writeAnswers(String examId, Map<String, Object> answers) {
        List<TmsExamItemEntity> items = this.loadItems(examId);
        Date now = new Date();
        for (TmsExamItemEntity item : items) {
            Object raw = answers == null ? null : answers.get(item.getId());
            item.setUserAnswer(this.normalizeAnswer(raw, item.getQuestionType()));
            item.setLastModifyTime(now);
            this.examItemMapper.updateById(item);
        }
    }
    private TmsOnlineExamPaperVO toPaperVo(TmsExamEntity exam, List<TmsExamItemEntity> items) {
        TmsOnlineExamPaperVO vo = new TmsOnlineExamPaperVO();
        vo.setExamId(exam.getId());
        vo.setPaperId(exam.getPaperId());
        vo.setPaperName(exam.getPaperName());
        vo.setPassScore(exam.getPassScore());
        vo.setDurationMin(exam.getDurationMin());
        vo.setStartTime(this.fmt(exam.getStartTime()));
        vo.setRemainSeconds(this.remainSeconds(exam));
        BigDecimal total = BigDecimal.ZERO;
        ArrayList<TmsOnlineExamQuestionVO> questions = new ArrayList<TmsOnlineExamQuestionVO>();
        for (TmsExamItemEntity item : items) {
            total = total.add(this.nvl(item.getScore()));
            TmsOnlineExamQuestionVO q = new TmsOnlineExamQuestionVO();
            q.setId(item.getId());
            q.setQuestionId(item.getQuestionId());
            q.setQuestionType(item.getQuestionType());
            q.setStem(item.getStem());
            q.setScore(item.getScore());
            q.setUserAnswer(item.getUserAnswer());
            q.setOptions(this.toClientOptions(item.getOptionsJson()));
            questions.add(q);
        }
        vo.setTotalScore(total);
        vo.setQuestions(questions);
        vo.setAttemptNo(Integer.valueOf(exam.getAttemptNo() == null ? 1 : exam.getAttemptNo()));
        vo.setAttemptLabel(this.attemptLabel(vo.getAttemptNo()));
        return vo;
    }
    private List<TmsOnlineExamQuestionVO> toReviewQuestions(List<TmsExamItemEntity> items) {
        ArrayList<TmsOnlineExamQuestionVO> questions = new ArrayList<TmsOnlineExamQuestionVO>();
        for (TmsExamItemEntity item : items) {
            TmsOnlineExamQuestionVO q = new TmsOnlineExamQuestionVO();
            q.setId(item.getId());
            q.setQuestionId(item.getQuestionId());
            q.setQuestionType(item.getQuestionType());
            q.setStem(item.getStem());
            q.setScore(item.getScore());
            q.setGotScore(item.getGotScore());
            q.setUserAnswer(item.getUserAnswer());
            q.setCorrectAnswer(item.getCorrectAnswer());
            boolean subjective = "1".equals(item.getIsSubjective());
            q.setSubjective(Boolean.valueOf(subjective));
            if (subjective) {
                q.setRight(null);
            } else if (item.getScore() != null && item.getScore().compareTo(BigDecimal.ZERO) > 0) {
                q.setRight(Boolean.valueOf(this.nvl(item.getGotScore()).compareTo(item.getScore()) >= 0));
            } else {
                q.setRight(Boolean.valueOf(Objects.equals(this.normalizeCorrect(item.getCorrectAnswer()), this.normalizeCorrect(item.getUserAnswer()))));
            }
            q.setOptions(this.toReviewOptions(item.getOptionsJson()));
            questions.add(q);
        }
        return questions;
    }
    private List<TmsOnlineExamOptionVO> toReviewOptions(String json) {
        List<StoredOption> stored = this.parseOptions(json);
        ArrayList<TmsOnlineExamOptionVO> list = new ArrayList<TmsOnlineExamOptionVO>(stored.size());
        for (StoredOption opt : stored) {
            TmsOnlineExamOptionVO vo = new TmsOnlineExamOptionVO();
            vo.setOptionLabel(opt.getOptionLabel());
            vo.setOptionContent(opt.getOptionContent());
            vo.setIsCorrect(opt.getIsCorrect());
            list.add(vo);
        }
        return list;
    }
    private List<TmsOnlineExamOptionVO> toClientOptions(String json) {
        List<StoredOption> stored = this.parseOptions(json);
        ArrayList<TmsOnlineExamOptionVO> list = new ArrayList<TmsOnlineExamOptionVO>(stored.size());
        for (StoredOption opt : stored) {
            TmsOnlineExamOptionVO vo = new TmsOnlineExamOptionVO();
            vo.setOptionLabel(opt.getOptionLabel());
            vo.setOptionContent(opt.getOptionContent());
            list.add(vo);
        }
        return list;
    }
    private TmsOnlineExamListVO toListVo(TmsPaperEntity paper, TmsExamEntity exam, String status) {
        TmsOnlineExamListVO vo = new TmsOnlineExamListVO();
        vo.setId(paper.getId());
        vo.setPaperId(paper.getId());
        vo.setPaperName(paper.getPaperName());
        vo.setStatus(status);
        vo.setExamStart(this.fmt(paper.getExamStart()));
        vo.setExamEnd(this.fmt(paper.getExamEnd()));
        vo.setExamTimeText(this.timeText(paper.getExamStart(), paper.getExamEnd()));
        vo.setTotalScore(paper.getTotalScore());
        vo.setPassScore(paper.getPassScore());
        vo.setDurationMin(paper.getDurationMin());
        if (exam != null && EXAM_SUBMITTED.equals(exam.getExamStatus())) {
            vo.setGotScore(exam.getTotalScore());
            vo.setPassFlag(exam.getPassFlag());
            vo.setExamId(exam.getId());
        } else if (exam != null) {
            vo.setExamId(exam.getId());
        }
        this.fillRetakeMeta(vo, paper.getId(), exam, UserProvider.getLoginUserId());
        return vo;
    }
    private void fillRetakeMeta(TmsOnlineExamListVO vo, String paperId, TmsExamEntity exam, String userId) {
        RetakeMeta meta = this.buildRetakeMeta(paperId, exam, userId);
        vo.setAttemptNo(meta.attemptNo);
        vo.setAttemptLabel(meta.attemptLabel);
        vo.setSubmittedCount(meta.submittedCount);
        vo.setRetakeLimit(meta.retakeLimit);
        vo.setRemainingRetakes(meta.remainingRetakes);
        vo.setCanRetake(meta.canRetake);
    }
    private void fillRetakeMeta(TmsOnlineExamDetailVO vo, String paperId, TmsExamEntity exam, String userId) {
        RetakeMeta meta = this.buildRetakeMeta(paperId, exam, userId);
        vo.setAttemptNo(meta.attemptNo);
        vo.setAttemptLabel(meta.attemptLabel);
        vo.setSubmittedCount(meta.submittedCount);
        vo.setRetakeLimit(meta.retakeLimit);
        vo.setRemainingRetakes(meta.remainingRetakes);
        vo.setCanRetake(meta.canRetake);
    }
    private RetakeMeta buildRetakeMeta(String paperId, TmsExamEntity exam, String userId) {
        RetakeMeta meta = new RetakeMeta();
        RetakeContext ctx = this.resolveRetake(userId, paperId);
        meta.retakeLimit = ctx.retakeLimit;
        meta.submittedCount = ctx.submittedCount == null ? 0 : ctx.submittedCount;
        meta.attemptNo = exam != null && exam.getAttemptNo() != null ? exam.getAttemptNo() : (ctx.nextAttemptNo != null ? ctx.nextAttemptNo : Integer.valueOf(1));
        meta.attemptLabel = this.attemptLabel(meta.attemptNo);
        if (meta.retakeLimit == null) {
            meta.remainingRetakes = 0;
            meta.canRetake = false;
        } else {
            int usedRetakes = Math.max(0, meta.submittedCount - 1);
            meta.remainingRetakes = Math.max(0, meta.retakeLimit - usedRetakes);
            boolean submittedFail = exam != null && EXAM_SUBMITTED.equals(exam.getExamStatus()) && !"1".equals(exam.getPassFlag());
            meta.canRetake = submittedFail && ctx.allowStart;
        }
        return meta;
    }
    private String attemptLabel(Integer attemptNo) {
        int n;
        int n2 = n = attemptNo == null ? 1 : attemptNo;
        if (n <= 1) {
            return "首考";
        }
        return "补考(第" + (n - 1) + "次)";
    }
    private Map<String, TmsExamEntity> loadLatestExams(String userId, List<TmsPaperEntity> papers) {
        HashMap<String, TmsExamEntity> map = new HashMap<String, TmsExamEntity>();
        if (papers.isEmpty() || StringUtil.isEmpty(userId)) {
            return map;
        }
        List<String> ids = papers.stream().map(TmsPaperEntity::getId).collect(Collectors.toList());
        LambdaQueryWrapper<TmsExamEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamEntity::getUserId, userId);
        qw.in(TmsExamEntity::getPaperId, ids);
        qw.orderByDesc(TmsExamEntity::getCreatorTime);
        for (TmsExamEntity exam : this.examMapper.selectList(qw)) {
            TmsExamEntity exists = map.get(exam.getPaperId());
            if (exists != null && !EXAM_DOING.equals(exam.getExamStatus())) continue;
            map.put(exam.getPaperId(), exam);
        }
        return map;
    }
    private TmsExamEntity latestExam(String userId, String paperId) {
        LambdaQueryWrapper<TmsExamEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamEntity::getUserId, userId);
        qw.eq(TmsExamEntity::getPaperId, paperId);
        qw.orderByDesc(TmsExamEntity::getCreatorTime);
        List<TmsExamEntity> list = this.examMapper.selectList(qw);
        TmsExamEntity picked = null;
        for (TmsExamEntity exam : list) {
            if (EXAM_DOING.equals(exam.getExamStatus())) {
                return exam;
            }
            if (picked != null) continue;
            picked = exam;
        }
        return picked;
    }
    private TmsExamEntity findDoing(String userId, String paperId) {
        LambdaQueryWrapper<TmsExamEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamEntity::getUserId, userId);
        qw.eq(TmsExamEntity::getPaperId, paperId);
        qw.eq(TmsExamEntity::getExamStatus, EXAM_DOING);
        qw.orderByDesc(TmsExamEntity::getCreatorTime);
        qw.last("limit 1");
        return this.examMapper.selectOne(qw);
    }
    private TmsExamEntity latestSubmitted(String userId, String paperId) {
        LambdaQueryWrapper<TmsExamEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamEntity::getUserId, userId);
        qw.eq(TmsExamEntity::getPaperId, paperId);
        qw.eq(TmsExamEntity::getExamStatus, EXAM_SUBMITTED);
        qw.orderByDesc(TmsExamEntity::getCreatorTime);
        qw.last("limit 1");
        return this.examMapper.selectOne(qw);
    }
    private RetakeContext resolveRetake(String userId, String paperId) {
        RetakeContext ctx = new RetakeContext();
        ctx.allowStart = true;
        ctx.nextAttemptNo = 1;
        TmsExamEntity last = this.latestSubmitted(userId, paperId);
        TmsPersonTaskEntity personTask = this.findActivePersonTask(userId, paperId);
        TmsTaskEntity task = null;
        if (personTask != null) {
            task = this.taskMapper.selectById((personTask.getTaskId()));
        }
        if (task != null) {
            ctx.taskId = task.getId();
            ctx.personTaskId = personTask == null ? null : personTask.getId();
            ctx.passScore = task.getPassScore();
            int retakeLimit = task.getRetakeLimit() == null ? 1 : Math.max(0, task.getRetakeLimit());
            long submitted = this.countSubmitted(userId, paperId, task.getId());
            ctx.retakeLimit = retakeLimit;
            ctx.submittedCount = (int)submitted;
            ctx.nextAttemptNo = (int)submitted + 1;
            if (last == null) {
                return ctx;
            }
            if ("1".equals(last.getPassFlag())) {
                ctx.allowStart = false;
                ctx.message = "该试卷已合格,无需重考";
                return ctx;
            }
            if (submitted > (long)retakeLimit) {
                ctx.allowStart = false;
                ctx.message = "补考次数已用尽(上限 " + retakeLimit + " 次)";
                return ctx;
            }
            return ctx;
        }
        if (last != null) {
            ctx.submittedCount = 1;
            ctx.retakeLimit = 0;
            ctx.allowStart = false;
            ctx.message = "该试卷已交卷";
        }
        return ctx;
    }
    private long countSubmitted(String userId, String paperId, String taskId) {
        Long cnt;
        LambdaQueryWrapper<TmsExamEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamEntity::getUserId, userId);
        qw.eq(TmsExamEntity::getPaperId, paperId);
        qw.eq(TmsExamEntity::getExamStatus, EXAM_SUBMITTED);
        if (StringUtil.isNotEmpty(taskId)) {
            qw.and(w -> ((w.eq(TmsExamEntity::getTaskId, taskId)).or()).isNull(TmsExamEntity::getTaskId));
        }
        return (cnt = this.examMapper.selectCount(qw)) == null ? 0L : cnt;
    }
    private TmsPersonTaskEntity findActivePersonTask(String userId, String paperId) {
        List<TmsTaskEntity> tasks = this.taskMapper.selectList(
                new LambdaQueryWrapper<TmsTaskEntity>()
                        .eq(TmsTaskEntity::getPaperId, paperId)
                        .eq(TmsTaskEntity::getPublishStatus, "published")
                        .orderByDesc(TmsTaskEntity::getPublishTime));
        for (TmsTaskEntity t : tasks) {
            TmsPersonTaskEntity pt = this.findPersonTask(t.getId(), userId);
            if (pt == null || "cancelled".equals(pt.getBizStatus())) {
                continue;
            }
            return pt;
        }
        return null;
    }
    private TmsPersonTaskEntity findPersonTask(String taskId, String userId) {
        return this.personTaskMapper.selectOne(new LambdaQueryWrapper<TmsPersonTaskEntity>()
                .eq(TmsPersonTaskEntity::getTaskId, taskId)
                .eq(TmsPersonTaskEntity::getUserId, userId)
                .last("limit 1"));
    }
    private TmsExamEntity requireDoingExam(String examId) {
        TmsExamEntity exam = this.examMapper.selectById(examId);
        if (exam == null) {
            throw new DataException("答卷不存在");
        }
        if (!Objects.equals(UserProvider.getLoginUserId(), exam.getUserId())) {
            throw new DataException("无权操作该答卷");
        }
        if (!EXAM_DOING.equals(exam.getExamStatus())) {
            throw new DataException("试卷已交卷");
        }
        return exam;
    }
    private TmsPaperEntity requireOpenPaper(String paperId) {
        TmsPaperEntity paper = this.paperMapper.selectById(paperId);
        if (paper == null) {
            throw new DataException("试卷不存在");
        }
        if (!STATUS_OPEN.equals(paper.getBizStatus())) {
            throw new DataException("试卷未开放,不能参加考试");
        }
        return paper;
    }
    private void assertExamWindow(TmsPaperEntity paper) {
        Date now = new Date();
        if (paper.getExamStart() != null && now.before(paper.getExamStart())) {
            throw new DataException("未到考试时间");
        }
        if (paper.getExamEnd() != null && now.after(paper.getExamEnd())) {
            throw new DataException("考试时间已结束");
        }
    }
    private List<TmsExamItemEntity> loadItems(String examId) {
        LambdaQueryWrapper<TmsExamItemEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsExamItemEntity::getForeignId, examId);
        qw.orderByAsc(TmsExamItemEntity::getSortNo);
        return this.examItemMapper.selectList(qw);
    }
    private Map<String, List<TmsQuestionOptionEntity>> loadOptions(List<String> questionIds) {
        if (questionIds == null || questionIds.isEmpty()) {
            return Collections.emptyMap();
        }
        LambdaQueryWrapper<TmsQuestionOptionEntity> qw = new LambdaQueryWrapper<>();
        qw.in(TmsQuestionOptionEntity::getForeignId, questionIds);
        qw.orderByAsc(TmsQuestionOptionEntity::getSortNo);
        HashMap<String, List<TmsQuestionOptionEntity>> map = new HashMap<String, List<TmsQuestionOptionEntity>>();
        for (TmsQuestionOptionEntity opt : this.optionMapper.selectList(qw)) {
            map.computeIfAbsent(opt.getForeignId(), k -> new ArrayList()).add(opt);
        }
        return map;
    }
    private List<StoredOption> toStoredOptions(List<TmsQuestionOptionEntity> opts) {
        ArrayList<StoredOption> list = new ArrayList<StoredOption>();
        for (TmsQuestionOptionEntity opt : opts) {
            StoredOption stored = new StoredOption();
            stored.setOptionLabel(opt.getOptionLabel());
            stored.setOptionContent(opt.getOptionContent());
            stored.setIsCorrect(opt.getIsCorrect());
            list.add(stored);
        }
        return list;
    }
    private String correctOf(QuestionSnap snap) {
        String type = snap.question.getQuestionType();
        if (TYPE_ESSAY.equals(type)) {
            return "";
        }
        if (TYPE_BLANK.equals(type)) {
            return snap.options.stream().map(StoredOption::getOptionContent).filter(StringUtil::isNotEmpty).collect(Collectors.joining("?"));
        }
        return snap.options.stream().filter(o -> "1".equals(o.getIsCorrect())).map(StoredOption::getOptionLabel).filter(StringUtil::isNotEmpty).sorted().collect(Collectors.joining(","));
    }
    private boolean answerRight(TmsExamItemEntity item, String userAnswer) {
        if (StringUtil.isEmpty(item.getCorrectAnswer())) {
            return false;
        }
        if (TYPE_BLANK.equals(item.getQuestionType())) {
            return item.getCorrectAnswer().trim().equals(userAnswer == null ? "" : userAnswer.trim());
        }
        return Objects.equals(this.normalizeCorrect(item.getCorrectAnswer()), this.normalizeCorrect(userAnswer));
    }
    private String normalizeAnswer(Object raw, String questionType) {
        if (raw == null) {
            return "";
        }
        if (TYPE_MULTI.equals(questionType)) {
            LinkedHashSet<String> labels = new LinkedHashSet<String>();
            if (raw instanceof List) {
                for (Object o : (List)raw) {
                    if (o == null || !StringUtil.isNotEmpty(String.valueOf(o))) continue;
                    labels.add(String.valueOf(o).trim());
                }
            } else {
                String text = String.valueOf(raw).trim();
                if (StringUtil.isNotEmpty(text)) {
                    for (String part : text.split("[,?]")) {
                        if (!StringUtil.isNotEmpty(part.trim())) continue;
                        labels.add(part.trim());
                    }
                }
            }
            return labels.stream().sorted().collect(Collectors.joining(","));
        }
        return String.valueOf(raw).trim();
    }
    private String normalizeCorrect(String correct) {
        if (StringUtil.isEmpty(correct)) {
            return "";
        }
        return Arrays.stream(correct.split("[,?]")).map(String::trim).filter(StringUtil::isNotEmpty).sorted().collect(Collectors.joining(","));
    }
    private boolean isTimedOut(TmsExamEntity exam) {
        Integer remain = this.remainSeconds(exam);
        return remain != null && remain <= 0;
    }
    private Integer remainSeconds(TmsExamEntity exam) {
        if (exam == null || exam.getDurationMin() == null || exam.getDurationMin() <= 0 || exam.getStartTime() == null) {
            return null;
        }
        long endMs = exam.getStartTime().getTime() + (long)exam.getDurationMin().intValue() * 60000L;
        long remain = (endMs - System.currentTimeMillis()) / 1000L;
        return (int)Math.max(0L, remain);
    }
    private String passFlag(BigDecimal score, BigDecimal passScore) {
        if (passScore == null) {
            return null;
        }
        return this.nvl(score).compareTo(passScore) >= 0 ? "1" : "0";
    }
    private String statusOf(TmsExamEntity exam) {
        if (exam == null) {
            return NOT_STARTED;
        }
        if (EXAM_DOING.equals(exam.getExamStatus())) {
            return EXAM_DOING;
        }
        return EXAM_SUBMITTED;
    }
    private String timeText(Date start, Date end) {
        if (start == null && end == null) {
            return "??";
        }
        String left = start == null ? "??" : this.fmt(start);
        String right = end == null ? "??" : this.fmt(end);
        return left + " ~ " + right;
    }
    private String fmt(Date date) {
        return date == null ? null : DateUtil.dateToString((Date)date, (String)FMT);
    }
    private BigDecimal nvl(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private String toJson(Object obj) {
        try {
            return OBJECT_MAPPER.writeValueAsString(obj);
        }
        catch (Exception e) {
            throw new DataException("JSON序列化失败");
        }
    }
    private List<StoredOption> parseOptions(String json) {
        if (StringUtil.isEmpty(json)) {
            return Collections.emptyList();
        }
        try {
            return (List)OBJECT_MAPPER.readValue(json, (TypeReference)new TypeReference<List<StoredOption>>(){});
        }
        catch (Exception e) {
            return Collections.emptyList();
        }
    }
    private static class RetakeContext {
        private boolean allowStart;
        private String message;
        private String taskId;
        private String personTaskId;
        private Integer nextAttemptNo;
        private BigDecimal passScore;
        private Integer retakeLimit;
        private Integer submittedCount;
        private RetakeContext() {
        }
    }
    private static class QuestionSnap {
        private TmsQuestionEntity question;
        private BigDecimal score;
        private List<StoredOption> options;
        private QuestionSnap() {
        }
    }
    @JsonIgnoreProperties(ignoreUnknown=true)
    private static class StoredOption {
        private String optionLabel;
        private String optionContent;
        private String isCorrect;
        public StoredOption() {
        }
        public String getOptionLabel() {
            return this.optionLabel;
        }
        public String getOptionContent() {
            return this.optionContent;
        }
        public String getIsCorrect() {
            return this.isCorrect;
        }
        public void setOptionLabel(String optionLabel) {
            this.optionLabel = optionLabel;
        }
        public void setOptionContent(String optionContent) {
            this.optionContent = optionContent;
        }
        public void setIsCorrect(String isCorrect) {
            this.isCorrect = isCorrect;
        }
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            }
            if (!(o instanceof StoredOption)) {
                return false;
            }
            StoredOption other = (StoredOption)o;
            if (!other.canEqual(this)) {
                return false;
            }
            String this$optionLabel = this.getOptionLabel();
            String other$optionLabel = other.getOptionLabel();
            if (this$optionLabel == null ? other$optionLabel != null : !this$optionLabel.equals(other$optionLabel)) {
                return false;
            }
            String this$optionContent = this.getOptionContent();
            String other$optionContent = other.getOptionContent();
            if (this$optionContent == null ? other$optionContent != null : !this$optionContent.equals(other$optionContent)) {
                return false;
            }
            String this$isCorrect = this.getIsCorrect();
            String other$isCorrect = other.getIsCorrect();
            return !(this$isCorrect == null ? other$isCorrect != null : !this$isCorrect.equals(other$isCorrect));
        }
        protected boolean canEqual(Object other) {
            return other instanceof StoredOption;
        }
        public int hashCode() {
            int PRIME = 59;
            int result = 1;
            String $optionLabel = this.getOptionLabel();
            result = result * 59 + ($optionLabel == null ? 43 : $optionLabel.hashCode());
            String $optionContent = this.getOptionContent();
            result = result * 59 + ($optionContent == null ? 43 : $optionContent.hashCode());
            String $isCorrect = this.getIsCorrect();
            result = result * 59 + ($isCorrect == null ? 43 : $isCorrect.hashCode());
            return result;
        }
        public String toString() {
            return "TmsOnlineExamServiceImpl.StoredOption(optionLabel=" + this.getOptionLabel() + ", optionContent=" + this.getOptionContent() + ", isCorrect=" + this.getIsCorrect() + ")";
        }
    }
    /** 超时已自动交卷:配合 noRollbackFor,保证交卷结果先提交 */
    public static class ExamTimedOutException extends DataException {
        public ExamTimedOutException() {
            super("考试时间已到,已自动交卷");
        }
    }
    private static class RetakeMeta {
        private Integer attemptNo;
        private String attemptLabel;
        private Integer submittedCount;
        private Integer retakeLimit;
        private Integer remainingRetakes;
        private Boolean canRetake;
        private RetakeMeta() {
        }
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPaperServiceImpl.java
New file
@@ -0,0 +1,558 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jnpf.exception.DataException;
import jnpf.tmsEntity.TmsPaperEntity;
import jnpf.tmsEntity.TmsPaperQuestionEntity;
import jnpf.tmsEntity.TmsPaperSectionEntity;
import jnpf.tmsEntity.TmsQuestionBankEntity;
import jnpf.tmsEntity.TmsQuestionEntity;
import jnpf.tmsEntity.paper.TmsPaperForm;
import jnpf.tmsEntity.paper.TmsPaperQuestionForm;
import jnpf.tmsEntity.paper.TmsPaperQuery;
import jnpf.tmsEntity.paper.TmsPaperSectionForm;
import jnpf.tmsMapper.TmsPaperMapper;
import jnpf.tmsMapper.TmsPaperQuestionMapper;
import jnpf.tmsMapper.TmsPaperSectionMapper;
import jnpf.tmsMapper.TmsQuestionBankMapper;
import jnpf.tmsMapper.TmsQuestionMapper;
import jnpf.tmsService.TmsPaperService;
import jnpf.util.DateUtil;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
@RequiredArgsConstructor
public class TmsPaperServiceImpl implements TmsPaperService {
    private static final String STATUS_OPEN = "open";
    private static final String STATUS_INVALID = "invalid";
    private static final String SORT_PAPER = "paper";
    private static final Set<String> SUBJECTIVE_TYPES = Collections.singleton("essay");
    private final TmsPaperMapper paperMapper;
    private final TmsPaperSectionMapper sectionMapper;
    private final TmsPaperQuestionMapper paperQuestionMapper;
    private final TmsQuestionMapper questionMapper;
    private final TmsQuestionBankMapper bankMapper;
    @Override
    public List<TmsPaperForm> getList(TmsPaperQuery query) {
        LambdaQueryWrapper<TmsPaperEntity> qw = new LambdaQueryWrapper<>();
        if (StringUtil.isNotEmpty(query.getBizStatus())) {
            qw.eq(TmsPaperEntity::getBizStatus, query.getBizStatus());
        } else {
            qw.ne(TmsPaperEntity::getBizStatus, STATUS_INVALID);
        }
        if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) {
            String kw = query.getKeyword().trim();
            qw.and(w -> w.like(TmsPaperEntity::getPaperName, kw).or().like(TmsPaperEntity::getPaperNo, kw));
        }
        qw.orderByDesc(TmsPaperEntity::getCreatorTime);
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        Page<TmsPaperEntity> page = paperMapper.selectPage(new Page<>(current, size), qw);
        Map<String, Integer> countMap = loadQuestionCounts(page.getRecords());
        List<TmsPaperForm> list = new ArrayList<>(page.getRecords().size());
        for (TmsPaperEntity row : page.getRecords()) {
            TmsPaperForm form = toHeaderForm(row);
            form.setQuestionCount(countMap.getOrDefault(row.getId(), 0));
            list.add(form);
        }
        query.setData(list, page.getTotal());
        return list;
    }
    @Override
    public TmsPaperForm getInfo(String id) {
        TmsPaperEntity entity = requireOne(id);
        TmsPaperForm form = toHeaderForm(entity);
        form.setSections(loadSections(id));
        int count = 0;
        if (form.getSections() != null) {
            for (TmsPaperSectionForm sec : form.getSections()) {
                if (sec.getQuestions() != null) {
                    count += sec.getQuestions().size();
                }
            }
        }
        form.setQuestionCount(count);
        return form;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String create(TmsPaperForm form) {
        validateForm(form);
        String id = RandomUtil.uuId();
        Date now = new Date();
        String userId = UserProvider.getLoginUserId();
        TmsPaperEntity entity = new TmsPaperEntity();
        entity.setId(id);
        entity.setPaperNo(nextPaperNo());
        applyHeader(entity, form);
        entity.setCreatorUserId(userId);
        entity.setCreatorTime(now);
        paperMapper.insert(entity);
        saveComposition(id, form.getSections(), userId, now);
        refreshComputed(id);
        return id;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(String id, TmsPaperForm form) {
        TmsPaperEntity entity = requireOne(id);
        if (STATUS_INVALID.equals(entity.getBizStatus())) {
            throw new DataException("已废弃试卷不可编辑");
        }
        if (STATUS_OPEN.equals(entity.getBizStatus())) {
            throw new DataException("请先停用后再编辑");
        }
        validateForm(form);
        Date now = new Date();
        String userId = UserProvider.getLoginUserId();
        applyHeader(entity, form);
        entity.setLastModifyUserId(userId);
        entity.setLastModifyTime(now);
        paperMapper.updateById(entity);
        // 章节/试题全量替换(已考快照在 tms_exam_item,改卷不影响历史)
        clearComposition(id);
        saveComposition(id, form.getSections(), userId, now);
        refreshComputed(id);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void invalidate(String id) {
        TmsPaperEntity entity = requireOne(id);
        if (STATUS_INVALID.equals(entity.getBizStatus())) {
            return;
        }
        if (STATUS_OPEN.equals(entity.getBizStatus())) {
            throw new DataException("请先停用后再废弃");
        }
        entity.setBizStatus(STATUS_INVALID);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        paperMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void setBizStatus(String id, String bizStatus) {
        if (!STATUS_OPEN.equals(bizStatus) && !"closed".equals(bizStatus)) {
            throw new DataException("状态无效,仅支持开放/不开放");
        }
        TmsPaperEntity entity = requireOne(id);
        if (STATUS_INVALID.equals(entity.getBizStatus())) {
            throw new DataException("已废弃试卷不可启用或停用");
        }
        entity.setBizStatus(bizStatus);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        paperMapper.updateById(entity);
    }
    private void validateForm(TmsPaperForm form) {
        if (form == null || StringUtil.isEmpty(form.getPaperName())) {
            throw new DataException("请填写试卷名称");
        }
        if (form.getDurationMin() != null && form.getDurationMin() < 0) {
            throw new DataException("考试时长不能为负数");
        }
        Date start = parseDate(form.getExamStart());
        Date end = parseDate(form.getExamEnd());
        Date publish = parseDate(form.getScorePublishTime());
        if ((start == null) != (end == null)) {
            throw new DataException("开考时间与结束时间需同时填写或同时留空");
        }
        if (start != null && end != null) {
            if (!end.after(start)) {
                throw new DataException("结束时间必须晚于开考时间");
            }
            if (form.getDurationMin() != null && form.getDurationMin() > 0) {
                long spanMin = (end.getTime() - start.getTime()) / 60_000L;
                if (form.getDurationMin() > spanMin) {
                    throw new DataException("考试时长不能超过开考至结束的间隔(" + spanMin + " 分钟)");
                }
            }
        }
        if (publish != null) {
            if (end != null && publish.before(end)) {
                throw new DataException("成绩公布时间不能早于结束时间");
            }
            if (end == null && start != null && publish.before(start)) {
                throw new DataException("成绩公布时间不能早于开考时间");
            }
        }
        BigDecimal totalFromForm = sumSectionScores(form);
        if (form.getPassScore() != null && totalFromForm != null
                && form.getPassScore().compareTo(totalFromForm) > 0) {
            throw new DataException("合格分数不能大于试卷总分(当前总分 " + totalFromForm.stripTrailingZeros().toPlainString() + ")");
        }
        if (form.getSections() != null) {
            Set<String> usedQuestionIds = new HashSet<>();
            for (TmsPaperSectionForm sec : form.getSections()) {
                if (sec.getQuestions() == null) {
                    continue;
                }
                for (TmsPaperQuestionForm q : sec.getQuestions()) {
                    if (StringUtil.isEmpty(q.getQuestionId())) {
                        throw new DataException("试卷试题缺少试题ID");
                    }
                    if (q.getScore() == null || q.getScore().compareTo(BigDecimal.ZERO) <= 0) {
                        throw new DataException("组卷试题分值必须大于 0");
                    }
                    if (!usedQuestionIds.add(q.getQuestionId())) {
                        throw new DataException("同一试卷不能重复添加同一试题");
                    }
                    TmsQuestionEntity question = questionMapper.selectById(q.getQuestionId());
                    if (question == null) {
                        throw new DataException("试题不存在或已废弃,无法组卷");
                    }
                    if (STATUS_INVALID.equals(question.getBizStatus())) {
                        throw new DataException("废弃试题不可用于组卷:" + question.getQuestionNo());
                    }
                    if (StringUtil.isNotEmpty(sec.getQuestionType())
                            && StringUtil.isNotEmpty(question.getQuestionType())
                            && !sec.getQuestionType().equals(question.getQuestionType())) {
                        throw new DataException("试题「" + question.getQuestionNo() + "」与章节题型不一致");
                    }
                }
            }
        }
    }
    private static BigDecimal sumSectionScores(TmsPaperForm form) {
        if (form == null || form.getSections() == null) {
            return BigDecimal.ZERO;
        }
        BigDecimal total = BigDecimal.ZERO;
        for (TmsPaperSectionForm sec : form.getSections()) {
            if (sec.getQuestions() == null) {
                continue;
            }
            for (TmsPaperQuestionForm q : sec.getQuestions()) {
                if (q.getScore() != null) {
                    total = total.add(q.getScore());
                }
            }
        }
        return total;
    }
    private void applyHeader(TmsPaperEntity entity, TmsPaperForm form) {
        entity.setPaperName(form.getPaperName().trim());
        // 保存仅允许开放/不开放;废弃走 invalidate
        String status = StringUtil.isNotEmpty(form.getBizStatus()) ? form.getBizStatus() : STATUS_OPEN;
        if (!STATUS_OPEN.equals(status) && !"closed".equals(status)) {
            status = STATUS_OPEN;
        }
        entity.setBizStatus(status);
        entity.setDurationMin(form.getDurationMin() == null ? 60 : form.getDurationMin());
        entity.setExamStart(parseDate(form.getExamStart()));
        entity.setExamEnd(parseDate(form.getExamEnd()));
        entity.setScorePublishTime(parseDate(form.getScorePublishTime()));
        entity.setPassScore(form.getPassScore());
        entity.setTotalScore(form.getTotalScore());
        entity.setSortMode(StringUtil.isNotEmpty(form.getSortMode()) ? form.getSortMode() : SORT_PAPER);
        entity.setRemark(form.getRemark());
    }
    private void clearComposition(String paperId) {
        LambdaQueryWrapper<TmsPaperQuestionEntity> qQw = new LambdaQueryWrapper<>();
        qQw.eq(TmsPaperQuestionEntity::getForeignId, paperId);
        paperQuestionMapper.delete(qQw);
        LambdaQueryWrapper<TmsPaperSectionEntity> sQw = new LambdaQueryWrapper<>();
        sQw.eq(TmsPaperSectionEntity::getForeignId, paperId);
        sectionMapper.delete(sQw);
    }
    private void saveComposition(String paperId, List<TmsPaperSectionForm> sections, String userId, Date now) {
        if (sections == null || sections.isEmpty()) {
            return;
        }
        int secIdx = 0;
        for (TmsPaperSectionForm secForm : sections) {
            String sectionId = RandomUtil.uuId();
            TmsPaperSectionEntity sec = new TmsPaperSectionEntity();
            sec.setId(sectionId);
            sec.setForeignId(paperId);
            sec.setSectionName(StringUtil.isEmpty(secForm.getSectionName())
                    ? defaultSectionName(secForm.getQuestionType(), ++secIdx)
                    : secForm.getSectionName().trim());
            sec.setQuestionType(secForm.getQuestionType());
            sec.setSortNo(secForm.getSortNo() != null ? secForm.getSortNo() : secIdx);
            sec.setCreatorUserId(userId);
            sec.setCreatorTime(now);
            sectionMapper.insert(sec);
            if (secForm.getQuestions() == null || secForm.getQuestions().isEmpty()) {
                continue;
            }
            int qIdx = 0;
            for (TmsPaperQuestionForm qForm : secForm.getQuestions()) {
                TmsPaperQuestionEntity pq = new TmsPaperQuestionEntity();
                pq.setId(RandomUtil.uuId());
                pq.setForeignId(paperId);
                pq.setSectionId(sectionId);
                pq.setQuestionId(qForm.getQuestionId());
                pq.setScore(qForm.getScore() == null ? BigDecimal.ZERO : qForm.getScore());
                pq.setSortNo(qForm.getSortNo() != null ? qForm.getSortNo() : ++qIdx);
                pq.setCreatorUserId(userId);
                pq.setCreatorTime(now);
                paperQuestionMapper.insert(pq);
            }
        }
    }
    /** 按卷内试题重算总分、是否含主观题 */
    private void refreshComputed(String paperId) {
        TmsPaperEntity entity = paperMapper.selectById(paperId);
        if (entity == null) {
            return;
        }
        LambdaQueryWrapper<TmsPaperQuestionEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsPaperQuestionEntity::getForeignId, paperId);
        List<TmsPaperQuestionEntity> rows = paperQuestionMapper.selectList(qw);
        BigDecimal total = BigDecimal.ZERO;
        boolean subjective = false;
        for (TmsPaperQuestionEntity row : rows) {
            if (row.getScore() != null) {
                total = total.add(row.getScore());
            }
            if (!subjective) {
                TmsQuestionEntity q = questionMapper.selectById(row.getQuestionId());
                if (q != null && SUBJECTIVE_TYPES.contains(q.getQuestionType())) {
                    subjective = true;
                }
            }
        }
        entity.setTotalScore(total);
        entity.setHasSubjective(subjective ? "1" : "0");
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        paperMapper.updateById(entity);
    }
    private List<TmsPaperSectionForm> loadSections(String paperId) {
        LambdaQueryWrapper<TmsPaperSectionEntity> sQw = new LambdaQueryWrapper<>();
        sQw.eq(TmsPaperSectionEntity::getForeignId, paperId);
        sQw.orderByAsc(TmsPaperSectionEntity::getSortNo);
        List<TmsPaperSectionEntity> sections = sectionMapper.selectList(sQw);
        LambdaQueryWrapper<TmsPaperQuestionEntity> qQw = new LambdaQueryWrapper<>();
        qQw.eq(TmsPaperQuestionEntity::getForeignId, paperId);
        qQw.orderByAsc(TmsPaperQuestionEntity::getSortNo);
        List<TmsPaperQuestionEntity> questions = paperQuestionMapper.selectList(qQw);
        Map<String, TmsQuestionEntity> questionMap = loadQuestions(questions);
        Map<String, String> bankNames = loadBankNames(questionMap.values());
        Map<String, List<TmsPaperQuestionForm>> bySection = new HashMap<>();
        for (TmsPaperQuestionEntity pq : questions) {
            TmsPaperQuestionForm qf = new TmsPaperQuestionForm();
            qf.setId(pq.getId());
            qf.setSectionId(pq.getSectionId());
            qf.setQuestionId(pq.getQuestionId());
            qf.setScore(pq.getScore());
            qf.setSortNo(pq.getSortNo());
            TmsQuestionEntity q = questionMap.get(pq.getQuestionId());
            if (q != null) {
                qf.setQuestionNo(q.getQuestionNo());
                qf.setStem(q.getStem());
                qf.setQuestionType(q.getQuestionType());
                qf.setBankName(bankNames.get(q.getBankId()));
            }
            bySection.computeIfAbsent(pq.getSectionId(), k -> new ArrayList<>()).add(qf);
        }
        List<TmsPaperSectionForm> result = new ArrayList<>(sections.size());
        for (TmsPaperSectionEntity sec : sections) {
            TmsPaperSectionForm sf = new TmsPaperSectionForm();
            sf.setId(sec.getId());
            sf.setSectionName(sec.getSectionName());
            sf.setQuestionType(sec.getQuestionType());
            sf.setSortNo(sec.getSortNo());
            sf.setQuestions(bySection.getOrDefault(sec.getId(), Collections.emptyList()));
            result.add(sf);
        }
        return result;
    }
    private Map<String, TmsQuestionEntity> loadQuestions(List<TmsPaperQuestionEntity> rows) {
        Set<String> ids = new HashSet<>();
        for (TmsPaperQuestionEntity row : rows) {
            if (StringUtil.isNotEmpty(row.getQuestionId())) {
                ids.add(row.getQuestionId());
            }
        }
        if (ids.isEmpty()) {
            return Collections.emptyMap();
        }
        List<TmsQuestionEntity> list = questionMapper.selectBatchIds(ids);
        Map<String, TmsQuestionEntity> map = new HashMap<>(list.size());
        for (TmsQuestionEntity q : list) {
            map.put(q.getId(), q);
        }
        return map;
    }
    private Map<String, String> loadBankNames(Iterable<TmsQuestionEntity> questions) {
        Set<String> bankIds = new HashSet<>();
        for (TmsQuestionEntity q : questions) {
            if (q != null && StringUtil.isNotEmpty(q.getBankId())) {
                bankIds.add(q.getBankId());
            }
        }
        if (bankIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<TmsQuestionBankEntity> banks = bankMapper.selectBatchIds(bankIds);
        Map<String, String> map = new HashMap<>(banks.size());
        for (TmsQuestionBankEntity bank : banks) {
            map.put(bank.getId(), bank.getBankName());
        }
        return map;
    }
    private Map<String, Integer> loadQuestionCounts(List<TmsPaperEntity> papers) {
        if (papers == null || papers.isEmpty()) {
            return Collections.emptyMap();
        }
        Set<String> ids = new HashSet<>();
        for (TmsPaperEntity p : papers) {
            ids.add(p.getId());
        }
        LambdaQueryWrapper<TmsPaperQuestionEntity> qw = new LambdaQueryWrapper<>();
        qw.in(TmsPaperQuestionEntity::getForeignId, ids);
        List<TmsPaperQuestionEntity> rows = paperQuestionMapper.selectList(qw);
        Map<String, Integer> map = new HashMap<>();
        for (TmsPaperQuestionEntity row : rows) {
            map.merge(row.getForeignId(), 1, Integer::sum);
        }
        return map;
    }
    private TmsPaperEntity requireOne(String id) {
        if (StringUtil.isEmpty(id)) {
            throw new DataException("试卷不存在");
        }
        TmsPaperEntity entity = paperMapper.selectById(id);
        if (entity == null) {
            throw new DataException("试卷不存在");
        }
        return entity;
    }
    private TmsPaperForm toHeaderForm(TmsPaperEntity entity) {
        TmsPaperForm form = new TmsPaperForm();
        form.setId(entity.getId());
        form.setPaperNo(entity.getPaperNo());
        form.setPaperName(entity.getPaperName());
        form.setBizStatus(entity.getBizStatus());
        form.setDurationMin(entity.getDurationMin());
        form.setExamStart(formatDate(entity.getExamStart()));
        form.setExamEnd(formatDate(entity.getExamEnd()));
        form.setScorePublishTime(formatDate(entity.getScorePublishTime()));
        form.setPassScore(entity.getPassScore());
        form.setTotalScore(entity.getTotalScore());
        form.setSortMode(entity.getSortMode());
        form.setHasSubjective(entity.getHasSubjective());
        form.setRemark(entity.getRemark());
        form.setCreatorTime(formatDate(entity.getCreatorTime()));
        return form;
    }
    /**
     * 试卷编号:年份后两位 + 4 位流水,如 260001。
     * 按当年最大号 +1,含已删记录,避免流水回退。
     */
    private synchronized String nextPaperNo() {
        String prefix = String.format("%02d", LocalDate.now().getYear() % 100);
        String maxNo = paperMapper.selectMaxPaperNo(prefix);
        int next = 1;
        if (StringUtil.isNotEmpty(maxNo) && maxNo.length() >= 6 && maxNo.startsWith(prefix)) {
            try {
                next = Integer.parseInt(maxNo.substring(prefix.length())) + 1;
            } catch (NumberFormatException ignored) {
                next = 1;
            }
        }
        if (next > 9999) {
            throw new DataException("当年试卷编号已用尽(" + prefix + "9999)");
        }
        return prefix + String.format("%04d", next);
    }
    private static String defaultSectionName(String questionType, int idx) {
        if (StringUtil.isEmpty(questionType)) {
            return "第" + idx + "章";
        }
        switch (questionType) {
            case "single":
                return "单选题";
            case "multi":
                return "多选题";
            case "judge":
                return "判断题";
            case "blank":
                return "填空题";
            case "essay":
                return "问答题";
            default:
                return "第" + idx + "章";
        }
    }
    private static Date parseDate(String text) {
        if (StringUtil.isEmpty(text)) {
            return null;
        }
        String raw = text.trim();
        try {
            if (raw.matches("^\\d{10,13}$")) {
                long ts = Long.parseLong(raw);
                if (raw.length() == 10) {
                    ts = ts * 1000L;
                }
                return new Date(ts);
            }
            return DateUtil.stringToDate(raw);
        } catch (Exception ex) {
            throw new DataException("时间格式无效:" + text);
        }
    }
    private static String formatDate(Date date) {
        if (date == null) {
            return null;
        }
        return DateUtil.dateToString(date, "yyyy-MM-dd HH:mm:ss");
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPersonTaskServiceImpl.java
New file
@@ -0,0 +1,543 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jnpf.exception.DataException;
import jnpf.tmsEntity.TmsMaterialEntity;
import jnpf.tmsEntity.TmsPersonTaskEntity;
import jnpf.tmsEntity.TmsTaskEntity;
import jnpf.tmsEntity.TmsTaskFileEntity;
import jnpf.tmsEntity.TmsTrainModeEntity;
import jnpf.tmsEntity.person.TmsPersonTaskFileForm;
import jnpf.tmsEntity.person.TmsPersonTaskLearnResult;
import jnpf.tmsEntity.person.TmsPersonTaskMineForm;
import jnpf.tmsEntity.person.TmsPersonTaskQuery;
import jnpf.tmsEntity.person.TmsPersonTaskSignResult;
import jnpf.tmsMapper.TmsMaterialMapper;
import jnpf.tmsMapper.TmsPersonTaskMapper;
import jnpf.tmsMapper.TmsTaskFileMapper;
import jnpf.tmsMapper.TmsTaskMapper;
import jnpf.tmsMapper.TmsTrainModeMapper;
import jnpf.tmsService.TmsPersonTaskService;
import jnpf.util.DateUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class TmsPersonTaskServiceImpl implements TmsPersonTaskService {
    private static final String FMT = "yyyy-MM-dd HH:mm:ss";
    private static final String STATUS_CANCELLED = "cancelled";
    private static final String STATUS_DONE = "done";
    private static final String STATUS_SIGNED = "signed";
    private static final String STATUS_TODO = "todo";
    private static final String SIGN_MODE_ONLINE = "online";
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private final TmsPersonTaskMapper personTaskMapper;
    private final TmsTaskMapper taskMapper;
    private final TmsTaskFileMapper taskFileMapper;
    private final TmsMaterialMapper materialMapper;
    private final TmsTrainModeMapper trainModeMapper;
    @Override
    public List<TmsPersonTaskMineForm> getMine(TmsPersonTaskQuery query) {
        String userId = UserProvider.getLoginUserId();
        LambdaQueryWrapper<TmsPersonTaskEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsPersonTaskEntity::getUserId, userId);
        qw.ne(TmsPersonTaskEntity::getBizStatus, STATUS_CANCELLED);
        if (StringUtil.isNotEmpty(query.getBizStatus())) {
            qw.eq(TmsPersonTaskEntity::getBizStatus, query.getBizStatus().trim());
        }
        qw.orderByDesc(TmsPersonTaskEntity::getCreatorTime);
        List<TmsPersonTaskEntity> persons = personTaskMapper.selectList(qw);
        Map<String, TmsTaskEntity> taskMap = loadTasks(persons);
        String keyword = StringUtil.isNotEmpty(query.getKeyword()) ? query.getKeyword().trim() : null;
        List<TmsPersonTaskMineForm> all = new ArrayList<>();
        for (TmsPersonTaskEntity person : persons) {
            TmsTaskEntity task = taskMap.get(person.getTaskId());
            if (task == null || !"published".equals(task.getPublishStatus())) {
                continue;
            }
            if (keyword != null && !matchKeyword(person, task, keyword)) {
                continue;
            }
            all.add(toForm(person, task, false));
        }
        all.sort(Comparator.comparing(TmsPersonTaskMineForm::getStartTime,
                Comparator.nullsLast(Comparator.reverseOrder())));
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        int from = (int) Math.min((current - 1) * size, all.size());
        int to = (int) Math.min(from + size, all.size());
        List<TmsPersonTaskMineForm> page = all.subList(from, to);
        query.setData(page, all.size());
        return page;
    }
    @Override
    public TmsPersonTaskMineForm getInfo(String id) {
        TmsPersonTaskEntity person = requireMine(id);
        TmsTaskEntity task = taskMapper.selectById(person.getTaskId());
        if (task == null) {
            throw new DataException("培训任务不存在");
        }
        TmsPersonTaskMineForm form = toForm(person, task, true);
        form.setFiles(loadFiles(task.getId()));
        return form;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public TmsPersonTaskLearnResult markLearned(String id, Integer seconds) {
        TmsPersonTaskEntity person = requireMine(id);
        TmsTaskEntity task = taskMapper.selectById(person.getTaskId());
        if (task == null) {
            throw new DataException("培训任务不存在");
        }
        int before = person.getLearnSeconds() == null ? 0 : person.getLearnSeconds();
        int required = toRequiredSeconds(person.getHours());
        boolean alreadyFull = required > 0 && before >= required;
        int add = seconds == null ? 0 : Math.max(0, seconds);
        if (add > 0) {
            person.setLearnSeconds(before + add);
            person.setLastModifyUserId(UserProvider.getLoginUserId());
            person.setLastModifyTime(new Date());
            personTaskMapper.updateById(person);
        }
        int learned = person.getLearnSeconds() == null ? 0 : person.getLearnSeconds();
        boolean justFull = required > 0 && !alreadyFull && learned >= required;
        TmsPersonTaskLearnResult result = new TmsPersonTaskLearnResult();
        result.setLearnedSeconds(learned);
        result.setRequiredSeconds(required);
        result.setLearnStatus(resolveLearnStatus(learned, required));
        result.setBizStatus(person.getBizStatus());
        result.setAutoCompleted(false);
        result.setMessage("");
        boolean needExam = "exam".equals(task.getEvalMode()) && StringUtil.isNotEmpty(task.getPaperId());
        if (justFull) {
            if (!needExam && !STATUS_DONE.equals(person.getBizStatus())
                    && !STATUS_CANCELLED.equals(person.getBizStatus())) {
                person.setBizStatus(STATUS_DONE);
                person.setLastModifyUserId(UserProvider.getLoginUserId());
                person.setLastModifyTime(new Date());
                personTaskMapper.updateById(person);
                result.setAutoCompleted(true);
                result.setBizStatus(STATUS_DONE);
                result.setMessage("已学满规定课时,系统已自动确认完成");
            } else if (needExam) {
                if (person.getSignTime() == null) {
                    result.setMessage("已学满规定课时,请先签到后再进入考试");
                } else {
                    result.setMessage("已学满规定课时,可以进入考试");
                }
            }
        }
        TmsPersonTaskMineForm tmp = new TmsPersonTaskMineForm();
        fillExamFlag(tmp, task, person);
        result.setCanExam(Boolean.TRUE.equals(tmp.getCanExam()));
        result.setExamTip(tmp.getExamTip());
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public TmsPersonTaskSignResult sign(String id) {
        TmsPersonTaskEntity person = requireMine(id);
        TmsTaskEntity task = taskMapper.selectById(person.getTaskId());
        if (task == null) {
            throw new DataException("培训任务不存在");
        }
        if (!"published".equals(task.getPublishStatus())) {
            throw new DataException("任务未发布,不可签到");
        }
        if (STATUS_CANCELLED.equals(person.getBizStatus())) {
            throw new DataException("任务已取消,不可签到");
        }
        if (person.getSignTime() != null) {
            throw new DataException("您已签到,无需重复签到");
        }
        String deny = resolveSignDenyReason(task, person);
        if (StringUtil.isNotEmpty(deny)) {
            throw new DataException(deny);
        }
        Date now = new Date();
        person.setSignTime(now);
        person.setSignMode(SIGN_MODE_ONLINE);
        if (STATUS_TODO.equals(person.getBizStatus()) || StringUtil.isEmpty(person.getBizStatus())) {
            person.setBizStatus(STATUS_SIGNED);
        }
        person.setLastModifyUserId(UserProvider.getLoginUserId());
        person.setLastModifyTime(now);
        personTaskMapper.updateById(person);
        TmsPersonTaskSignResult result = new TmsPersonTaskSignResult();
        result.setSignTime(formatDate(now));
        result.setBizStatus(person.getBizStatus());
        result.setSigned(true);
        result.setCanSign(false);
        result.setSignTip("已签到");
        result.setMessage("签到成功");
        TmsPersonTaskMineForm tmp = new TmsPersonTaskMineForm();
        fillExamFlag(tmp, task, person);
        result.setCanExam(Boolean.TRUE.equals(tmp.getCanExam()));
        result.setExamTip(tmp.getExamTip());
        return result;
    }
    private TmsPersonTaskMineForm toForm(TmsPersonTaskEntity person, TmsTaskEntity task, boolean detail) {
        TmsPersonTaskMineForm form = new TmsPersonTaskMineForm();
        form.setId(person.getId());
        form.setTaskId(task.getId());
        form.setTaskNo(StringUtil.isNotEmpty(person.getTaskNo()) ? person.getTaskNo() : task.getTaskNo());
        form.setSubject(task.getSubject());
        form.setCategory(task.getCategory());
        form.setTrainMode(task.getTrainMode());
        form.setEvalMode(task.getEvalMode());
        form.setStartTime(formatDate(task.getStartTime()));
        form.setEndTime(formatDate(task.getEndTime()));
        form.setCloseTime(formatDate(task.getCloseTime()));
        form.setPlaceName(task.getPlaceName());
        form.setBizStatus(person.getBizStatus());
        form.setSignTime(formatDate(person.getSignTime()));
        form.setSigned(person.getSignTime() != null);
        fillSignFlag(form, task, person);
        form.setLearnSeconds(person.getLearnSeconds());
        int learned = person.getLearnSeconds() == null ? 0 : person.getLearnSeconds();
        form.setLearnedSeconds(learned);
        BigDecimal hours = person.getHours();
        form.setRequiredHours(hours);
        int requiredSeconds = toRequiredSeconds(hours);
        form.setRequiredSeconds(requiredSeconds);
        form.setLearnStatus(resolveLearnStatus(learned, requiredSeconds));
        form.setExamScore(person.getExamScore());
        form.setPassFlag(person.getPassFlag());
        form.setPaperId(task.getPaperId());
        form.setPaperName(task.getPaperName());
        fillExamFlag(form, task, person);
        if (detail) {
            form.setKeyPoints(task.getKeyPoints());
        }
        return form;
    }
    private void fillExamFlag(TmsPersonTaskMineForm form, TmsTaskEntity task, TmsPersonTaskEntity person) {
        if (!"exam".equals(task.getEvalMode())) {
            form.setCanExam(false);
            form.setExamTip("本次培训无需在线考试");
            return;
        }
        if (StringUtil.isEmpty(task.getPaperId())) {
            form.setCanExam(false);
            form.setExamTip("未配置试卷");
            return;
        }
        if (person.getSignTime() == null) {
            form.setCanExam(false);
            form.setExamTip("请先签到后再考试");
            return;
        }
        if ("online".equals(task.getTrainMode())) {
            int required = toRequiredSeconds(person.getHours());
            int learned = person.getLearnSeconds() == null ? 0 : person.getLearnSeconds();
            if (required <= 0) {
                form.setCanExam(false);
                form.setExamTip("未配置课时,暂不能考试");
                return;
            }
            if (learned < required) {
                form.setCanExam(false);
                form.setExamTip("请先学满规定课时后再考试(已学 "
                        + formatDuration(learned) + " / 需学 " + formatDuration(required) + ")");
                return;
            }
        }
        Date now = new Date();
        if (task.getExamStart() != null && now.before(task.getExamStart())) {
            form.setCanExam(false);
            form.setExamTip("未到考试开始时间");
            return;
        }
        if (task.getExamEnd() != null && now.after(task.getExamEnd())) {
            form.setCanExam(false);
            form.setExamTip("已过考试结束时间");
            return;
        }
        form.setCanExam(true);
        form.setExamTip("");
    }
    private void fillSignFlag(TmsPersonTaskMineForm form, TmsTaskEntity task, TmsPersonTaskEntity person) {
        if (person.getSignTime() != null) {
            form.setCanSign(false);
            form.setSignTip("已签到");
            return;
        }
        String deny = resolveSignDenyReason(task, person);
        if (StringUtil.isNotEmpty(deny)) {
            form.setCanSign(false);
            form.setSignTip(deny);
            return;
        }
        form.setCanSign(true);
        form.setSignTip("");
    }
    /** 返回不可签到原因;空字符串表示可签到 */
    private String resolveSignDenyReason(TmsTaskEntity task, TmsPersonTaskEntity person) {
        if (STATUS_CANCELLED.equals(person.getBizStatus())) {
            return "任务已取消,不可签到";
        }
        if (task == null || !"published".equals(task.getPublishStatus())) {
            return "任务未发布,不可签到";
        }
        if (!allowOnlineSign(task.getTrainMode())) {
            return "该培训方式不支持在线签到,请扫码签到";
        }
        Date now = new Date();
        if (task.getStartTime() != null && now.before(task.getStartTime())) {
            return "未到培训开始时间,暂不可签到";
        }
        if (task.getCloseTime() != null && now.after(task.getCloseTime())) {
            return "培训已关闭,不可签到";
        }
        if (task.getEndTime() != null && now.after(task.getEndTime()) && task.getCloseTime() == null) {
            return "已过培训结束时间,不可签到";
        }
        return "";
    }
    private boolean allowOnlineSign(String trainMode) {
        if (StringUtil.isEmpty(trainMode)) {
            return false;
        }
        // 配置表优先:sign_rule=both 才允许在线;无配置时按字典约定 online 允许
        TmsTrainModeEntity mode = trainModeMapper.selectOne(
                new LambdaQueryWrapper<TmsTrainModeEntity>()
                        .eq(TmsTrainModeEntity::getModeCode, trainMode)
                        .last("LIMIT 1"));
        if (mode != null && StringUtil.isNotEmpty(mode.getSignRule())) {
            return "both".equalsIgnoreCase(mode.getSignRule());
        }
        return "online".equalsIgnoreCase(trainMode);
    }
    private int toRequiredSeconds(BigDecimal hours) {
        if (hours == null || hours.compareTo(BigDecimal.ZERO) <= 0) {
            return 0;
        }
        return hours.multiply(BigDecimal.valueOf(3600)).intValue();
    }
    private String resolveLearnStatus(int learned, int required) {
        if (required > 0 && learned >= required) {
            return "已学满";
        }
        if (learned > 0) {
            return "学习中";
        }
        return "未学习";
    }
    private String formatDuration(int seconds) {
        int h = seconds / 3600;
        int m = (seconds % 3600) / 60;
        int s = seconds % 60;
        if (h > 0) {
            return h + "小时" + m + "分";
        }
        if (m > 0) {
            return m + "分" + s + "秒";
        }
        return s + "秒";
    }
    private List<TmsPersonTaskFileForm> loadFiles(String taskId) {
        List<TmsTaskFileEntity> files = taskFileMapper.selectList(
                new LambdaQueryWrapper<TmsTaskFileEntity>()
                        .eq(TmsTaskFileEntity::getForeignId, taskId)
                        .orderByAsc(TmsTaskFileEntity::getCreatorTime));
        if (files.isEmpty()) {
            return Collections.emptyList();
        }
        List<String> materialIds = files.stream()
                .map(TmsTaskFileEntity::getMaterialId)
                .filter(StringUtil::isNotEmpty)
                .distinct()
                .collect(Collectors.toList());
        Map<String, TmsMaterialEntity> materialMap = new HashMap<>();
        if (!materialIds.isEmpty()) {
            for (TmsMaterialEntity material : materialMapper.selectBatchIds(materialIds)) {
                materialMap.put(material.getId(), material);
            }
        }
        List<TmsPersonTaskFileForm> list = new ArrayList<>();
        for (TmsTaskFileEntity file : files) {
            String url = extractUrl(file.getFileJson());
            String ext = extractExt(file.getFileJson(), file.getFileName());
            if ((StringUtil.isEmpty(url) || StringUtil.isEmpty(ext)) && StringUtil.isNotEmpty(file.getMaterialId())) {
                TmsMaterialEntity material = materialMap.get(file.getMaterialId());
                if (material != null) {
                    if (StringUtil.isEmpty(url)) {
                        url = extractUrl(material.getFileJson());
                    }
                    if (StringUtil.isEmpty(ext)) {
                        ext = extractExt(material.getFileJson(), material.getMaterialName());
                    }
                }
            }
            TmsPersonTaskFileForm item = new TmsPersonTaskFileForm();
            item.setId(file.getId());
            item.setName(file.getFileName());
            item.setUrl(url);
            item.setFileExt(ext);
            item.setPreviewType(resolvePreviewType(ext));
            list.add(item);
        }
        return list;
    }
    private String extractExt(String fileJson, String fileName) {
        try {
            if (StringUtil.isNotEmpty(fileJson)) {
                String text = fileJson.trim();
                if (text.startsWith("[")) {
                    List<Map<String, Object>> arr = OBJECT_MAPPER.readValue(text, new TypeReference<List<Map<String, Object>>>() {
                    });
                    if (arr != null && !arr.isEmpty() && arr.get(0).get("fileExtension") != null) {
                        return String.valueOf(arr.get(0).get("fileExtension")).toLowerCase();
                    }
                    if (arr != null && !arr.isEmpty() && arr.get(0).get("name") != null) {
                        fileName = String.valueOf(arr.get(0).get("name"));
                    }
                } else {
                    Map<String, Object> map = OBJECT_MAPPER.readValue(text, new TypeReference<Map<String, Object>>() {
                    });
                    if (map.get("fileExtension") != null) {
                        return String.valueOf(map.get("fileExtension")).toLowerCase();
                    }
                    if (map.get("name") != null) {
                        fileName = String.valueOf(map.get("name"));
                    }
                }
            }
        } catch (Exception ignored) {
            // ignore
        }
        if (StringUtil.isEmpty(fileName) || !fileName.contains(".")) {
            return "";
        }
        return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
    }
    private String resolvePreviewType(String ext) {
        if (StringUtil.isEmpty(ext)) {
            return "other";
        }
        String e = ext.toLowerCase();
        if (Set.of("jpg", "jpeg", "png", "gif", "webp", "bmp", "svg").contains(e)) {
            return "image";
        }
        if (Set.of("mp4", "webm", "ogg", "mov", "m4v").contains(e)) {
            return "video";
        }
        if (Set.of("mp3", "wav", "aac", "m4a", "flac").contains(e)) {
            return "audio";
        }
        if ("pdf".equals(e)) {
            return "pdf";
        }
        if (Set.of("doc", "docx", "xls", "xlsx", "ppt", "pptx").contains(e)) {
            return "office";
        }
        return "other";
    }
    private String extractUrl(String fileJson) {
        if (StringUtil.isEmpty(fileJson)) {
            return null;
        }
        try {
            String text = fileJson.trim();
            if (text.startsWith("[")) {
                List<Map<String, Object>> arr = OBJECT_MAPPER.readValue(text, new TypeReference<List<Map<String, Object>>>() {
                });
                if (arr == null || arr.isEmpty() || arr.get(0).get("url") == null) {
                    return null;
                }
                return String.valueOf(arr.get(0).get("url"));
            }
            Map<String, Object> map = OBJECT_MAPPER.readValue(text, new TypeReference<Map<String, Object>>() {
            });
            return map.get("url") == null ? null : String.valueOf(map.get("url"));
        } catch (Exception ex) {
            return null;
        }
    }
    private Map<String, TmsTaskEntity> loadTasks(List<TmsPersonTaskEntity> persons) {
        List<String> ids = persons.stream()
                .map(TmsPersonTaskEntity::getTaskId)
                .filter(StringUtil::isNotEmpty)
                .distinct()
                .collect(Collectors.toList());
        if (ids.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<String, TmsTaskEntity> map = new HashMap<>();
        for (TmsTaskEntity task : taskMapper.selectBatchIds(ids)) {
            map.put(task.getId(), task);
        }
        return map;
    }
    private boolean matchKeyword(TmsPersonTaskEntity person, TmsTaskEntity task, String keyword) {
        String no = person.getTaskNo() == null ? "" : person.getTaskNo();
        String taskNo = task.getTaskNo() == null ? "" : task.getTaskNo();
        String subject = task.getSubject() == null ? "" : task.getSubject();
        return no.contains(keyword) || taskNo.contains(keyword) || subject.contains(keyword);
    }
    private TmsPersonTaskEntity requireMine(String id) {
        if (StringUtil.isEmpty(id)) {
            throw new DataException("个人任务ID不能为空");
        }
        TmsPersonTaskEntity person = personTaskMapper.selectById(id);
        if (person == null) {
            throw new DataException("个人培训任务不存在");
        }
        if (!UserProvider.getLoginUserId().equals(person.getUserId())) {
            throw new DataException("只能查看自己的培训任务");
        }
        return person;
    }
    private String formatDate(Date date) {
        return date == null ? null : DateUtil.dateToString(date, FMT);
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPlaceServiceImpl.java
New file
@@ -0,0 +1,37 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jnpf.tmsEntity.TmsPlaceEntity;
import jnpf.tmsEntity.place.TmsPlaceOption;
import jnpf.tmsMapper.TmsPlaceMapper;
import jnpf.tmsService.TmsPlaceService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TmsPlaceServiceImpl implements TmsPlaceService {
    private final TmsPlaceMapper placeMapper;
    @Override
    public List<TmsPlaceOption> listOptions() {
        LambdaQueryWrapper<TmsPlaceEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsPlaceEntity::getEnabled, "1");
        qw.orderByAsc(TmsPlaceEntity::getPlaceNo);
        List<TmsPlaceEntity> rows = placeMapper.selectList(qw);
        List<TmsPlaceOption> list = new ArrayList<>(rows.size());
        for (TmsPlaceEntity row : rows) {
            TmsPlaceOption opt = new TmsPlaceOption();
            opt.setId(row.getId());
            opt.setPlaceNo(row.getPlaceNo());
            opt.setFullName(row.getPlaceName());
            opt.setPlaceGroup(row.getPlaceGroup());
            list.add(opt);
        }
        return list;
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsQuestionServiceImpl.java
New file
@@ -0,0 +1,378 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jnpf.exception.DataException;
import jnpf.permission.UserApi;
import jnpf.permission.entity.UserEntity;
import jnpf.tmsEntity.TmsQuestionBankEntity;
import jnpf.tmsEntity.TmsQuestionEntity;
import jnpf.tmsEntity.TmsQuestionOptionEntity;
import jnpf.tmsEntity.question.TmsQuestionBankOption;
import jnpf.tmsEntity.question.TmsQuestionForm;
import jnpf.tmsEntity.question.TmsQuestionOptionForm;
import jnpf.tmsEntity.question.TmsQuestionQuery;
import jnpf.tmsMapper.TmsQuestionBankMapper;
import jnpf.tmsMapper.TmsQuestionMapper;
import jnpf.tmsMapper.TmsQuestionOptionMapper;
import jnpf.tmsService.TmsQuestionService;
import jnpf.util.DateUtil;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class TmsQuestionServiceImpl implements TmsQuestionService {
    private final TmsQuestionMapper questionMapper;
    private final TmsQuestionOptionMapper optionMapper;
    private final TmsQuestionBankMapper bankMapper;
    private final UserApi userApi;
    @Override
    public List<TmsQuestionBankOption> listBanks() {
        LambdaQueryWrapper<TmsQuestionBankEntity> qw = new LambdaQueryWrapper<>();
        qw.orderByDesc(TmsQuestionBankEntity::getCreatorTime);
        List<TmsQuestionBankEntity> list = bankMapper.selectList(qw);
        List<TmsQuestionBankOption> result = new ArrayList<>(list.size());
        for (TmsQuestionBankEntity bank : list) {
            TmsQuestionBankOption opt = new TmsQuestionBankOption();
            opt.setId(bank.getId());
            opt.setFullName(bank.getBankName());
            result.add(opt);
        }
        return result;
    }
    @Override
    public List<Map<String, String>> listAdmins() {
        QueryWrapper<TmsQuestionEntity> qw = new QueryWrapper<>();
        qw.select("DISTINCT admin_user_id");
        qw.isNotNull("admin_user_id");
        qw.ne("admin_user_id", "");
        List<TmsQuestionEntity> rows = questionMapper.selectList(qw);
        List<String> userIds = rows.stream()
                .map(TmsQuestionEntity::getAdminUserId)
                .filter(StringUtil::isNotEmpty)
                .distinct()
                .collect(Collectors.toList());
        if (userIds.isEmpty()) {
            return Collections.emptyList();
        }
        Map<String, String> nameMap = loadUserNames(userIds);
        List<Map<String, String>> result = new ArrayList<>();
        for (String userId : userIds) {
            Map<String, String> item = new HashMap<>(2);
            item.put("id", userId);
            item.put("fullName", nameMap.getOrDefault(userId, userId));
            result.add(item);
        }
        return result;
    }
    @Override
    public List<TmsQuestionForm> getList(TmsQuestionQuery query) {
        LambdaQueryWrapper<TmsQuestionEntity> qw = new LambdaQueryWrapper<>();
        if (StringUtil.isNotEmpty(query.getBankId())) {
            qw.eq(TmsQuestionEntity::getBankId, query.getBankId());
        }
        if (StringUtil.isNotEmpty(query.getQuestionType())) {
            qw.eq(TmsQuestionEntity::getQuestionType, query.getQuestionType());
        }
        if (StringUtil.isNotEmpty(query.getBizStatus())) {
            qw.eq(TmsQuestionEntity::getBizStatus, query.getBizStatus());
        }
        if (StringUtil.isNotEmpty(query.getAdminUserId())) {
            qw.eq(TmsQuestionEntity::getAdminUserId, query.getAdminUserId());
        }
        if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) {
            String kw = query.getKeyword().trim();
            qw.and(w -> w.like(TmsQuestionEntity::getStem, kw)
                    .or()
                    .like(TmsQuestionEntity::getQuestionNo, kw));
        }
        qw.orderByDesc(TmsQuestionEntity::getCreatorTime);
        Page<TmsQuestionEntity> page = new Page<>(query.getCurrentPage(), query.getPageSize());
        Page<TmsQuestionEntity> result = questionMapper.selectPage(page, qw);
        List<TmsQuestionEntity> records = result.getRecords();
        Map<String, String> bankNames = loadBankNames(
                records.stream().map(TmsQuestionEntity::getBankId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()));
        Map<String, String> userNames = loadUserNames(
                records.stream().map(TmsQuestionEntity::getAdminUserId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()));
        List<TmsQuestionForm> list = new ArrayList<>(records.size());
        for (TmsQuestionEntity entity : records) {
            list.add(toForm(entity, bankNames, userNames, false));
        }
        query.setData(list, result.getTotal());
        return list;
    }
    @Override
    public TmsQuestionForm getInfo(String id) {
        TmsQuestionEntity entity = questionMapper.selectById(id);
        if (entity == null) {
            throw new DataException("试题不存在");
        }
        Map<String, String> bankNames = loadBankNames(Collections.singletonList(entity.getBankId()));
        Map<String, String> userNames = loadUserNames(
                StringUtil.isEmpty(entity.getAdminUserId())
                        ? Collections.emptyList()
                        : Collections.singletonList(entity.getAdminUserId()));
        return toForm(entity, bankNames, userNames, true);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String create(TmsQuestionForm form) {
        validateForm(form);
        String id = RandomUtil.uuId();
        Date now = new Date();
        String userId = UserProvider.getLoginUserId();
        TmsQuestionEntity entity = new TmsQuestionEntity();
        entity.setId(id);
        entity.setQuestionNo(nextQuestionNo());
        entity.setBankId(form.getBankId());
        entity.setQuestionType(form.getQuestionType());
        entity.setDifficulty(form.getDifficulty());
        entity.setSourceType(form.getSourceType());
        entity.setStem(form.getStem());
        entity.setAnalysis(form.getAnalysis());
        entity.setAdminUserId(StringUtil.isNotEmpty(form.getAdminUserId()) ? form.getAdminUserId() : userId);
        entity.setBizStatus(StringUtil.isNotEmpty(form.getBizStatus()) ? form.getBizStatus() : "closed");
        entity.setCreatorUserId(userId);
        entity.setCreatorTime(now);
        questionMapper.insert(entity);
        saveOptions(id, form.getOptions());
        refreshBankCount(form.getBankId());
        return id;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(String id, TmsQuestionForm form) {
        TmsQuestionEntity entity = questionMapper.selectById(id);
        if (entity == null) {
            throw new DataException("试题不存在");
        }
        validateForm(form);
        String oldBankId = entity.getBankId();
        entity.setBankId(form.getBankId());
        entity.setQuestionType(form.getQuestionType());
        entity.setDifficulty(form.getDifficulty());
        entity.setSourceType(form.getSourceType());
        entity.setStem(form.getStem());
        entity.setAnalysis(form.getAnalysis());
        if (StringUtil.isNotEmpty(form.getAdminUserId())) {
            entity.setAdminUserId(form.getAdminUserId());
        }
        if (StringUtil.isNotEmpty(form.getBizStatus())) {
            entity.setBizStatus(form.getBizStatus());
        }
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        questionMapper.updateById(entity);
        // 选项全量替换
        LambdaQueryWrapper<TmsQuestionOptionEntity> delQw = new LambdaQueryWrapper<>();
        delQw.eq(TmsQuestionOptionEntity::getForeignId, id);
        optionMapper.delete(delQw);
        saveOptions(id, form.getOptions());
        refreshBankCount(oldBankId);
        if (!Objects.equals(oldBankId, form.getBankId())) {
            refreshBankCount(form.getBankId());
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void invalidate(String id) {
        TmsQuestionEntity entity = questionMapper.selectById(id);
        if (entity == null) {
            return;
        }
        String bankId = entity.getBankId();
        // TableLogic:写 f_delete_mark,列表/详情自动过滤
        questionMapper.deleteById(id);
        LambdaQueryWrapper<TmsQuestionOptionEntity> delQw = new LambdaQueryWrapper<>();
        delQw.eq(TmsQuestionOptionEntity::getForeignId, id);
        optionMapper.delete(delQw);
        refreshBankCount(bankId);
    }
    private void validateForm(TmsQuestionForm form) {
        if (form == null) {
            throw new DataException("参数不能为空");
        }
        if (StringUtil.isEmpty(form.getBankId())) {
            throw new DataException("请选择所属题库");
        }
        if (StringUtil.isEmpty(form.getQuestionType())) {
            throw new DataException("请选择题型");
        }
        if (StringUtil.isEmpty(form.getStem())) {
            throw new DataException("请填写题干");
        }
        TmsQuestionBankEntity bank = bankMapper.selectById(form.getBankId());
        if (bank == null) {
            throw new DataException("题库不存在或已删除");
        }
    }
    private void saveOptions(String questionId, List<TmsQuestionOptionForm> options) {
        if (options == null || options.isEmpty()) {
            return;
        }
        Date now = new Date();
        String userId = UserProvider.getLoginUserId();
        int i = 0;
        for (TmsQuestionOptionForm opt : options) {
            TmsQuestionOptionEntity row = new TmsQuestionOptionEntity();
            row.setId(RandomUtil.uuId());
            row.setForeignId(questionId);
            row.setSortNo(opt.getSortNo() != null ? opt.getSortNo() : ++i);
            row.setOptionLabel(opt.getOptionLabel());
            row.setOptionContent(opt.getOptionContent());
            row.setIsCorrect(StringUtil.isNotEmpty(opt.getIsCorrect()) ? opt.getIsCorrect() : "0");
            row.setCreatorUserId(userId);
            row.setCreatorTime(now);
            optionMapper.insert(row);
        }
    }
    /** 按题库下未删除试题实计回写 question_count(创建 +1 / 删除 -1 / 换库两边重算) */
    private void refreshBankCount(String bankId) {
        if (StringUtil.isEmpty(bankId)) {
            return;
        }
        TmsQuestionBankEntity bank = bankMapper.selectById(bankId);
        if (bank == null) {
            return;
        }
        LambdaQueryWrapper<TmsQuestionEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsQuestionEntity::getBankId, bankId);
        Long count = questionMapper.selectCount(qw);
        bank.setQuestionCount(count == null ? 0 : count.intValue());
        bank.setLastModifyUserId(UserProvider.getLoginUserId());
        bank.setLastModifyTime(new Date());
        bankMapper.updateById(bank);
    }
    /**
     * 试题编号:年份后两位 + 4 位流水,如 260001。
     * 按当年最大号 +1,含已删记录,避免流水回退。
     */
    private synchronized String nextQuestionNo() {
        String prefix = String.format("%02d", LocalDate.now().getYear() % 100);
        String maxNo = questionMapper.selectMaxQuestionNo(prefix);
        int next = 1;
        if (StringUtil.isNotEmpty(maxNo) && maxNo.length() >= 6 && maxNo.startsWith(prefix)) {
            try {
                next = Integer.parseInt(maxNo.substring(prefix.length())) + 1;
            } catch (NumberFormatException ignored) {
                next = 1;
            }
        }
        if (next > 9999) {
            throw new DataException("当年试题编号已用尽(" + prefix + "9999)");
        }
        return prefix + String.format("%04d", next);
    }
    private TmsQuestionForm toForm(
            TmsQuestionEntity entity,
            Map<String, String> bankNames,
            Map<String, String> userNames,
            boolean withOptions) {
        TmsQuestionForm form = new TmsQuestionForm();
        form.setId(entity.getId());
        form.setQuestionNo(entity.getQuestionNo());
        form.setBankId(entity.getBankId());
        form.setBankName(bankNames.get(entity.getBankId()));
        form.setQuestionType(entity.getQuestionType());
        form.setDifficulty(entity.getDifficulty());
        form.setSourceType(entity.getSourceType());
        form.setStem(entity.getStem());
        form.setAnalysis(entity.getAnalysis());
        form.setAdminUserId(entity.getAdminUserId());
        form.setAdminUserName(userNames.get(entity.getAdminUserId()));
        form.setBizStatus(entity.getBizStatus());
        form.setCreatorTime(entity.getCreatorTime() == null
                ? null
                : DateUtil.dateToString(entity.getCreatorTime(), "yyyy-MM-dd HH:mm:ss"));
        if (withOptions) {
            form.setOptions(loadOptions(entity.getId()));
        }
        return form;
    }
    private List<TmsQuestionOptionForm> loadOptions(String questionId) {
        LambdaQueryWrapper<TmsQuestionOptionEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsQuestionOptionEntity::getForeignId, questionId);
        qw.orderByAsc(TmsQuestionOptionEntity::getSortNo);
        List<TmsQuestionOptionEntity> rows = optionMapper.selectList(qw);
        List<TmsQuestionOptionForm> list = new ArrayList<>(rows.size());
        for (TmsQuestionOptionEntity row : rows) {
            TmsQuestionOptionForm opt = new TmsQuestionOptionForm();
            opt.setId(row.getId());
            opt.setSortNo(row.getSortNo());
            opt.setOptionLabel(row.getOptionLabel());
            opt.setOptionContent(row.getOptionContent());
            opt.setIsCorrect(row.getIsCorrect());
            list.add(opt);
        }
        return list;
    }
    private Map<String, String> loadBankNames(List<String> bankIds) {
        if (bankIds == null || bankIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<TmsQuestionBankEntity> banks = bankMapper.selectBatchIds(bankIds);
        Map<String, String> map = new HashMap<>();
        for (TmsQuestionBankEntity bank : banks) {
            map.put(bank.getId(), bank.getBankName());
        }
        return map;
    }
    private Map<String, String> loadUserNames(List<String> userIds) {
        if (userIds == null || userIds.isEmpty()) {
            return Collections.emptyMap();
        }
        try {
            List<UserEntity> users = userApi.getUserName(userIds);
            if (users == null || users.isEmpty()) {
                return Collections.emptyMap();
            }
            return users.stream()
                    .filter(Objects::nonNull)
                    .collect(Collectors.toMap(UserEntity::getId, UserEntity::getRealName, (a, b) -> a));
        } catch (Exception ex) {
            return Collections.emptyMap();
        }
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsRecordServiceImpl.java
New file
@@ -0,0 +1,388 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jnpf.exception.DataException;
import jnpf.permission.UserApi;
import jnpf.permission.entity.UserEntity;
import jnpf.tmsEntity.TmsMaterialEntity;
import jnpf.tmsEntity.TmsPersonTaskEntity;
import jnpf.tmsEntity.TmsRecordEntity;
import jnpf.tmsEntity.TmsTaskEntity;
import jnpf.tmsEntity.TmsTaskFileEntity;
import jnpf.tmsEntity.record.TmsRecordAttachmentForm;
import jnpf.tmsEntity.record.TmsRecordForm;
import jnpf.tmsEntity.record.TmsRecordParticipantForm;
import jnpf.tmsEntity.record.TmsRecordQuery;
import jnpf.tmsEntity.record.TmsRecordSignForm;
import jnpf.tmsMapper.TmsMaterialMapper;
import jnpf.tmsMapper.TmsPersonTaskMapper;
import jnpf.tmsMapper.TmsRecordMapper;
import jnpf.tmsMapper.TmsTaskFileMapper;
import jnpf.tmsMapper.TmsTaskMapper;
import jnpf.tmsService.TmsRecordService;
import jnpf.util.DateUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class TmsRecordServiceImpl implements TmsRecordService {
    private static final String FMT = "yyyy-MM-dd HH:mm:ss";
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private final TmsRecordMapper recordMapper;
    private final TmsTaskMapper taskMapper;
    private final TmsTaskFileMapper taskFileMapper;
    private final TmsMaterialMapper materialMapper;
    private final TmsPersonTaskMapper personTaskMapper;
    private final UserApi userApi;
    @Override
    public List<TmsRecordForm> getList(TmsRecordQuery query) {
        LambdaQueryWrapper<TmsRecordEntity> qw = new LambdaQueryWrapper<>();
        applyListMode(qw, query);
        if (StringUtil.isNotEmpty(query.getRecordNo())) {
            qw.like(TmsRecordEntity::getRecordNo, query.getRecordNo().trim());
        }
        if (StringUtil.isNotEmpty(query.getCategory())) {
            qw.like(TmsRecordEntity::getCategory, query.getCategory().trim());
        }
        if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) {
            String kw = query.getKeyword().trim();
            qw.and(w -> w.like(TmsRecordEntity::getRecordNo, kw)
                    .or().like(TmsRecordEntity::getSubject, kw));
        }
        qw.orderByDesc(TmsRecordEntity::getStartTime).orderByDesc(TmsRecordEntity::getCreatorTime);
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        Page<TmsRecordEntity> page = recordMapper.selectPage(new Page<>(current, size), qw);
        List<TmsRecordEntity> rows = page.getRecords();
        Map<String, String> names = loadUserNames(rows.stream()
                .map(TmsRecordEntity::getTeacherId)
                .filter(StringUtil::isNotEmpty)
                .distinct()
                .collect(Collectors.toList()));
        List<TmsRecordForm> list = new ArrayList<>(rows.size());
        String trainerFilter = StringUtil.isNotEmpty(query.getTrainerName()) ? query.getTrainerName().trim() : null;
        for (TmsRecordEntity row : rows) {
            TmsRecordForm form = toListForm(row, names);
            if (trainerFilter != null) {
                String name = form.getTrainerName() == null ? "" : form.getTrainerName();
                if (!name.contains(trainerFilter)) {
                    continue;
                }
            }
            list.add(form);
        }
        query.setData(list, page.getTotal());
        return list;
    }
    @Override
    public TmsRecordForm getInfo(String id) {
        TmsRecordEntity entity = requireOne(id);
        Map<String, String> teacherNames = loadUserNames(
                StringUtil.isEmpty(entity.getTeacherId())
                        ? Collections.emptyList()
                        : Collections.singletonList(entity.getTeacherId()));
        TmsRecordForm form = toListForm(entity, teacherNames);
        TmsTaskEntity task = StringUtil.isEmpty(entity.getTaskId()) ? null : taskMapper.selectById(entity.getTaskId());
        if (task != null) {
            form.setKeyPoints(task.getKeyPoints());
            form.setPlaceName(task.getPlaceName());
            if (StringUtil.isEmpty(form.getTrainType()) && StringUtil.isNotEmpty(task.getTrainType())) {
                form.setTrainType(task.getTrainType());
            }
            if (StringUtil.isEmpty(form.getTrainMode()) && StringUtil.isNotEmpty(task.getTrainMode())) {
                form.setTrainMode(task.getTrainMode());
            }
        }
        List<TmsPersonTaskEntity> persons = StringUtil.isEmpty(entity.getTaskId())
                ? Collections.emptyList()
                : personTaskMapper.selectList(new LambdaQueryWrapper<TmsPersonTaskEntity>()
                .eq(TmsPersonTaskEntity::getTaskId, entity.getTaskId())
                .orderByAsc(TmsPersonTaskEntity::getCreatorTime));
        Map<String, String> userNames = loadUserNames(persons.stream()
                .map(TmsPersonTaskEntity::getUserId)
                .filter(StringUtil::isNotEmpty)
                .distinct()
                .collect(Collectors.toList()));
        List<TmsRecordParticipantForm> participants = new ArrayList<>(persons.size());
        List<TmsRecordSignForm> signs = new ArrayList<>();
        List<String> traineeNames = new ArrayList<>();
        for (TmsPersonTaskEntity p : persons) {
            String userName = userNames.getOrDefault(p.getUserId(), p.getUserId());
            traineeNames.add(userName);
            TmsRecordParticipantForm pf = new TmsRecordParticipantForm();
            pf.setId(p.getId());
            pf.setUserId(p.getUserId());
            pf.setUserName(userName);
            pf.setSignDate(formatDate(p.getSignTime()));
            pf.setExamScore(p.getExamScore());
            pf.setPassFlag(p.getPassFlag());
            participants.add(pf);
            if (p.getSignTime() != null) {
                TmsRecordSignForm sf = new TmsRecordSignForm();
                sf.setId(p.getId());
                sf.setUserName(userName);
                sf.setSignTime(formatDate(p.getSignTime()));
                sf.setSignMode(p.getSignMode());
                sf.setGuestFlag(p.getGuestFlag());
                signs.add(sf);
            }
        }
        form.setParticipants(participants);
        form.setSignList(signs);
        form.setTrainees(String.join(",", traineeNames));
        form.setAttachments(loadAttachments(entity.getTaskId()));
        form.setRemark(entity.getRemark());
        return form;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(String id, TmsRecordForm form) {
        TmsRecordEntity entity = requireOne(id);
        if (form == null) {
            throw new DataException("表单不能为空");
        }
        if (StringUtil.isNotEmpty(form.getSubject())) {
            entity.setSubject(form.getSubject().trim());
        }
        if (form.getCategory() != null) {
            entity.setCategory(form.getCategory());
        }
        if (form.getTrainType() != null) {
            entity.setTrainType(form.getTrainType());
        }
        if (form.getTrainMode() != null) {
            entity.setTrainMode(form.getTrainMode());
        }
        if (form.getEvalMode() != null) {
            entity.setEvalMode(form.getEvalMode());
        }
        entity.setRemark(form.getRemark());
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        recordMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int archive(List<String> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new DataException("请选择要归档的记录");
        }
        int count = 0;
        Date now = new Date();
        String userId = UserProvider.getLoginUserId();
        for (String id : ids) {
            if (StringUtil.isEmpty(id)) {
                continue;
            }
            TmsRecordEntity entity = recordMapper.selectById(id);
            if (entity == null) {
                continue;
            }
            entity.setArchiveStatus("archived");
            entity.setLastModifyUserId(userId);
            entity.setLastModifyTime(now);
            recordMapper.updateById(entity);
            count++;
        }
        return count;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete(List<String> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new DataException("请选择要删除的记录");
        }
        for (String id : ids) {
            if (StringUtil.isEmpty(id)) {
                continue;
            }
            recordMapper.deleteById(id);
        }
    }
    private void applyListMode(LambdaQueryWrapper<TmsRecordEntity> qw, TmsRecordQuery query) {
        String archiveStatus = query.getArchiveStatus();
        if (StringUtil.isNotEmpty(archiveStatus)) {
            qw.eq(TmsRecordEntity::getArchiveStatus, archiveStatus);
            return;
        }
        String mode = query.getListMode();
        if ("ready".equals(mode)) {
            qw.eq(TmsRecordEntity::getArchiveStatus, "ready");
        } else if ("archived".equals(mode)) {
            qw.eq(TmsRecordEntity::getArchiveStatus, "archived");
        } else {
            // 主列表:待归档 / 进行中
            qw.and(w -> w.eq(TmsRecordEntity::getArchiveStatus, "pending")
                    .or().isNull(TmsRecordEntity::getArchiveStatus));
        }
    }
    private TmsRecordForm toListForm(TmsRecordEntity entity, Map<String, String> names) {
        TmsRecordForm form = new TmsRecordForm();
        form.setId(entity.getId());
        form.setRecordNo(entity.getRecordNo());
        form.setTaskId(entity.getTaskId());
        form.setTaskNo(entity.getTaskNo());
        form.setCategory(entity.getCategory());
        form.setSubject(entity.getSubject());
        form.setTeacherId(entity.getTeacherId());
        form.setTrainerName(names.getOrDefault(entity.getTeacherId(),
                StringUtil.isEmpty(entity.getTeacherId()) ? "-" : entity.getTeacherId()));
        form.setTrainType(StringUtil.isNotEmpty(entity.getTrainType())
                ? entity.getTrainType()
                : entity.getTrainMode());
        form.setTrainMode(entity.getTrainMode());
        form.setEvalMode(entity.getEvalMode());
        form.setStartTime(formatDate(entity.getStartTime()));
        form.setEndTime(formatDate(entity.getEndTime()));
        form.setCloseTime(formatDate(entity.getCloseTime()));
        form.setArchiveStatus(entity.getArchiveStatus());
        form.setSignRate(entity.getSignRate());
        form.setPassRate(entity.getPassRate());
        form.setRemark(entity.getRemark());
        return form;
    }
    private List<TmsRecordAttachmentForm> loadAttachments(String taskId) {
        if (StringUtil.isEmpty(taskId)) {
            return Collections.emptyList();
        }
        List<TmsTaskFileEntity> files = taskFileMapper.selectList(
                new LambdaQueryWrapper<TmsTaskFileEntity>()
                        .eq(TmsTaskFileEntity::getForeignId, taskId)
                        .orderByAsc(TmsTaskFileEntity::getCreatorTime));
        if (files.isEmpty()) {
            return Collections.emptyList();
        }
        List<String> materialIds = files.stream()
                .map(TmsTaskFileEntity::getMaterialId)
                .filter(StringUtil::isNotEmpty)
                .distinct()
                .collect(Collectors.toList());
        Map<String, TmsMaterialEntity> materialMap = new HashMap<>();
        if (!materialIds.isEmpty()) {
            for (TmsMaterialEntity material : materialMapper.selectBatchIds(materialIds)) {
                materialMap.put(material.getId(), material);
            }
        }
        List<TmsRecordAttachmentForm> list = new ArrayList<>();
        for (TmsTaskFileEntity file : files) {
            String url = extractUrl(file.getFileJson());
            if (StringUtil.isEmpty(url) && StringUtil.isNotEmpty(file.getMaterialId())) {
                TmsMaterialEntity material = materialMap.get(file.getMaterialId());
                if (material != null) {
                    url = extractUrl(material.getFileJson());
                }
            }
            TmsRecordAttachmentForm item = new TmsRecordAttachmentForm();
            item.setId(file.getId());
            item.setName(file.getFileName());
            item.setUrl(url);
            list.add(item);
        }
        return list;
    }
    private String extractUrl(String fileJson) {
        if (StringUtil.isEmpty(fileJson)) {
            return null;
        }
        try {
            String text = fileJson.trim();
            if (text.startsWith("[")) {
                List<Map<String, Object>> arr = OBJECT_MAPPER.readValue(text,
                        new TypeReference<List<Map<String, Object>>>() {
                        });
                if (arr == null || arr.isEmpty()) {
                    return null;
                }
                Object url = arr.get(0).get("url");
                return url == null ? null : String.valueOf(url);
            }
            Map<String, Object> map = OBJECT_MAPPER.readValue(text, new TypeReference<Map<String, Object>>() {
            });
            Object url = map.get("url");
            return url == null ? null : String.valueOf(url);
        } catch (Exception ex) {
            return null;
        }
    }
    private Map<String, String> loadUserNames(List<String> userIds) {
        if (userIds == null || userIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<String> ids = userIds.stream().filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList());
        if (ids.isEmpty()) {
            return Collections.emptyMap();
        }
        try {
            List<UserEntity> users = userApi.getUserName(ids);
            if (users == null || users.isEmpty()) {
                return Collections.emptyMap();
            }
            Map<String, String> map = new LinkedHashMap<>();
            for (UserEntity user : users) {
                if (user == null || StringUtil.isEmpty(user.getId())) {
                    continue;
                }
                String name = StringUtil.isNotEmpty(user.getRealName()) ? user.getRealName() : user.getAccount();
                map.put(user.getId(), name);
            }
            return map;
        } catch (Exception ex) {
            return Collections.emptyMap();
        }
    }
    private TmsRecordEntity requireOne(String id) {
        if (StringUtil.isEmpty(id)) {
            throw new DataException("记录ID不能为空");
        }
        TmsRecordEntity entity = recordMapper.selectById(id);
        if (entity == null) {
            throw new DataException("培训记录不存在");
        }
        return entity;
    }
    private String formatDate(Date date) {
        return date == null ? null : DateUtil.dateToString(date, FMT);
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSelfTestServiceImpl.java
New file
@@ -0,0 +1,484 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jnpf.exception.DataException;
import jnpf.tmsEntity.TmsQuestionBankEntity;
import jnpf.tmsEntity.TmsQuestionEntity;
import jnpf.tmsEntity.TmsQuestionOptionEntity;
import jnpf.tmsEntity.TmsSelfTestEntity;
import jnpf.tmsEntity.TmsSelfTestItemEntity;
import jnpf.tmsEntity.selftest.TmsSelfTestOptionVO;
import jnpf.tmsEntity.selftest.TmsSelfTestPaperVO;
import jnpf.tmsEntity.selftest.TmsSelfTestQuery;
import jnpf.tmsEntity.selftest.TmsSelfTestQuestionVO;
import jnpf.tmsEntity.selftest.TmsSelfTestRecordVO;
import jnpf.tmsEntity.selftest.TmsSelfTestStartForm;
import jnpf.tmsEntity.selftest.TmsSelfTestSubmitForm;
import jnpf.tmsEntity.selftest.TmsSelfTestSubmitResultVO;
import jnpf.tmsEntity.selftest.TmsSelfTestTypeSetting;
import jnpf.tmsMapper.TmsQuestionBankMapper;
import jnpf.tmsMapper.TmsQuestionMapper;
import jnpf.tmsMapper.TmsQuestionOptionMapper;
import jnpf.tmsMapper.TmsSelfTestItemMapper;
import jnpf.tmsMapper.TmsSelfTestMapper;
import jnpf.tmsService.TmsSelfTestService;
import jnpf.util.DateUtil;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class TmsSelfTestServiceImpl implements TmsSelfTestService {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    /** 自我检测 UI 难度 → 试题 difficulty 值 */
    private static final Map<String, List<String>> DIFFICULTY_MAP = new HashMap<>();
    static {
        DIFFICULTY_MAP.put("veryEasy", Arrays.asList("easy", "veryEasy"));
        DIFFICULTY_MAP.put("easier", Arrays.asList("easy", "easier"));
        DIFFICULTY_MAP.put("normal", Collections.singletonList("normal"));
        DIFFICULTY_MAP.put("harder", Arrays.asList("hard", "harder"));
        DIFFICULTY_MAP.put("veryHard", Arrays.asList("hard", "veryHard"));
    }
    private final TmsSelfTestMapper selfTestMapper;
    private final TmsSelfTestItemMapper selfTestItemMapper;
    private final TmsQuestionMapper questionMapper;
    private final TmsQuestionOptionMapper optionMapper;
    private final TmsQuestionBankMapper bankMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public TmsSelfTestPaperVO start(TmsSelfTestStartForm form) {
        if (form == null || StringUtil.isEmpty(form.getBankId())) {
            throw new DataException("请选择题库");
        }
        TmsQuestionBankEntity bank = bankMapper.selectById(form.getBankId());
        if (bank == null) {
            throw new DataException("题库不存在或已删除");
        }
        int singleCnt = countOf(form.getSingle());
        int multiCnt = countOf(form.getMulti());
        int judgeCnt = countOf(form.getJudge());
        if (singleCnt + multiCnt + judgeCnt <= 0) {
            throw new DataException("请至少设置一种题型的抽题数量");
        }
        List<TmsQuestionEntity> drawn = new ArrayList<>();
        drawn.addAll(drawQuestions(form.getBankId(), "single", form.getSingle()));
        drawn.addAll(drawQuestions(form.getBankId(), "multi", form.getMulti()));
        drawn.addAll(drawQuestions(form.getBankId(), "judge", form.getJudge()));
        if (drawn.isEmpty()) {
            throw new DataException("当前条件下没有可抽题目,请调整数量或难度");
        }
        Map<String, List<TmsQuestionOptionEntity>> optionMap = loadOptions(
                drawn.stream().map(TmsQuestionEntity::getId).collect(Collectors.toList()));
        String userId = UserProvider.getLoginUserId();
        Date now = new Date();
        String paperId = RandomUtil.uuId();
        TmsSelfTestEntity entity = new TmsSelfTestEntity();
        entity.setId(paperId);
        entity.setBankId(bank.getId());
        entity.setBankName(StringUtil.isNotEmpty(form.getBankName()) ? form.getBankName() : bank.getBankName());
        entity.setUserId(userId);
        entity.setSingleCount(singleCnt);
        entity.setMultiCount(multiCnt);
        entity.setJudgeCount(judgeCnt);
        entity.setSingleDifficulty(diffOf(form.getSingle()));
        entity.setMultiDifficulty(diffOf(form.getMulti()));
        entity.setJudgeDifficulty(diffOf(form.getJudge()));
        entity.setTotalCount(drawn.size());
        entity.setStartTime(now);
        entity.setTestStatus("doing");
        entity.setCreatorUserId(userId);
        entity.setCreatorTime(now);
        selfTestMapper.insert(entity);
        List<TmsSelfTestQuestionVO> questions = new ArrayList<>(drawn.size());
        int sort = 0;
        for (TmsQuestionEntity q : drawn) {
            List<TmsQuestionOptionEntity> opts = optionMap.getOrDefault(q.getId(), Collections.emptyList());
            List<TmsSelfTestOptionVO> optionVos = toOptionVos(opts);
            String correct = opts.stream()
                    .filter(o -> "1".equals(o.getIsCorrect()))
                    .map(TmsQuestionOptionEntity::getOptionLabel)
                    .sorted()
                    .collect(Collectors.joining(","));
            TmsSelfTestItemEntity item = new TmsSelfTestItemEntity();
            item.setId(RandomUtil.uuId());
            item.setForeignId(paperId);
            item.setQuestionId(q.getId());
            item.setQuestionNo(q.getQuestionNo());
            item.setQuestionType(q.getQuestionType());
            item.setDifficulty(q.getDifficulty());
            item.setStem(q.getStem());
            item.setOptionsJson(toJson(optionVos));
            item.setCorrectAnswer(correct);
            item.setSortNo(++sort);
            item.setCreatorUserId(userId);
            item.setCreatorTime(now);
            selfTestItemMapper.insert(item);
            TmsSelfTestQuestionVO vo = new TmsSelfTestQuestionVO();
            vo.setId(q.getId());
            vo.setQuestionNo(q.getQuestionNo());
            vo.setQuestionType(q.getQuestionType());
            vo.setDifficulty(q.getDifficulty());
            vo.setStem(q.getStem());
            vo.setOptions(optionVos);
            questions.add(vo);
        }
        TmsSelfTestPaperVO paper = new TmsSelfTestPaperVO();
        paper.setPaperId(paperId);
        paper.setBankId(entity.getBankId());
        paper.setBankName(entity.getBankName());
        paper.setQuestions(questions);
        return paper;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveAnswers(String id, TmsSelfTestSubmitForm form) {
        TmsSelfTestEntity entity = requireDoingPaper(id, "无权暂存该检测");
        String userId = UserProvider.getLoginUserId();
        List<TmsSelfTestItemEntity> items = listItems(id);
        Map<String, Object> answers = form != null && form.getAnswers() != null
                ? form.getAnswers()
                : Collections.emptyMap();
        Date now = new Date();
        for (TmsSelfTestItemEntity item : items) {
            // 仅更新前端已作答的题,未传的保持原值,方便增量暂存
            if (!answers.containsKey(item.getQuestionId())) {
                continue;
            }
            String userAnswer = normalizeAnswer(answers.get(item.getQuestionId()), item.getQuestionType());
            item.setUserAnswer(userAnswer);
            item.setIsRight(null);
            item.setLastModifyUserId(userId);
            item.setLastModifyTime(now);
            selfTestItemMapper.updateById(item);
        }
        entity.setLastModifyUserId(userId);
        entity.setLastModifyTime(now);
        selfTestMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public TmsSelfTestSubmitResultVO submit(String id, TmsSelfTestSubmitForm form) {
        TmsSelfTestEntity entity = requireDoingPaper(id, "无权提交该检测");
        String userId = UserProvider.getLoginUserId();
        List<TmsSelfTestItemEntity> items = listItems(id);
        Map<String, Object> answers = form != null && form.getAnswers() != null
                ? form.getAnswers()
                : Collections.emptyMap();
        Date now = new Date();
        int correct = 0;
        for (TmsSelfTestItemEntity item : items) {
            String userAnswer = normalizeAnswer(answers.get(item.getQuestionId()), item.getQuestionType());
            boolean right = Objects.equals(userAnswer, normalizeCorrect(item.getCorrectAnswer()));
            item.setUserAnswer(userAnswer);
            item.setIsRight(right ? "1" : "0");
            item.setLastModifyUserId(userId);
            item.setLastModifyTime(now);
            selfTestItemMapper.updateById(item);
            if (right) {
                correct++;
            }
        }
        int total = items.size();
        BigDecimal rate = total <= 0
                ? BigDecimal.ZERO
                : BigDecimal.valueOf(correct * 100.0 / total).setScale(2, RoundingMode.HALF_UP);
        entity.setCorrectCount(correct);
        entity.setTotalCount(total);
        entity.setScoreRate(rate);
        entity.setSubmitTime(now);
        entity.setTestStatus("submitted");
        entity.setLastModifyUserId(userId);
        entity.setLastModifyTime(now);
        selfTestMapper.updateById(entity);
        TmsSelfTestSubmitResultVO result = new TmsSelfTestSubmitResultVO();
        result.setPaperId(id);
        result.setTotalCount(total);
        result.setCorrectCount(correct);
        result.setScoreRate(rate);
        return result;
    }
    private TmsSelfTestEntity requireDoingPaper(String id, String denyMsg) {
        TmsSelfTestEntity entity = selfTestMapper.selectById(id);
        if (entity == null) {
            throw new DataException("检测记录不存在");
        }
        String userId = UserProvider.getLoginUserId();
        if (!Objects.equals(userId, entity.getUserId())) {
            throw new DataException(denyMsg);
        }
        if ("submitted".equals(entity.getTestStatus())) {
            throw new DataException("已交卷,请勿重复操作");
        }
        return entity;
    }
    private List<TmsSelfTestItemEntity> listItems(String paperId) {
        LambdaQueryWrapper<TmsSelfTestItemEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsSelfTestItemEntity::getForeignId, paperId);
        qw.orderByAsc(TmsSelfTestItemEntity::getSortNo);
        return selfTestItemMapper.selectList(qw);
    }
    @Override
    public List<TmsSelfTestRecordVO> getList(TmsSelfTestQuery query) {
        String userId = UserProvider.getLoginUserId();
        LambdaQueryWrapper<TmsSelfTestEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsSelfTestEntity::getUserId, userId);
        if (StringUtil.isNotEmpty(query.getBankId())) {
            qw.eq(TmsSelfTestEntity::getBankId, query.getBankId());
        }
        if (StringUtil.isNotEmpty(query.getTestStatus())) {
            qw.eq(TmsSelfTestEntity::getTestStatus, query.getTestStatus());
        }
        if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) {
            qw.like(TmsSelfTestEntity::getBankName, query.getKeyword().trim());
        }
        if (StringUtil.isNotEmpty(query.getStartTimeBegin())) {
            Date begin = DateUtil.stringToDate(query.getStartTimeBegin());
            if (begin != null) {
                qw.ge(TmsSelfTestEntity::getStartTime, begin);
            }
        }
        if (StringUtil.isNotEmpty(query.getStartTimeEnd())) {
            Date end = DateUtil.stringToDate(query.getStartTimeEnd());
            if (end != null) {
                qw.le(TmsSelfTestEntity::getStartTime, end);
            }
        }
        qw.orderByDesc(TmsSelfTestEntity::getCreatorTime);
        Page<TmsSelfTestEntity> page = new Page<>(query.getCurrentPage(), query.getPageSize());
        Page<TmsSelfTestEntity> result = selfTestMapper.selectPage(page, qw);
        List<TmsSelfTestRecordVO> list = new ArrayList<>();
        for (TmsSelfTestEntity row : result.getRecords()) {
            TmsSelfTestRecordVO vo = new TmsSelfTestRecordVO();
            vo.setId(row.getId());
            vo.setBankId(row.getBankId());
            vo.setBankName(row.getBankName());
            vo.setTotalCount(row.getTotalCount());
            vo.setCorrectCount(row.getCorrectCount());
            vo.setScoreRate(row.getScoreRate());
            vo.setTestStatus(row.getTestStatus());
            vo.setStartTime(row.getStartTime() == null ? null : DateUtil.dateToString(row.getStartTime(), "yyyy-MM-dd HH:mm:ss"));
            vo.setSubmitTime(row.getSubmitTime() == null ? null : DateUtil.dateToString(row.getSubmitTime(), "yyyy-MM-dd HH:mm:ss"));
            list.add(vo);
        }
        query.setData(list, result.getTotal());
        return list;
    }
    @Override
    public TmsSelfTestPaperVO getInfo(String id) {
        TmsSelfTestEntity entity = selfTestMapper.selectById(id);
        if (entity == null) {
            throw new DataException("检测记录不存在");
        }
        if (!Objects.equals(UserProvider.getLoginUserId(), entity.getUserId())) {
            throw new DataException("无权查看该检测");
        }
        LambdaQueryWrapper<TmsSelfTestItemEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsSelfTestItemEntity::getForeignId, id);
        qw.orderByAsc(TmsSelfTestItemEntity::getSortNo);
        List<TmsSelfTestItemEntity> items = selfTestItemMapper.selectList(qw);
        List<TmsSelfTestQuestionVO> questions = new ArrayList<>(items.size());
        for (TmsSelfTestItemEntity item : items) {
            TmsSelfTestQuestionVO vo = new TmsSelfTestQuestionVO();
            vo.setId(item.getQuestionId());
            vo.setQuestionNo(item.getQuestionNo());
            vo.setQuestionType(item.getQuestionType());
            vo.setDifficulty(item.getDifficulty());
            vo.setStem(item.getStem());
            vo.setOptions(parseOptions(item.getOptionsJson()));
            vo.setCorrectAnswer(item.getCorrectAnswer());
            vo.setUserAnswer(item.getUserAnswer());
            vo.setIsRight(item.getIsRight());
            vo.setSortNo(item.getSortNo());
            questions.add(vo);
        }
        TmsSelfTestPaperVO paper = new TmsSelfTestPaperVO();
        paper.setPaperId(entity.getId());
        paper.setBankId(entity.getBankId());
        paper.setBankName(entity.getBankName());
        paper.setTestStatus(entity.getTestStatus());
        paper.setTotalCount(entity.getTotalCount());
        paper.setCorrectCount(entity.getCorrectCount());
        paper.setScoreRate(entity.getScoreRate());
        paper.setStartTime(entity.getStartTime() == null ? null : DateUtil.dateToString(entity.getStartTime(), "yyyy-MM-dd HH:mm:ss"));
        paper.setSubmitTime(entity.getSubmitTime() == null ? null : DateUtil.dateToString(entity.getSubmitTime(), "yyyy-MM-dd HH:mm:ss"));
        paper.setQuestions(questions);
        return paper;
    }
    private List<TmsQuestionEntity> drawQuestions(String bankId, String type, TmsSelfTestTypeSetting setting) {
        int count = countOf(setting);
        if (count <= 0) {
            return Collections.emptyList();
        }
        LambdaQueryWrapper<TmsQuestionEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsQuestionEntity::getBankId, bankId);
        qw.eq(TmsQuestionEntity::getQuestionType, type);
        qw.eq(TmsQuestionEntity::getBizStatus, "open");
        List<String> diffs = resolveDifficulties(diffOf(setting));
        if (!diffs.isEmpty()) {
            qw.in(TmsQuestionEntity::getDifficulty, diffs);
        }
        List<TmsQuestionEntity> pool = questionMapper.selectList(qw);
        if (pool.isEmpty()) {
            return Collections.emptyList();
        }
        Collections.shuffle(pool);
        if (pool.size() <= count) {
            return pool;
        }
        return new ArrayList<>(pool.subList(0, count));
    }
    private List<String> resolveDifficulties(String uiDifficulty) {
        if (StringUtil.isEmpty(uiDifficulty)) {
            return Collections.emptyList();
        }
        return DIFFICULTY_MAP.getOrDefault(uiDifficulty, Collections.singletonList(uiDifficulty));
    }
    private Map<String, List<TmsQuestionOptionEntity>> loadOptions(List<String> questionIds) {
        if (questionIds == null || questionIds.isEmpty()) {
            return Collections.emptyMap();
        }
        LambdaQueryWrapper<TmsQuestionOptionEntity> qw = new LambdaQueryWrapper<>();
        qw.in(TmsQuestionOptionEntity::getForeignId, questionIds);
        qw.orderByAsc(TmsQuestionOptionEntity::getSortNo);
        List<TmsQuestionOptionEntity> all = optionMapper.selectList(qw);
        return all.stream().collect(Collectors.groupingBy(TmsQuestionOptionEntity::getForeignId));
    }
    private List<TmsSelfTestOptionVO> toOptionVos(List<TmsQuestionOptionEntity> opts) {
        List<TmsSelfTestOptionVO> list = new ArrayList<>(opts.size());
        for (TmsQuestionOptionEntity opt : opts) {
            TmsSelfTestOptionVO vo = new TmsSelfTestOptionVO();
            vo.setOptionLabel(opt.getOptionLabel());
            vo.setOptionContent(opt.getOptionContent());
            vo.setIsCorrect(opt.getIsCorrect());
            list.add(vo);
        }
        return list;
    }
    private String toJson(Object obj) {
        try {
            return OBJECT_MAPPER.writeValueAsString(obj);
        } catch (Exception e) {
            throw new DataException("选项序列化失败");
        }
    }
    private List<TmsSelfTestOptionVO> parseOptions(String json) {
        if (StringUtil.isEmpty(json)) {
            return Collections.emptyList();
        }
        try {
            return OBJECT_MAPPER.readValue(json, new TypeReference<List<TmsSelfTestOptionVO>>() {
            });
        } catch (Exception e) {
            return Collections.emptyList();
        }
    }
    private int countOf(TmsSelfTestTypeSetting setting) {
        if (setting == null || setting.getCount() == null) {
            return 0;
        }
        return Math.max(0, setting.getCount());
    }
    private String diffOf(TmsSelfTestTypeSetting setting) {
        return setting == null ? null : setting.getDifficulty();
    }
    /** 归一化用户答案:多选排序后逗号拼接 */
    private String normalizeAnswer(Object raw, String questionType) {
        if (raw == null) {
            return "";
        }
        if ("multi".equals(questionType)) {
            Set<String> labels = new LinkedHashSet<>();
            if (raw instanceof List) {
                for (Object o : (List<?>) raw) {
                    if (o != null && StringUtil.isNotEmpty(String.valueOf(o))) {
                        labels.add(String.valueOf(o).trim());
                    }
                }
            } else {
                String s = String.valueOf(raw).trim();
                if (StringUtil.isNotEmpty(s)) {
                    for (String part : s.split("[,,]")) {
                        if (StringUtil.isNotEmpty(part.trim())) {
                            labels.add(part.trim());
                        }
                    }
                }
            }
            return labels.stream().sorted().collect(Collectors.joining(","));
        }
        return String.valueOf(raw).trim();
    }
    private String normalizeCorrect(String correct) {
        if (StringUtil.isEmpty(correct)) {
            return "";
        }
        return Arrays.stream(correct.split("[,,]"))
                .map(String::trim)
                .filter(StringUtil::isNotEmpty)
                .sorted()
                .collect(Collectors.joining(","));
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSignModeServiceImpl.java
New file
@@ -0,0 +1,164 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jnpf.exception.DataException;
import jnpf.tmsEntity.TmsSignModeEntity;
import jnpf.tmsEntity.signmode.TmsSignModeForm;
import jnpf.tmsEntity.signmode.TmsSignModeQuery;
import jnpf.tmsMapper.TmsSignModeMapper;
import jnpf.tmsService.TmsSignModeService;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TmsSignModeServiceImpl implements TmsSignModeService {
    private final TmsSignModeMapper signModeMapper;
    @Override
    public List<TmsSignModeForm> getList(TmsSignModeQuery query) {
        LambdaQueryWrapper<TmsSignModeEntity> qw = new LambdaQueryWrapper<>();
        if (StringUtil.isNotEmpty(query.getEnabled())) {
            qw.eq(TmsSignModeEntity::getEnabled, query.getEnabled());
        }
        if (StringUtil.isNotEmpty(query.getKeyword())) {
            String kw = query.getKeyword().trim();
            qw.and(w -> w.like(TmsSignModeEntity::getModeCode, kw)
                    .or()
                    .like(TmsSignModeEntity::getModeName, kw));
        }
        qw.orderByDesc(TmsSignModeEntity::getCreatorTime);
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        Page<TmsSignModeEntity> page = signModeMapper.selectPage(new Page<>(current, size), qw);
        List<TmsSignModeForm> list = new ArrayList<>(page.getRecords().size());
        for (TmsSignModeEntity row : page.getRecords()) {
            list.add(toForm(row));
        }
        query.setData(list, page.getTotal());
        return list;
    }
    @Override
    public TmsSignModeForm getInfo(String id) {
        return toForm(requireOne(id));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String create(TmsSignModeForm form) {
        validateForm(form, null);
        TmsSignModeEntity entity = new TmsSignModeEntity();
        entity.setId(RandomUtil.uuId());
        applyForm(entity, form);
        entity.setCreatorUserId(UserProvider.getLoginUserId());
        entity.setCreatorTime(new Date());
        signModeMapper.insert(entity);
        return entity.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(String id, TmsSignModeForm form) {
        TmsSignModeEntity entity = requireOne(id);
        validateForm(form, id);
        form.setModeCode(entity.getModeCode());
        applyForm(entity, form);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        signModeMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void setEnabled(String id, String enabled) {
        if (!"0".equals(enabled) && !"1".equals(enabled)) {
            throw new DataException("启用状态无效");
        }
        TmsSignModeEntity entity = requireOne(id);
        entity.setEnabled(enabled);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        signModeMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete(String id) {
        TmsSignModeEntity entity = requireOne(id);
        if (!"0".equals(entity.getEnabled())) {
            throw new DataException("请先停用后再删除");
        }
        signModeMapper.deleteById(id);
    }
    private TmsSignModeEntity requireOne(String id) {
        if (StringUtil.isEmpty(id)) {
            throw new DataException("签到方式不存在");
        }
        TmsSignModeEntity entity = signModeMapper.selectById(id);
        if (entity == null) {
            throw new DataException("签到方式不存在");
        }
        return entity;
    }
    private void validateForm(TmsSignModeForm form, String excludeId) {
        if (form == null || StringUtil.isEmpty(form.getModeCode())) {
            throw new DataException("请填写方式编码");
        }
        if (StringUtil.isEmpty(form.getModeName())) {
            throw new DataException("请填写方式名称");
        }
        String code = form.getModeCode().trim();
        form.setModeCode(code);
        LambdaQueryWrapper<TmsSignModeEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsSignModeEntity::getModeCode, code);
        if (StringUtil.isNotEmpty(excludeId)) {
            qw.ne(TmsSignModeEntity::getId, excludeId);
        }
        if (signModeMapper.selectCount(qw) > 0) {
            throw new DataException("方式编码已存在");
        }
    }
    private void applyForm(TmsSignModeEntity entity, TmsSignModeForm form) {
        entity.setModeCode(form.getModeCode().trim());
        entity.setModeName(form.getModeName().trim());
        entity.setTipSeconds(form.getTipSeconds() == null ? 30 : Math.max(0, form.getTipSeconds()));
        entity.setMatchRoster(yn(form.getMatchRoster(), "1"));
        entity.setEnabled(yn(form.getEnabled(), "1"));
        entity.setRemark(form.getRemark());
    }
    private static String yn(String v, String def) {
        if ("0".equals(v) || "1".equals(v)) {
            return v;
        }
        return def;
    }
    private TmsSignModeForm toForm(TmsSignModeEntity entity) {
        TmsSignModeForm form = new TmsSignModeForm();
        form.setId(entity.getId());
        form.setModeCode(entity.getModeCode());
        form.setModeName(entity.getModeName());
        form.setTipSeconds(entity.getTipSeconds());
        form.setMatchRoster(entity.getMatchRoster());
        form.setEnabled(entity.getEnabled());
        form.setRemark(entity.getRemark());
        return form;
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTaskServiceImpl.java
New file
@@ -0,0 +1,1069 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jnpf.exception.DataException;
import jnpf.tmsEntity.TmsAnnualPlanEntity;
import jnpf.tmsEntity.TmsAnnualPlanItemEntity;
import jnpf.tmsEntity.TmsMaterialEntity;
import jnpf.tmsEntity.TmsPaperEntity;
import jnpf.tmsEntity.TmsPersonTaskEntity;
import jnpf.tmsEntity.TmsPlaceEntity;
import jnpf.tmsEntity.TmsPostPlanEntity;
import jnpf.tmsEntity.TmsPostPlanItemEntity;
import jnpf.tmsEntity.TmsRecordEntity;
import jnpf.tmsEntity.TmsTaskEntity;
import jnpf.tmsEntity.TmsTaskFileEntity;
import jnpf.tmsEntity.TmsTrainerEntity;
import jnpf.tmsEntity.task.TmsFromPlanResult;
import jnpf.tmsEntity.task.TmsPersonTaskForm;
import jnpf.tmsEntity.task.TmsTaskFileForm;
import jnpf.tmsEntity.task.TmsTaskForm;
import jnpf.tmsEntity.task.TmsTaskQuery;
import jnpf.tmsMapper.TmsAnnualPlanItemMapper;
import jnpf.tmsMapper.TmsAnnualPlanMapper;
import jnpf.tmsMapper.TmsMaterialMapper;
import jnpf.tmsMapper.TmsPaperMapper;
import jnpf.tmsMapper.TmsPersonTaskMapper;
import jnpf.tmsMapper.TmsPlaceMapper;
import jnpf.tmsMapper.TmsPostPlanItemMapper;
import jnpf.tmsMapper.TmsPostPlanMapper;
import jnpf.tmsMapper.TmsRecordMapper;
import jnpf.tmsMapper.TmsTaskFileMapper;
import jnpf.tmsMapper.TmsTaskMapper;
import jnpf.tmsMapper.TmsTrainerMapper;
import jnpf.tmsService.TmsTaskService;
import jnpf.util.DateUtil;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class TmsTaskServiceImpl implements TmsTaskService {
    private static final String STATUS_DRAFT = "draft";
    private static final String STATUS_PUBLISHED = "published";
    private static final String STATUS_CANCELLED = "cancelled";
    private static final String PERSON_TODO = "todo";
    private static final String PERSON_SIGNED = "signed";
    private static final String PERSON_DONE = "done";
    private static final String PERSON_CANCELLED = "cancelled";
    private static final String PREFIX_DRAFT = "TD";
    private static final String PREFIX_PUBLISH = "TT";
    private static final String FMT = "yyyy-MM-dd HH:mm:ss";
    private final TmsTaskMapper taskMapper;
    private final TmsTrainerMapper trainerMapper;
    private final TmsPersonTaskMapper personTaskMapper;
    private final TmsRecordMapper recordMapper;
    private final TmsPaperMapper paperMapper;
    private final TmsMaterialMapper materialMapper;
    private final TmsTaskFileMapper taskFileMapper;
    private final TmsPlaceMapper placeMapper;
    private final TmsAnnualPlanMapper annualPlanMapper;
    private final TmsAnnualPlanItemMapper annualPlanItemMapper;
    private final TmsPostPlanMapper postPlanMapper;
    private final TmsPostPlanItemMapper postPlanItemMapper;
    @Override
    public List<TmsTaskForm> getList(TmsTaskQuery query) {
        LambdaQueryWrapper<TmsTaskEntity> qw = new LambdaQueryWrapper<>();
        if (StringUtil.isNotEmpty(query.getPublishStatus())) {
            qw.eq(TmsTaskEntity::getPublishStatus, query.getPublishStatus());
        }
        if (StringUtil.isNotEmpty(query.getTrainMode())) {
            qw.eq(TmsTaskEntity::getTrainMode, query.getTrainMode());
        }
        if (StringUtil.isNotEmpty(query.getSourceType())) {
            qw.eq(TmsTaskEntity::getSourceType, query.getSourceType());
        }
        if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) {
            String kw = query.getKeyword().trim();
            qw.and(w -> w.like(TmsTaskEntity::getTaskNo, kw).or().like(TmsTaskEntity::getSubject, kw));
        }
        Date from = parseDayStart(query.getStartTimeFrom());
        Date to = parseDayEnd(query.getStartTimeTo());
        if (from != null) {
            qw.ge(TmsTaskEntity::getStartTime, from);
        }
        if (to != null) {
            qw.le(TmsTaskEntity::getStartTime, to);
        }
        qw.orderByDesc(TmsTaskEntity::getStartTime).orderByDesc(TmsTaskEntity::getCreatorTime);
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        Page<TmsTaskEntity> page = taskMapper.selectPage(new Page<>(current, size), qw);
        List<TmsTaskForm> list = new ArrayList<>(page.getRecords().size());
        for (TmsTaskEntity row : page.getRecords()) {
            list.add(toForm(row, false));
        }
        query.setData(list, page.getTotal());
        return list;
    }
    @Override
    public TmsTaskForm getInfo(String id) {
        TmsTaskEntity entity = requireOne(id);
        TmsTaskForm form = toForm(entity, true);
        List<TmsPersonTaskEntity> persons = personTaskMapper.selectList(
                new LambdaQueryWrapper<TmsPersonTaskEntity>()
                        .eq(TmsPersonTaskEntity::getTaskId, id)
                        .orderByAsc(TmsPersonTaskEntity::getCreatorTime));
        List<TmsPersonTaskForm> personForms = new ArrayList<>(persons.size());
        for (TmsPersonTaskEntity p : persons) {
            TmsPersonTaskForm pf = new TmsPersonTaskForm();
            pf.setId(p.getId());
            pf.setUserId(p.getUserId());
            pf.setDeptId(p.getDeptId());
            pf.setBizStatus(p.getBizStatus());
            pf.setSignTime(formatDate(p.getSignTime()));
            pf.setExamScore(p.getExamScore());
            pf.setPassFlag(p.getPassFlag());
            pf.setGuestFlag(p.getGuestFlag());
            personForms.add(pf);
        }
        form.setPersonTasks(personForms);
        form.setPersonCount(personForms.size());
        form.setFiles(listTaskFiles(id));
        return form;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String create(TmsTaskForm form) {
        // 空白新增仅允许临时培训;年/岗计划任务应由计划侧生成并带 source_id/item_id
        boolean hasSourceLink = StringUtil.isNotEmpty(form.getSourceId())
                || StringUtil.isNotEmpty(form.getSourceItemId());
        if (!hasSourceLink) {
            form.setSourceType("temp");
            form.setCategory("temp");
        }
        validateForm(form, true);
        TmsTaskEntity entity = new TmsTaskEntity();
        entity.setId(RandomUtil.uuId());
        entity.setTaskNo(nextTaskNo(PREFIX_DRAFT));
        entity.setPublishStatus(STATUS_DRAFT);
        entity.setTaskKind(StringUtil.isNotEmpty(form.getTaskKind()) ? form.getTaskKind() : "new");
        fillEntity(entity, form, false);
        applyDefaults(entity);
        fillRefs(entity);
        taskMapper.insert(entity);
        saveTaskFiles(entity.getId(), form.getFiles());
        return entity.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(String id, TmsTaskForm form) {
        TmsTaskEntity entity = requireOne(id);
        if (!STATUS_DRAFT.equals(entity.getPublishStatus())) {
            throw new DataException("仅未发布任务可编辑");
        }
        boolean fromPlan = isPlanSourced(entity);
        if (fromPlan) {
            // 计划带出:来源链路只读,业务内容允许维护补全
            form.setCategory(entity.getCategory());
            form.setSourceType(entity.getSourceType());
            form.setSourcePlanNo(entity.getSourcePlanNo());
            form.setSourceId(entity.getSourceId());
            form.setSourceItemId(entity.getSourceItemId());
        }
        validateForm(form, false);
        fillEntity(entity, form, false);
        applyDefaults(entity);
        fillRefs(entity);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        taskMapper.updateById(entity);
        if (form.getFiles() != null) {
            saveTaskFiles(entity.getId(), form.getFiles());
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete(String id) {
        TmsTaskEntity entity = requireOne(id);
        if (!STATUS_DRAFT.equals(entity.getPublishStatus())) {
            throw new DataException("仅未发布任务可删除");
        }
        Long cnt = personTaskMapper.selectCount(
                new LambdaQueryWrapper<TmsPersonTaskEntity>().eq(TmsPersonTaskEntity::getTaskId, id));
        if (cnt != null && cnt > 0) {
            throw new DataException("已生成个人任务,不可删除");
        }
        taskMapper.deleteById(id);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void publish(String id) {
        TmsTaskEntity entity = requireOne(id);
        if (!STATUS_DRAFT.equals(entity.getPublishStatus())) {
            throw new DataException("仅未发布任务可发布");
        }
        validateForPublish(entity);
        String taskNo = entity.getTaskNo();
        if (StringUtil.isEmpty(taskNo) || taskNo.startsWith(PREFIX_DRAFT)) {
            taskNo = nextTaskNo(PREFIX_PUBLISH);
            entity.setTaskNo(taskNo);
        }
        List<String> userIds = splitTrainees(entity.getTrainees());
        if (userIds.isEmpty()) {
            throw new DataException("培训对象不能为空");
        }
        Date now = new Date();
        BigDecimal hours = resolveTaskHours(entity);
        for (String userId : userIds) {
            TmsPersonTaskEntity pt = new TmsPersonTaskEntity();
            pt.setId(RandomUtil.uuId());
            pt.setTaskId(entity.getId());
            pt.setTaskNo(taskNo);
            pt.setUserId(userId);
            pt.setHours(hours);
            pt.setBizStatus(PERSON_TODO);
            pt.setGuestFlag("0");
            pt.setCreatorUserId(UserProvider.getLoginUserId());
            pt.setCreatorTime(now);
            personTaskMapper.insert(pt);
        }
        Long recordCnt = recordMapper.selectCount(
                new LambdaQueryWrapper<TmsRecordEntity>().eq(TmsRecordEntity::getTaskId, entity.getId()));
        if (recordCnt == null || recordCnt == 0) {
            TmsRecordEntity record = new TmsRecordEntity();
            record.setId(RandomUtil.uuId());
            record.setRecordNo(taskNo);
            record.setTaskId(entity.getId());
            record.setTaskNo(taskNo);
            record.setCategory(entity.getCategory());
            record.setSubject(entity.getSubject());
            record.setTeacherId(entity.getTeacherId());
            record.setTrainType(entity.getTrainType());
            record.setTrainMode(entity.getTrainMode());
            record.setEvalMode(entity.getEvalMode());
            record.setStartTime(entity.getStartTime());
            record.setEndTime(entity.getEndTime());
            record.setCloseTime(entity.getCloseTime());
            record.setArchiveStatus("pending");
            record.setCreatorUserId(UserProvider.getLoginUserId());
            record.setCreatorTime(now);
            recordMapper.insert(record);
        }
        entity.setPublishStatus(STATUS_PUBLISHED);
        entity.setPublishTime(now);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(now);
        taskMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancel(String id) {
        TmsTaskEntity entity = requireOne(id);
        if (!STATUS_PUBLISHED.equals(entity.getPublishStatus())) {
            throw new DataException("仅已发布任务可取消");
        }
        Long signed = personTaskMapper.selectCount(
                new LambdaQueryWrapper<TmsPersonTaskEntity>()
                        .eq(TmsPersonTaskEntity::getTaskId, id)
                        .and(w -> w.isNotNull(TmsPersonTaskEntity::getSignTime)
                                .or().in(TmsPersonTaskEntity::getBizStatus, PERSON_SIGNED, PERSON_DONE)));
        if (signed != null && signed > 0) {
            throw new DataException("已有人员签到,不可取消");
        }
        Date now = new Date();
        entity.setPublishStatus(STATUS_CANCELLED);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(now);
        taskMapper.updateById(entity);
        personTaskMapper.update(null, new LambdaUpdateWrapper<TmsPersonTaskEntity>()
                .eq(TmsPersonTaskEntity::getTaskId, id)
                .set(TmsPersonTaskEntity::getBizStatus, PERSON_CANCELLED)
                .set(TmsPersonTaskEntity::getLastModifyUserId, UserProvider.getLoginUserId())
                .set(TmsPersonTaskEntity::getLastModifyTime, now));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelBatch(List<String> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new DataException("请选择要取消的任务");
        }
        for (String id : ids) {
            if (StringUtil.isNotEmpty(id)) {
                cancel(id);
            }
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void restore(String id) {
        TmsTaskEntity entity = requireOne(id);
        if (!STATUS_CANCELLED.equals(entity.getPublishStatus())) {
            throw new DataException("仅已取消任务可恢复");
        }
        Date now = new Date();
        String userId = UserProvider.getLoginUserId();
        // 逻辑删除已生成的个人任务与记录,便于再次编辑后发布
        List<TmsPersonTaskEntity> persons = personTaskMapper.selectList(
                new LambdaQueryWrapper<TmsPersonTaskEntity>().eq(TmsPersonTaskEntity::getTaskId, id));
        for (TmsPersonTaskEntity p : persons) {
            personTaskMapper.deleteById(p.getId());
        }
        List<TmsRecordEntity> records = recordMapper.selectList(
                new LambdaQueryWrapper<TmsRecordEntity>().eq(TmsRecordEntity::getTaskId, id));
        for (TmsRecordEntity r : records) {
            recordMapper.deleteById(r.getId());
        }
        entity.setPublishStatus(STATUS_DRAFT);
        entity.setPublishTime(null);
        entity.setLastModifyUserId(userId);
        entity.setLastModifyTime(now);
        taskMapper.updateById(entity);
        // 清空发布时间字段(updateById 可能跳过 null)
        taskMapper.update(null, new LambdaUpdateWrapper<TmsTaskEntity>()
                .eq(TmsTaskEntity::getId, id)
                .set(TmsTaskEntity::getPublishTime, null));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public TmsFromPlanResult createFromAnnual(String planId) {
        if (StringUtil.isEmpty(planId)) {
            throw new DataException("年计划ID不能为空");
        }
        String id = planId.trim();
        TmsAnnualPlanEntity plan = annualPlanMapper.selectById(id);
        if (plan == null) {
            throw new DataException("年度培训计划不存在: " + id);
        }
        List<TmsAnnualPlanItemEntity> items = annualPlanItemMapper.selectList(
                new LambdaQueryWrapper<TmsAnnualPlanItemEntity>()
                        .eq(TmsAnnualPlanItemEntity::getForeignId, plan.getId())
                        .orderByAsc(TmsAnnualPlanItemEntity::getTrainMonth));
        if (items.isEmpty()) {
            throw new DataException("年度计划没有明细,无法生成任务");
        }
        TmsFromPlanResult result = new TmsFromPlanResult();
        result.setPlanId(plan.getId());
        Date now = new Date();
        String userId = UserProvider.getLoginUserId();
        for (TmsAnnualPlanItemEntity item : items) {
            TmsTaskEntity existed = findTaskBySourceItem(item.getId());
            if (existed != null) {
                result.setSkipped(result.getSkipped() + 1);
                if (StringUtil.isEmpty(item.getTaskId())) {
                    item.setTaskId(existed.getId());
                    item.setLineStatus("tasked");
                    annualPlanItemMapper.updateById(item);
                }
                continue;
            }
            Date[] window = monthWindow(parseYear(plan.getPlanYear()), item.getTrainMonth());
            TmsTaskEntity task = new TmsTaskEntity();
            task.setId(RandomUtil.uuId());
            task.setTaskNo(nextTaskNo(PREFIX_DRAFT));
            task.setPublishStatus(STATUS_DRAFT);
            task.setTaskKind("new");
            task.setCategory("annual_plan");
            task.setSourceType("annual");
            task.setSourcePlanNo(plan.getPlanNo());
            task.setSourceId(plan.getId());
            task.setSourceItemId(item.getId());
            String subject = StringUtil.isNotEmpty(item.getTrainSubject()) ? item.getTrainSubject().trim() : "年度培训";
            task.setSubject(subject);
            task.setTrainMode(item.getTrainMode());
            task.setEvalMode(item.getEvalMode());
            task.setTeacherId(resolveTeacherUserId(item.getTeacherId()));
            task.setTrainees(normalizeTrainees(item.getTrainees()));
            if (window != null) {
                task.setStartTime(window[0]);
                task.setEndTime(window[1]);
                task.setCloseTime(window[1]);
            }
            task.setRetakeLimit(1);
            task.setOverdueRemind("0");
            task.setInviteFlag("0");
            task.setCreatorUserId(userId);
            task.setCreatorTime(now);
            fillRefs(task);
            taskMapper.insert(task);
            item.setTaskId(task.getId());
            item.setLineStatus("tasked");
            annualPlanItemMapper.updateById(item);
            result.setCreated(result.getCreated() + 1);
            result.getTaskIds().add(task.getId());
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public TmsFromPlanResult createFromPost(String planId) {
        if (StringUtil.isEmpty(planId)) {
            throw new DataException("岗位培训计划ID不能为空");
        }
        String id = planId.trim();
        TmsPostPlanEntity plan = postPlanMapper.selectById(id);
        if (plan == null) {
            throw new DataException("岗位培训计划不存在: " + id);
        }
        List<TmsPostPlanItemEntity> items = postPlanItemMapper.selectList(
                new LambdaQueryWrapper<TmsPostPlanItemEntity>()
                        .eq(TmsPostPlanItemEntity::getForeignId, plan.getId())
                        .orderByAsc(TmsPostPlanItemEntity::getPlanTime));
        if (items.isEmpty()) {
            throw new DataException("岗位培训计划没有明细,无法生成任务");
        }
        TmsFromPlanResult result = new TmsFromPlanResult();
        result.setPlanId(plan.getId());
        Date now = new Date();
        String userId = UserProvider.getLoginUserId();
        String trainees = normalizeTrainees(plan.getTrainees());
        for (TmsPostPlanItemEntity item : items) {
            TmsTaskEntity existed = findTaskBySourceItem(item.getId());
            if (existed != null) {
                result.setSkipped(result.getSkipped() + 1);
                if (StringUtil.isEmpty(item.getTaskId())) {
                    item.setTaskId(existed.getId());
                    item.setLineStatus("tasked");
                    postPlanItemMapper.updateById(item);
                }
                continue;
            }
            Date[] window = dayWindow(item.getPlanTime() != null ? item.getPlanTime() : plan.getPlanEndTime());
            TmsTaskEntity task = new TmsTaskEntity();
            task.setId(RandomUtil.uuId());
            task.setTaskNo(nextTaskNo(PREFIX_DRAFT));
            task.setPublishStatus(STATUS_DRAFT);
            task.setTaskKind("new");
            task.setCategory("post_plan");
            task.setSourceType("post");
            task.setSourcePlanNo(plan.getPlanNo());
            task.setSourceId(plan.getId());
            task.setSourceItemId(item.getId());
            String subject = StringUtil.isNotEmpty(item.getSubject()) ? item.getSubject().trim() : null;
            if (StringUtil.isEmpty(subject)) {
                subject = StringUtil.isNotEmpty(plan.getPostName()) ? plan.getPostName() + "岗位培训" : "岗位培训";
            }
            task.setSubject(subject);
            task.setTrainMode(item.getTrainMode());
            task.setEvalMode(item.getEvalMode());
            task.setTeacherId(resolveTeacherUserId(item.getTeacherId()));
            task.setTrainees(trainees);
            task.setMaterialId(item.getMaterialId());
            if (window != null) {
                task.setStartTime(window[0]);
                task.setEndTime(window[1]);
                task.setCloseTime(window[1]);
            }
            task.setRetakeLimit(1);
            task.setOverdueRemind("0");
            task.setInviteFlag("0");
            task.setCreatorUserId(userId);
            task.setCreatorTime(now);
            fillRefs(task);
            taskMapper.insert(task);
            item.setTaskId(task.getId());
            item.setLineStatus("tasked");
            postPlanItemMapper.updateById(item);
            result.setCreated(result.getCreated() + 1);
            result.getTaskIds().add(task.getId());
        }
        return result;
    }
    private TmsTaskEntity findTaskBySourceItem(String itemId) {
        if (StringUtil.isEmpty(itemId)) {
            return null;
        }
        return taskMapper.selectOne(new LambdaQueryWrapper<TmsTaskEntity>()
                .eq(TmsTaskEntity::getSourceItemId, itemId)
                .last("limit 1"));
    }
    /**
     * 年份:2026、2026-01-01…、或 13 位毫秒时间戳。
     * 在线表单日期控件会把年份存成日期字符串。
     */
    private static Integer parseYear(String raw) {
        if (StringUtil.isEmpty(raw)) {
            return null;
        }
        String text = raw.trim();
        try {
            if (text.matches("\\d{4}")) {
                return Integer.parseInt(text);
            }
            if (text.matches("\\d{13}")) {
                return java.time.Instant.ofEpochMilli(Long.parseLong(text))
                        .atZone(ZoneId.systemDefault()).getYear();
            }
            if (text.length() >= 4 && text.substring(0, 4).matches("\\d{4}")) {
                return Integer.parseInt(text.substring(0, 4));
            }
        } catch (Exception ignored) {
            return null;
        }
        return null;
    }
    /** 月份占位:精确时间在任务「执行安排」再改。支持 yyyyMM、1-12、yyyy-MM-dd、13 位毫秒时间戳。 */
    private static Date[] monthWindow(Integer planYear, String trainMonth) {
        if (StringUtil.isEmpty(trainMonth)) {
            return null;
        }
        String raw = trainMonth.trim();
        int year;
        int month;
        try {
            if (raw.matches("\\d{13}")) {
                java.time.LocalDate day = java.time.Instant.ofEpochMilli(Long.parseLong(raw))
                        .atZone(ZoneId.systemDefault()).toLocalDate();
                year = day.getYear();
                month = day.getMonthValue();
            } else if (raw.matches("\\d{4}-\\d{2}-\\d{2}.*")) {
                year = Integer.parseInt(raw.substring(0, 4));
                month = Integer.parseInt(raw.substring(5, 7));
            } else if (raw.matches("\\d{6}")) {
                year = Integer.parseInt(raw.substring(0, 4));
                month = Integer.parseInt(raw.substring(4, 6));
            } else if (raw.matches("\\d{1,2}")) {
                if (planYear == null) {
                    return null;
                }
                year = planYear;
                month = Integer.parseInt(raw);
            } else {
                return null;
            }
            if (month < 1 || month > 12) {
                return null;
            }
            YearMonth ym = YearMonth.of(year, month);
            ZoneId zone = ZoneId.systemDefault();
            Date start = Date.from(ym.atDay(1).atStartOfDay(zone).toInstant());
            Date end = Date.from(ym.atEndOfMonth().atTime(23, 59, 59).atZone(zone).toInstant());
            return new Date[]{start, end};
        } catch (Exception ex) {
            return null;
        }
    }
    /** 岗位计划明细是具体日期;没有明细日期时用计划结束日。 */
    private static Date[] dayWindow(Date day) {
        if (day == null) {
            return null;
        }
        try {
            java.time.LocalDate date = day.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
            ZoneId zone = ZoneId.systemDefault();
            Date start = Date.from(date.atStartOfDay(zone).toInstant());
            Date end = Date.from(date.atTime(23, 59, 59).atZone(zone).toInstant());
            return new Date[]{start, end};
        } catch (Exception ex) {
            return null;
        }
    }
    private static String normalizeTrainees(String raw) {
        if (StringUtil.isEmpty(raw)) {
            return null;
        }
        String text = raw.trim();
        if (text.startsWith("[") && text.endsWith("]")) {
            text = text.substring(1, text.length() - 1).replace("\"", "");
        }
        List<String> ids = splitTrainees(text);
        return ids.isEmpty() ? text : joinTrainees(ids);
    }
    private void validateForm(TmsTaskForm form, boolean creating) {
        if (form == null) {
            throw new DataException("表单不能为空");
        }
        if (StringUtil.isEmpty(form.getSubject())) {
            throw new DataException("培训主题不能为空");
        }
        if (StringUtil.isEmpty(form.getCategory())) {
            throw new DataException("培训分类不能为空");
        }
        if (StringUtil.isEmpty(form.getTrainMode())) {
            throw new DataException("培训方式不能为空");
        }
        if (StringUtil.isEmpty(form.getSourceType())) {
            throw new DataException("来源类型不能为空");
        }
        if (StringUtil.isEmpty(form.getEvalMode())) {
            throw new DataException("考核方式不能为空");
        }
        Date start = parseDate(form.getStartTime());
        Date end = parseDate(form.getEndTime());
        Date close = parseDate(form.getCloseTime());
        if (start == null || end == null || close == null) {
            throw new DataException("开始/结束/关闭时间不能为空");
        }
        if (start.after(end)) {
            throw new DataException("开始时间不能晚于结束时间");
        }
        if (end.after(close)) {
            throw new DataException("结束时间不能晚于关闭时间");
        }
        if (creating && start.before(new Date())) {
            // 允许当天稍早几分钟;严格:开始不得早于当前日 00:00
            Calendar cal = Calendar.getInstance();
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
            cal.set(Calendar.MILLISECOND, 0);
            if (start.before(cal.getTime())) {
                throw new DataException("开始时间不得早于当天");
            }
        }
        String mode = form.getTrainMode();
        if ("onsite".equals(mode) || "practice".equals(mode)) {
            if (!sameDay(start, end)) {
                throw new DataException("集中/操作授课的开始与结束须为同一天");
            }
            if (StringUtil.isEmpty(form.getPlaceId()) && StringUtil.isEmpty(form.getPlaceName())) {
                throw new DataException("现场类培训须填写地点");
            }
        }
        if ("online".equals(mode) && splitTrainees(form.getMaterialId()).isEmpty()) {
            throw new DataException("在线学习须选择教材");
        }
        if ("exam".equals(form.getEvalMode())) {
            if (StringUtil.isEmpty(form.getPaperId())) {
                throw new DataException("在线考试须选择试卷");
            }
            Date examStart = parseDate(form.getExamStart());
            Date examEnd = parseDate(form.getExamEnd());
            if (examStart != null && examStart.before(start)) {
                throw new DataException("考试开始不得早于培训开始");
            }
            if (examStart != null && examEnd != null && examStart.after(examEnd)) {
                throw new DataException("考试开始不能晚于考试结束");
            }
        }
        List<String> trainees = resolveTraineeIds(form);
        if (trainees.isEmpty()) {
            throw new DataException("培训对象不能为空");
        }
        Integer retake = form.getRetakeLimit();
        if (retake != null && retake < 0) {
            throw new DataException("补考次数不能为负数");
        }
    }
    private void validateForPublish(TmsTaskEntity entity) {
        if (StringUtil.isEmpty(entity.getSubject())
                || StringUtil.isEmpty(entity.getTrainMode())
                || StringUtil.isEmpty(entity.getEvalMode())
                || entity.getStartTime() == null
                || entity.getEndTime() == null
                || entity.getCloseTime() == null) {
            throw new DataException("任务信息不完整,请先完善后发布");
        }
        if (splitTrainees(entity.getTrainees()).isEmpty()) {
            throw new DataException("培训对象不能为空");
        }
        if ("exam".equals(entity.getEvalMode())) {
            if (StringUtil.isEmpty(entity.getPaperId())) {
                throw new DataException("在线考试须选择试卷");
            }
            TmsPaperEntity paper = paperMapper.selectById(entity.getPaperId());
            if (paper == null) {
                throw new DataException("所选试卷不存在");
            }
            if ("invalid".equals(paper.getBizStatus())) {
                throw new DataException("失效试卷不可用于发布");
            }
        }
    }
    private void fillEntity(TmsTaskEntity entity, TmsTaskForm form, boolean keepPlanContent) {
        if (!keepPlanContent) {
            entity.setCategory(form.getCategory());
            entity.setTrainType(form.getTrainType());
            entity.setSourceType(form.getSourceType());
            entity.setSourcePlanNo(form.getSourcePlanNo());
            entity.setSourceId(form.getSourceId());
            entity.setSourceItemId(form.getSourceItemId());
            entity.setSubject(form.getSubject() == null ? null : form.getSubject().trim());
            entity.setKeyPoints(form.getKeyPoints());
            entity.setMaterialId(firstId(form.getMaterialId()));
            entity.setMaterialNo(firstId(form.getMaterialNo()));
            entity.setTeacherId(form.getTeacherId());
            entity.setTrainMode(form.getTrainMode());
            entity.setEvalMode(form.getEvalMode());
            entity.setTrainees(joinTrainees(resolveTraineeIds(form)));
            if (StringUtil.isNotEmpty(form.getTaskKind())) {
                entity.setTaskKind(form.getTaskKind());
            }
            entity.setPrevTaskNo(form.getPrevTaskNo());
        }
        // 执行信息:计划/临时均可维护
        entity.setPlaceId(form.getPlaceId());
        entity.setPlaceNo(form.getPlaceNo());
        entity.setPlaceName(form.getPlaceName());
        entity.setStartTime(parseDate(form.getStartTime()));
        entity.setEndTime(parseDate(form.getEndTime()));
        entity.setCloseTime(parseDate(form.getCloseTime()));
        entity.setPaperId(form.getPaperId());
        entity.setPaperName(form.getPaperName());
        entity.setExamStart(parseDate(form.getExamStart()));
        entity.setExamEnd(parseDate(form.getExamEnd()));
        entity.setRetakeLimit(form.getRetakeLimit() == null ? 1 : form.getRetakeLimit());
        entity.setPassScore(form.getPassScore());
        entity.setQuizRatio(form.getQuizRatio());
        entity.setOverdueRemind(StringUtil.isEmpty(form.getOverdueRemind()) ? "0" : form.getOverdueRemind());
        entity.setInviteFlag(StringUtil.isEmpty(form.getInviteFlag()) ? "0" : form.getInviteFlag());
        entity.setRemark(form.getRemark());
    }
    private static boolean isPlanSourced(TmsTaskEntity entity) {
        if (StringUtil.isNotEmpty(entity.getSourceId())
                || StringUtil.isNotEmpty(entity.getSourceItemId())
                || StringUtil.isNotEmpty(entity.getSourcePlanNo())) {
            return true;
        }
        String st = entity.getSourceType();
        return StringUtil.isNotEmpty(st) && !"temp".equals(st);
    }
    private void applyDefaults(TmsTaskEntity entity) {
        if (entity.getRetakeLimit() == null) {
            entity.setRetakeLimit(1);
        }
        if (StringUtil.isEmpty(entity.getOverdueRemind())) {
            entity.setOverdueRemind("0");
        }
        if (StringUtil.isEmpty(entity.getInviteFlag())) {
            entity.setInviteFlag("0");
        }
        if (StringUtil.isEmpty(entity.getTaskKind())) {
            entity.setTaskKind("new");
        }
    }
    private void fillRefs(TmsTaskEntity entity) {
        if (StringUtil.isNotEmpty(entity.getPaperId())) {
            TmsPaperEntity paper = paperMapper.selectById(entity.getPaperId());
            if (paper != null) {
                entity.setPaperName(paper.getPaperName());
                if (entity.getPassScore() == null) {
                    entity.setPassScore(paper.getPassScore());
                }
            }
        } else {
            entity.setPaperName(null);
        }
        if (StringUtil.isNotEmpty(entity.getMaterialId())) {
            TmsMaterialEntity material = materialMapper.selectById(entity.getMaterialId());
            if (material != null && StringUtil.isNotEmpty(material.getMaterialNo())) {
                entity.setMaterialNo(material.getMaterialNo());
            }
        } else {
            entity.setMaterialNo(null);
        }
        if (StringUtil.isNotEmpty(entity.getPlaceId())) {
            TmsPlaceEntity place = placeMapper.selectById(entity.getPlaceId());
            if (place != null) {
                entity.setPlaceNo(place.getPlaceNo());
                entity.setPlaceName(place.getPlaceName());
            }
        }
    }
    private void saveTaskFiles(String taskId, List<TmsTaskFileForm> files) {
        taskFileMapper.delete(new LambdaQueryWrapper<TmsTaskFileEntity>()
                .eq(TmsTaskFileEntity::getForeignId, taskId));
        if (files == null || files.isEmpty()) {
            return;
        }
        Date now = new Date();
        String userId = UserProvider.getLoginUserId();
        for (TmsTaskFileForm row : files) {
            if (row == null || StringUtil.isEmpty(row.getFileName())) {
                continue;
            }
            TmsTaskFileEntity entity = new TmsTaskFileEntity();
            entity.setId(RandomUtil.uuId());
            entity.setForeignId(taskId);
            entity.setFileNo(row.getFileNo());
            entity.setFileName(row.getFileName().trim());
            entity.setMaterialId(row.getMaterialId());
            entity.setFileJson(row.getFileJson());
            entity.setCanDownload(StringUtil.isEmpty(row.getCanDownload()) ? "0" : row.getCanDownload());
            entity.setCreatorUserId(userId);
            entity.setCreatorTime(now);
            taskFileMapper.insert(entity);
        }
    }
    private List<TmsTaskFileForm> listTaskFiles(String taskId) {
        List<TmsTaskFileEntity> rows = taskFileMapper.selectList(
                new LambdaQueryWrapper<TmsTaskFileEntity>()
                        .eq(TmsTaskFileEntity::getForeignId, taskId)
                        .orderByAsc(TmsTaskFileEntity::getCreatorTime));
        List<TmsTaskFileForm> list = new ArrayList<>(rows.size());
        for (TmsTaskFileEntity row : rows) {
            TmsTaskFileForm form = new TmsTaskFileForm();
            form.setId(row.getId());
            form.setFileNo(row.getFileNo());
            form.setFileName(row.getFileName());
            form.setMaterialId(row.getMaterialId());
            form.setFileJson(row.getFileJson());
            form.setCanDownload(row.getCanDownload());
            list.add(form);
        }
        return list;
    }
    private static String firstId(String raw) {
        List<String> ids = splitTrainees(raw);
        return ids.isEmpty() ? null : ids.get(0);
    }
    private synchronized String nextTaskNo(String typePrefix) {
        String ym = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
        String prefix = typePrefix + ym;
        String max = taskMapper.selectMaxTaskNo(prefix);
        int seq = 1;
        if (StringUtil.isNotEmpty(max) && max.length() == 12) {
            try {
                seq = Integer.parseInt(max.substring(8)) + 1;
            } catch (NumberFormatException ignored) {
                seq = 1;
            }
        }
        if (seq > 9999) {
            throw new DataException("当月任务编号已用尽");
        }
        return prefix + String.format("%04d", seq);
    }
    /**
     * 年计划子表 teacher_id 多为培训师档案 f_id;任务表单用 UserSelect,需落到系统用户。
     */
    private String resolveTeacherUserId(String teacherRef) {
        if (StringUtil.isEmpty(teacherRef)) {
            return null;
        }
        String raw = teacherRef.trim();
        TmsTrainerEntity trainer = trainerMapper.selectById(raw);
        if (trainer != null && StringUtil.isNotEmpty(trainer.getUserId())) {
            return trainer.getUserId();
        }
        // 已是系统用户,或档案尚未关联用户
        return raw;
    }
    private TmsTaskEntity requireOne(String id) {
        if (StringUtil.isEmpty(id)) {
            throw new DataException("任务ID不能为空");
        }
        TmsTaskEntity entity = taskMapper.selectById(id);
        if (entity == null) {
            throw new DataException("培训任务不存在");
        }
        return entity;
    }
    private TmsTaskForm toForm(TmsTaskEntity entity, boolean detail) {
        TmsTaskForm form = new TmsTaskForm();
        form.setId(entity.getId());
        form.setTaskNo(entity.getTaskNo());
        form.setCategory(entity.getCategory());
        form.setTrainType(entity.getTrainType());
        form.setSourceType(entity.getSourceType());
        form.setSourcePlanNo(entity.getSourcePlanNo());
        form.setSourceId(entity.getSourceId());
        form.setSourceItemId(entity.getSourceItemId());
        form.setSubject(entity.getSubject());
        form.setKeyPoints(entity.getKeyPoints());
        form.setMaterialId(entity.getMaterialId());
        form.setMaterialNo(entity.getMaterialNo());
        form.setPlaceId(entity.getPlaceId());
        form.setPlaceNo(entity.getPlaceNo());
        form.setPlaceName(entity.getPlaceName());
        form.setStartTime(formatDate(entity.getStartTime()));
        form.setEndTime(formatDate(entity.getEndTime()));
        form.setCloseTime(formatDate(entity.getCloseTime()));
        form.setPaperId(entity.getPaperId());
        form.setPaperName(entity.getPaperName());
        form.setExamStart(formatDate(entity.getExamStart()));
        form.setExamEnd(formatDate(entity.getExamEnd()));
        form.setTeacherId(resolveTeacherUserId(entity.getTeacherId()));
        form.setTrainMode(entity.getTrainMode());
        form.setEvalMode(entity.getEvalMode());
        form.setTrainees(entity.getTrainees());
        form.setTraineeIds(splitTrainees(entity.getTrainees()));
        form.setTaskKind(entity.getTaskKind());
        form.setPrevTaskNo(entity.getPrevTaskNo());
        form.setPublishStatus(entity.getPublishStatus());
        form.setPublishTime(formatDate(entity.getPublishTime()));
        form.setRetakeLimit(entity.getRetakeLimit());
        form.setPassScore(entity.getPassScore());
        form.setQuizRatio(entity.getQuizRatio());
        form.setOverdueRemind(entity.getOverdueRemind());
        form.setInviteFlag(entity.getInviteFlag());
        form.setRemark(entity.getRemark());
        form.setCreatorTime(formatDate(entity.getCreatorTime()));
        form.setCreatorUserId(entity.getCreatorUserId());
        if (detail) {
            // person tasks filled by caller
        }
        return form;
    }
    private static List<String> resolveTraineeIds(TmsTaskForm form) {
        Set<String> set = new LinkedHashSet<>();
        if (form.getTraineeIds() != null) {
            for (String id : form.getTraineeIds()) {
                if (StringUtil.isNotEmpty(id)) {
                    set.add(id.trim());
                }
            }
        }
        if (set.isEmpty() && StringUtil.isNotEmpty(form.getTrainees())) {
            set.addAll(splitTrainees(form.getTrainees()));
        }
        return new ArrayList<>(set);
    }
    private static List<String> splitTrainees(String raw) {
        if (StringUtil.isEmpty(raw)) {
            return new ArrayList<>();
        }
        return Arrays.stream(raw.split("[,;,;\\s]+"))
                .map(String::trim)
                .filter(s -> !s.isEmpty())
                .distinct()
                .collect(Collectors.toList());
    }
    private static String joinTrainees(List<String> ids) {
        if (ids == null || ids.isEmpty()) {
            return null;
        }
        return ids.stream().filter(Objects::nonNull).map(String::trim).filter(s -> !s.isEmpty())
                .distinct().collect(Collectors.joining(","));
    }
    private static boolean sameDay(Date a, Date b) {
        LocalDate da = a.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate db = b.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        return da.equals(db);
    }
    private static Date parseDate(String text) {
        if (StringUtil.isEmpty(text)) {
            return null;
        }
        String raw = text.trim();
        try {
            if (raw.matches("^\\d{10,13}$")) {
                long ts = Long.parseLong(raw);
                if (raw.length() == 10) {
                    ts = ts * 1000L;
                }
                return new Date(ts);
            }
            return DateUtil.stringToDate(raw);
        } catch (Exception ex) {
            throw new DataException("时间格式无效:" + text);
        }
    }
    private static Date parseDayStart(String day) {
        if (StringUtil.isEmpty(day) || "null".equalsIgnoreCase(day)) {
            return null;
        }
        String raw = day.trim();
        if (raw.length() == 10) {
            raw = raw + " 00:00:00";
        }
        return parseDate(raw);
    }
    private static Date parseDayEnd(String day) {
        if (StringUtil.isEmpty(day) || "null".equalsIgnoreCase(day)) {
            return null;
        }
        String raw = day.trim();
        if (raw.length() == 10) {
            raw = raw + " 23:59:59";
        }
        return parseDate(raw);
    }
    private BigDecimal resolveTaskHours(TmsTaskEntity task) {
        if (task == null || StringUtil.isEmpty(task.getSourceItemId())) {
            return null;
        }
        if ("annual".equals(task.getSourceType())) {
            TmsAnnualPlanItemEntity item = annualPlanItemMapper.selectById(task.getSourceItemId());
            return item == null ? null : item.getHours();
        }
        if ("post".equals(task.getSourceType())) {
            TmsPostPlanItemEntity item = postPlanItemMapper.selectById(task.getSourceItemId());
            return item == null ? null : item.getHours();
        }
        return null;
    }
    private static String formatDate(Date date) {
        if (date == null) {
            return null;
        }
        return DateUtil.dateToString(date, FMT);
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainArchiveServiceImpl.java
New file
@@ -0,0 +1,268 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jnpf.base.UserInfo;
import jnpf.exception.DataException;
import jnpf.permission.OrganizeApi;
import jnpf.permission.PositionApi;
import jnpf.permission.entity.OrganizeEntity;
import jnpf.permission.entity.PositionEntity;
import jnpf.tmsEntity.TmsPersonFileEntity;
import jnpf.tmsEntity.TmsPersonTrainEntity;
import jnpf.tmsEntity.TmsPersonWorkEntity;
import jnpf.tmsEntity.archive.TmsTrainArchiveCatalogVO;
import jnpf.tmsEntity.archive.TmsTrainArchiveDetailVO;
import jnpf.tmsEntity.archive.TmsTrainArchiveProfileVO;
import jnpf.tmsEntity.archive.TmsTrainArchiveWorkVO;
import jnpf.tmsMapper.TmsPersonFileMapper;
import jnpf.tmsMapper.TmsPersonTrainMapper;
import jnpf.tmsMapper.TmsPersonWorkMapper;
import jnpf.tmsService.TmsTrainArchiveService;
import jnpf.util.DateUtil;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class TmsTrainArchiveServiceImpl implements TmsTrainArchiveService {
    private final TmsPersonFileMapper personFileMapper;
    private final TmsPersonWorkMapper personWorkMapper;
    private final TmsPersonTrainMapper personTrainMapper;
    private final OrganizeApi organizeApi;
    private final PositionApi positionApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public TmsTrainArchiveDetailVO getMine() {
        UserInfo user = UserProvider.getUser();
        if (user == null || StringUtil.isEmpty(user.getUserId())) {
            throw new DataException("未获取到登录用户");
        }
        String userId = user.getUserId();
        TmsPersonFileEntity file = findByUserId(userId);
        if (file == null) {
            file = createArchive(user);
        }
        TmsTrainArchiveDetailVO detail = new TmsTrainArchiveDetailVO();
        detail.setProfile(toProfile(file, user));
        detail.setWorkList(loadWorkList(file.getId()));
        detail.setCatalogList(loadCatalogList(file.getId(), userId));
        return detail;
    }
    private TmsPersonFileEntity findByUserId(String userId) {
        LambdaQueryWrapper<TmsPersonFileEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsPersonFileEntity::getUserId, userId);
        qw.last("LIMIT 1");
        return personFileMapper.selectOne(qw);
    }
    private TmsPersonFileEntity createArchive(UserInfo user) {
        Date now = new Date();
        TmsPersonFileEntity entity = new TmsPersonFileEntity();
        entity.setId(RandomUtil.uuId());
        entity.setArchiveNo(nextArchiveNo());
        entity.setArchiveDate(now);
        entity.setUserId(user.getUserId());
        entity.setUserName(user.getUserName());
        entity.setDeptId(user.getOrganizeId());
        List<String> positionIds = normalizeIds(user.getPositionIds());
        if (!positionIds.isEmpty()) {
            entity.setMainPostId(positionIds.get(0));
            if (positionIds.size() > 1) {
                entity.setPartPostId(positionIds.get(1));
            }
        }
        entity.setBizStatus("valid");
        entity.setCreatorUserId(user.getUserId());
        entity.setCreatorTime(now);
        personFileMapper.insert(entity);
        return entity;
    }
    private List<String> normalizeIds(Object raw) {
        if (raw == null) {
            return Collections.emptyList();
        }
        if (raw instanceof List) {
            List<?> list = (List<?>) raw;
            List<String> out = new ArrayList<>();
            for (Object o : list) {
                if (o != null && StringUtil.isNotEmpty(String.valueOf(o))) {
                    out.add(String.valueOf(o));
                }
            }
            return out;
        }
        if (raw instanceof String[]) {
            List<String> out = new ArrayList<>();
            for (String s : (String[]) raw) {
                if (StringUtil.isNotEmpty(s)) {
                    out.add(s);
                }
            }
            return out;
        }
        if (raw instanceof String && StringUtil.isNotEmpty((String) raw)) {
            return Collections.singletonList((String) raw);
        }
        return Collections.emptyList();
    }
    /** 档案编号:AR + 年份 + 4 位流水 */
    private String nextArchiveNo() {
        int year = Calendar.getInstance().get(Calendar.YEAR);
        String prefix = "AR" + year;
        LambdaQueryWrapper<TmsPersonFileEntity> qw = new LambdaQueryWrapper<>();
        qw.likeRight(TmsPersonFileEntity::getArchiveNo, prefix);
        qw.orderByDesc(TmsPersonFileEntity::getArchiveNo);
        qw.last("LIMIT 1");
        TmsPersonFileEntity last = personFileMapper.selectOne(qw);
        int seq = 1;
        if (last != null && StringUtil.isNotEmpty(last.getArchiveNo()) && last.getArchiveNo().length() >= prefix.length() + 4) {
            try {
                seq = Integer.parseInt(last.getArchiveNo().substring(prefix.length())) + 1;
            } catch (NumberFormatException ignored) {
                seq = 1;
            }
        }
        return prefix + String.format("%04d", Math.min(seq, 9999));
    }
    private TmsTrainArchiveProfileVO toProfile(TmsPersonFileEntity file, UserInfo user) {
        TmsTrainArchiveProfileVO vo = new TmsTrainArchiveProfileVO();
        vo.setId(file.getId());
        vo.setArchiveNo(file.getArchiveNo());
        vo.setArchiveDate(file.getArchiveDate() == null ? null : DateUtil.dateToString(file.getArchiveDate(), "yyyy-MM-dd"));
        vo.setUserId(file.getUserId());
        vo.setUserName(StringUtil.isNotEmpty(file.getUserName()) ? file.getUserName() : user.getUserName());
        vo.setDeptName(resolveDeptName(file.getDeptId(), user));
        vo.setMainPostName(resolvePostName(file.getMainPostId(), user, true));
        vo.setPartPostName(resolvePostName(file.getPartPostId(), user, false));
        return vo;
    }
    private String resolveDeptName(String deptId, UserInfo user) {
        if (StringUtil.isNotEmpty(deptId)) {
            try {
                OrganizeEntity org = organizeApi.getInfoById(deptId);
                if (org != null && StringUtil.isNotEmpty(org.getFullName())) {
                    return org.getFullName();
                }
            } catch (Exception e) {
                log.debug("resolve dept name failed: {}", e.getMessage());
            }
        }
        if (user != null && StringUtil.isNotEmpty(user.getOrganize())) {
            return user.getOrganize();
        }
        return "无";
    }
    private String resolvePostName(String postId, UserInfo user, boolean main) {
        if (StringUtil.isNotEmpty(postId)) {
            try {
                PositionEntity pos = positionApi.queryInfoById(postId);
                if (pos != null && StringUtil.isNotEmpty(pos.getFullName())) {
                    return pos.getFullName();
                }
            } catch (Exception e) {
                log.debug("resolve post name failed: {}", e.getMessage());
            }
        }
        if (main && user != null) {
            List<String> positionIds = normalizeIds(user.getPositionIds());
            if (!positionIds.isEmpty()) {
                try {
                    List<PositionEntity> list = positionApi.getListByIds(positionIds);
                    if (list != null && !list.isEmpty() && StringUtil.isNotEmpty(list.get(0).getFullName())) {
                        return list.get(0).getFullName();
                    }
                } catch (Exception e) {
                    log.debug("resolve main post from user failed: {}", e.getMessage());
                }
            }
        }
        return "无";
    }
    private List<TmsTrainArchiveWorkVO> loadWorkList(String archiveId) {
        LambdaQueryWrapper<TmsPersonWorkEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsPersonWorkEntity::getForeignId, archiveId);
        qw.orderByAsc(TmsPersonWorkEntity::getHireDate);
        List<TmsPersonWorkEntity> rows = personWorkMapper.selectList(qw);
        if (rows == null || rows.isEmpty()) {
            return Collections.emptyList();
        }
        List<TmsTrainArchiveWorkVO> list = new ArrayList<>(rows.size());
        for (TmsPersonWorkEntity row : rows) {
            TmsTrainArchiveWorkVO vo = new TmsTrainArchiveWorkVO();
            vo.setId(row.getId());
            vo.setCompanyName(row.getCompanyName());
            vo.setPostName(row.getPostName());
            vo.setHireDate(row.getHireDate() == null ? null : DateUtil.dateToString(row.getHireDate(), "yyyy-MM-dd"));
            vo.setLeaveDate(row.getLeaveDate() == null ? null : DateUtil.dateToString(row.getLeaveDate(), "yyyy-MM-dd"));
            list.add(vo);
        }
        return list;
    }
    private List<TmsTrainArchiveCatalogVO> loadCatalogList(String archiveId, String userId) {
        LambdaQueryWrapper<TmsPersonTrainEntity> qw = new LambdaQueryWrapper<>();
        qw.and(w -> w.eq(TmsPersonTrainEntity::getArchiveId, archiveId)
                .or()
                .eq(TmsPersonTrainEntity::getUserId, userId));
        qw.orderByDesc(TmsPersonTrainEntity::getStartTime);
        List<TmsPersonTrainEntity> rows = personTrainMapper.selectList(qw);
        if (rows == null || rows.isEmpty()) {
            return Collections.emptyList();
        }
        // 去重:同一 id 只保留一条
        return rows.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(TmsPersonTrainEntity::getId, r -> r, (a, b) -> a))
                .values()
                .stream()
                .sorted((a, b) -> {
                    Date ta = a.getStartTime();
                    Date tb = b.getStartTime();
                    if (ta == null && tb == null) return 0;
                    if (ta == null) return 1;
                    if (tb == null) return -1;
                    return tb.compareTo(ta);
                })
                .map(this::toCatalog)
                .collect(Collectors.toList());
    }
    private TmsTrainArchiveCatalogVO toCatalog(TmsPersonTrainEntity row) {
        TmsTrainArchiveCatalogVO vo = new TmsTrainArchiveCatalogVO();
        vo.setId(row.getId());
        vo.setRecordNo(row.getRecordNo());
        vo.setTrainType(row.getCategory());
        vo.setTrainContent(row.getSubject());
        vo.setTrainMode(row.getTrainMode());
        vo.setTrainerName(row.getTeacherId());
        vo.setEvalMode(row.getEvalMode());
        vo.setPassFlag(row.getPassFlag() == null ? "" : row.getPassFlag());
        vo.setTrainStart(row.getStartTime() == null ? null : DateUtil.dateToString(row.getStartTime(), "yyyy-MM-dd HH:mm"));
        vo.setTrainEnd(row.getEndTime() == null ? null : DateUtil.dateToString(row.getEndTime(), "yyyy-MM-dd HH:mm"));
        return vo;
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainModeServiceImpl.java
New file
@@ -0,0 +1,174 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jnpf.exception.DataException;
import jnpf.tmsEntity.TmsTrainModeEntity;
import jnpf.tmsEntity.trainmode.TmsTrainModeForm;
import jnpf.tmsEntity.trainmode.TmsTrainModeQuery;
import jnpf.tmsMapper.TmsTrainModeMapper;
import jnpf.tmsService.TmsTrainModeService;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TmsTrainModeServiceImpl implements TmsTrainModeService {
    private final TmsTrainModeMapper trainModeMapper;
    @Override
    public List<TmsTrainModeForm> getList(TmsTrainModeQuery query) {
        LambdaQueryWrapper<TmsTrainModeEntity> qw = new LambdaQueryWrapper<>();
        if (StringUtil.isNotEmpty(query.getEnabled())) {
            qw.eq(TmsTrainModeEntity::getEnabled, query.getEnabled());
        }
        if (StringUtil.isNotEmpty(query.getKeyword())) {
            String kw = query.getKeyword().trim();
            qw.and(w -> w.like(TmsTrainModeEntity::getModeCode, kw)
                    .or()
                    .like(TmsTrainModeEntity::getModeName, kw));
        }
        qw.orderByAsc(TmsTrainModeEntity::getSortNo);
        qw.orderByDesc(TmsTrainModeEntity::getCreatorTime);
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        Page<TmsTrainModeEntity> page = trainModeMapper.selectPage(new Page<>(current, size), qw);
        List<TmsTrainModeForm> list = new ArrayList<>(page.getRecords().size());
        for (TmsTrainModeEntity row : page.getRecords()) {
            list.add(toForm(row));
        }
        query.setData(list, page.getTotal());
        return list;
    }
    @Override
    public TmsTrainModeForm getInfo(String id) {
        TmsTrainModeEntity entity = requireOne(id);
        return toForm(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String create(TmsTrainModeForm form) {
        validateForm(form, null);
        Date now = new Date();
        TmsTrainModeEntity entity = new TmsTrainModeEntity();
        entity.setId(RandomUtil.uuId());
        applyForm(entity, form);
        entity.setCreatorUserId(UserProvider.getLoginUserId());
        entity.setCreatorTime(now);
        trainModeMapper.insert(entity);
        return entity.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(String id, TmsTrainModeForm form) {
        TmsTrainModeEntity entity = requireOne(id);
        validateForm(form, id);
        // 编码不允许改
        form.setModeCode(entity.getModeCode());
        applyForm(entity, form);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        trainModeMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void setEnabled(String id, String enabled) {
        if (!"0".equals(enabled) && !"1".equals(enabled)) {
            throw new DataException("启用状态无效");
        }
        TmsTrainModeEntity entity = requireOne(id);
        entity.setEnabled(enabled);
        entity.setLastModifyUserId(UserProvider.getLoginUserId());
        entity.setLastModifyTime(new Date());
        trainModeMapper.updateById(entity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete(String id) {
        TmsTrainModeEntity entity = requireOne(id);
        if (!"0".equals(entity.getEnabled())) {
            throw new DataException("请先停用后再删除");
        }
        trainModeMapper.deleteById(id);
    }
    private TmsTrainModeEntity requireOne(String id) {
        if (StringUtil.isEmpty(id)) {
            throw new DataException("培训方式不存在");
        }
        TmsTrainModeEntity entity = trainModeMapper.selectById(id);
        if (entity == null) {
            throw new DataException("培训方式不存在");
        }
        return entity;
    }
    private void validateForm(TmsTrainModeForm form, String excludeId) {
        if (form == null || StringUtil.isEmpty(form.getModeCode())) {
            throw new DataException("请填写方式编码");
        }
        if (StringUtil.isEmpty(form.getModeName())) {
            throw new DataException("请填写方式名称");
        }
        String code = form.getModeCode().trim();
        form.setModeCode(code);
        LambdaQueryWrapper<TmsTrainModeEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsTrainModeEntity::getModeCode, code);
        if (StringUtil.isNotEmpty(excludeId)) {
            qw.ne(TmsTrainModeEntity::getId, excludeId);
        }
        if (trainModeMapper.selectCount(qw) > 0) {
            throw new DataException("方式编码已存在");
        }
    }
    private void applyForm(TmsTrainModeEntity entity, TmsTrainModeForm form) {
        entity.setModeCode(form.getModeCode().trim());
        entity.setModeName(form.getModeName().trim());
        entity.setSignRule(StringUtil.isNotEmpty(form.getSignRule()) ? form.getSignRule() : "both");
        entity.setAllowGuestSign(yn(form.getAllowGuestSign(), "0"));
        entity.setSameDayRequired(yn(form.getSameDayRequired(), "0"));
        entity.setPlaceRequired(yn(form.getPlaceRequired(), "1"));
        entity.setSortNo(form.getSortNo() == null ? 0 : form.getSortNo());
        entity.setEnabled(yn(form.getEnabled(), "1"));
        entity.setRemark(form.getRemark());
    }
    private static String yn(String v, String def) {
        if ("0".equals(v) || "1".equals(v)) {
            return v;
        }
        return def;
    }
    private TmsTrainModeForm toForm(TmsTrainModeEntity entity) {
        TmsTrainModeForm form = new TmsTrainModeForm();
        form.setId(entity.getId());
        form.setModeCode(entity.getModeCode());
        form.setModeName(entity.getModeName());
        form.setSignRule(entity.getSignRule());
        form.setAllowGuestSign(entity.getAllowGuestSign());
        form.setSameDayRequired(entity.getSameDayRequired());
        form.setPlaceRequired(entity.getPlaceRequired());
        form.setSortNo(entity.getSortNo());
        form.setEnabled(entity.getEnabled());
        form.setRemark(entity.getRemark());
        return form;
    }
}
jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainRecordServiceImpl.java
New file
@@ -0,0 +1,446 @@
package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jnpf.exception.DataException;
import jnpf.permission.OrganizeApi;
import jnpf.permission.PositionApi;
import jnpf.permission.UserApi;
import jnpf.permission.entity.OrganizeEntity;
import jnpf.permission.entity.PositionEntity;
import jnpf.permission.entity.UserEntity;
import jnpf.tmsEntity.TmsPersonFileEntity;
import jnpf.tmsEntity.TmsPersonTaskEntity;
import jnpf.tmsEntity.TmsPersonTrainEntity;
import jnpf.tmsEntity.TmsTaskEntity;
import jnpf.tmsEntity.trainrecord.TmsTrainRecordCatalogItemVO;
import jnpf.tmsEntity.trainrecord.TmsTrainRecordCatalogVO;
import jnpf.tmsEntity.trainrecord.TmsTrainRecordListVO;
import jnpf.tmsEntity.trainrecord.TmsTrainRecordQuery;
import jnpf.tmsMapper.TmsPersonFileMapper;
import jnpf.tmsMapper.TmsPersonTaskMapper;
import jnpf.tmsMapper.TmsPersonTrainMapper;
import jnpf.tmsMapper.TmsTaskMapper;
import jnpf.tmsService.TmsTrainRecordService;
import jnpf.util.DateUtil;
import jnpf.util.StringUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class TmsTrainRecordServiceImpl implements TmsTrainRecordService {
    private static final String DATE_FMT = "yyyy-MM-dd";
    private final TmsPersonFileMapper personFileMapper;
    private final TmsPersonTrainMapper personTrainMapper;
    private final TmsPersonTaskMapper personTaskMapper;
    private final TmsTaskMapper taskMapper;
    private final OrganizeApi organizeApi;
    private final PositionApi positionApi;
    private final UserApi userApi;
    @Override
    public List<TmsTrainRecordListVO> getList(TmsTrainRecordQuery query) {
        LambdaQueryWrapper<TmsPersonFileEntity> qw = new LambdaQueryWrapper<>();
        if (StringUtil.isNotEmpty(query.getArchiveNo()) && !"null".equalsIgnoreCase(query.getArchiveNo())) {
            qw.like(TmsPersonFileEntity::getArchiveNo, query.getArchiveNo().trim());
        }
        if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) {
            String kw = query.getKeyword().trim();
            qw.and(w -> w.like(TmsPersonFileEntity::getUserName, kw)
                    .or().like(TmsPersonFileEntity::getArchiveNo, kw)
                    .or().like(TmsPersonFileEntity::getDeptId, kw));
        }
        qw.orderByDesc(TmsPersonFileEntity::getArchiveDate)
                .orderByDesc(TmsPersonFileEntity::getCreatorTime);
        long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
        long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
        Page<TmsPersonFileEntity> page = personFileMapper.selectPage(new Page<>(current, size), qw);
        List<TmsPersonFileEntity> rows = page.getRecords();
        if (rows == null || rows.isEmpty()) {
            query.setData(Collections.emptyList(), page.getTotal());
            return Collections.emptyList();
        }
        Map<String, String> deptNames = loadDeptNames(rows.stream()
                .map(TmsPersonFileEntity::getDeptId)
                .filter(StringUtil::isNotEmpty)
                .collect(Collectors.toSet()));
        Map<String, String> postNames = loadPostNames(rows);
        List<TmsTrainRecordListVO> list = new ArrayList<>(rows.size());
        for (TmsPersonFileEntity row : rows) {
            list.add(toListVo(row, deptNames, postNames));
        }
        query.setData(list, page.getTotal());
        return list;
    }
    @Override
    public TmsTrainRecordCatalogVO getCatalog(String archiveId, String startDate, String endDate) {
        if (StringUtil.isEmpty(archiveId)) {
            throw new DataException("档案ID不能为空");
        }
        TmsPersonFileEntity file = personFileMapper.selectById(archiveId);
        if (file == null) {
            throw new DataException("个人培训记录不存在");
        }
        List<TmsTrainRecordCatalogItemVO> items = new ArrayList<>();
        Set<String> seenNos = new HashSet<>();
        // 1) 档案目录表(若已同步)
        for (TmsPersonTrainEntity row : loadPersonTrains(file)) {
            TmsTrainRecordCatalogItemVO item = fromPersonTrain(row);
            if (item == null || !inDateRange(item.getTrainDate(), startDate, endDate)) {
                continue;
            }
            if (StringUtil.isNotEmpty(item.getRecordNo())) {
                seenNos.add(item.getRecordNo());
            }
            items.add(item);
        }
        // 2) 个人培训任务(联调/日常主数据源)
        for (TmsTrainRecordCatalogItemVO item : loadFromPersonTasks(file.getUserId())) {
            if (item == null || !inDateRange(item.getTrainDate(), startDate, endDate)) {
                continue;
            }
            if (StringUtil.isNotEmpty(item.getRecordNo()) && seenNos.contains(item.getRecordNo())) {
                continue;
            }
            if (StringUtil.isNotEmpty(item.getRecordNo())) {
                seenNos.add(item.getRecordNo());
            }
            items.add(item);
        }
        items.sort(Comparator.comparing(TmsTrainRecordCatalogItemVO::getTrainDate,
                Comparator.nullsLast(Comparator.reverseOrder())));
        TmsTrainRecordCatalogVO vo = new TmsTrainRecordCatalogVO();
        vo.setId(file.getId());
        vo.setUserName(file.getUserName());
        vo.setArchiveNo(file.getArchiveNo());
        vo.setList(items);
        return vo;
    }
    private List<TmsPersonTrainEntity> loadPersonTrains(TmsPersonFileEntity file) {
        LambdaQueryWrapper<TmsPersonTrainEntity> qw = new LambdaQueryWrapper<>();
        qw.and(w -> w.eq(TmsPersonTrainEntity::getArchiveId, file.getId())
                .or()
                .eq(TmsPersonTrainEntity::getUserId, file.getUserId()));
        qw.orderByDesc(TmsPersonTrainEntity::getStartTime);
        List<TmsPersonTrainEntity> rows = personTrainMapper.selectList(qw);
        if (rows == null || rows.isEmpty()) {
            return Collections.emptyList();
        }
        return new ArrayList<>(rows.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(TmsPersonTrainEntity::getId, r -> r, (a, b) -> a, LinkedHashMap::new))
                .values());
    }
    private List<TmsTrainRecordCatalogItemVO> loadFromPersonTasks(String userId) {
        if (StringUtil.isEmpty(userId)) {
            return Collections.emptyList();
        }
        LambdaQueryWrapper<TmsPersonTaskEntity> qw = new LambdaQueryWrapper<>();
        qw.eq(TmsPersonTaskEntity::getUserId, userId);
        qw.ne(TmsPersonTaskEntity::getBizStatus, "cancelled");
        qw.orderByDesc(TmsPersonTaskEntity::getCreatorTime);
        List<TmsPersonTaskEntity> pts = personTaskMapper.selectList(qw);
        if (pts == null || pts.isEmpty()) {
            return Collections.emptyList();
        }
        Set<String> taskIds = pts.stream()
                .map(TmsPersonTaskEntity::getTaskId)
                .filter(StringUtil::isNotEmpty)
                .collect(Collectors.toSet());
        Map<String, TmsTaskEntity> taskMap = new HashMap<>();
        if (!taskIds.isEmpty()) {
            for (TmsTaskEntity t : taskMapper.selectBatchIds(taskIds)) {
                if (t != null) {
                    taskMap.put(t.getId(), t);
                }
            }
        }
        Set<String> teacherIds = taskMap.values().stream()
                .map(TmsTaskEntity::getTeacherId)
                .filter(StringUtil::isNotEmpty)
                .flatMap(s -> splitIds(s).stream())
                .collect(Collectors.toSet());
        Map<String, String> teacherNames = loadUserNames(teacherIds);
        List<TmsTrainRecordCatalogItemVO> list = new ArrayList<>(pts.size());
        for (TmsPersonTaskEntity pt : pts) {
            TmsTaskEntity task = taskMap.get(pt.getTaskId());
            TmsTrainRecordCatalogItemVO item = new TmsTrainRecordCatalogItemVO();
            item.setId(pt.getId());
            item.setRecordNo(StringUtil.isNotEmpty(pt.getTaskNo())
                    ? pt.getTaskNo()
                    : (task == null ? "" : task.getTaskNo()));
            if (task != null) {
                item.setTrainType(labelCategory(firstNonEmpty(task.getCategory(), task.getTrainType())));
                item.setTrainContent(task.getSubject());
                item.setTrainMode(labelTrainMode(task.getTrainMode()));
                item.setEvalMode(labelEvalMode(task.getEvalMode()));
                item.setTrainerName(resolveTeacherNames(task.getTeacherId(), teacherNames));
                item.setTrainDate(fmtDate(task.getStartTime()));
            } else {
                item.setTrainType("");
                item.setTrainContent(pt.getTaskNo());
                item.setTrainMode("");
                item.setEvalMode("");
                item.setTrainerName("");
                item.setTrainDate(fmtDate(pt.getSignTime() != null ? pt.getSignTime() : pt.getCreatorTime()));
            }
            if (pt.getExamScore() != null) {
                item.setTrainResult(pt.getExamScore().stripTrailingZeros());
            } else {
                item.setTrainResult("");
            }
            item.setPassFlag(pt.getPassFlag() == null ? "" : pt.getPassFlag());
            list.add(item);
        }
        return list;
    }
    private TmsTrainRecordCatalogItemVO fromPersonTrain(TmsPersonTrainEntity row) {
        TmsTrainRecordCatalogItemVO item = new TmsTrainRecordCatalogItemVO();
        item.setId(row.getId());
        item.setRecordNo(row.getRecordNo());
        item.setTrainType(labelCategory(row.getCategory()));
        item.setTrainContent(row.getSubject());
        item.setTrainMode(labelTrainMode(row.getTrainMode()));
        item.setEvalMode(labelEvalMode(row.getEvalMode()));
        item.setTrainerName(row.getTeacherId());
        item.setTrainResult("");
        item.setPassFlag(row.getPassFlag() == null ? "" : row.getPassFlag());
        item.setTrainDate(fmtDate(row.getStartTime()));
        return item;
    }
    private TmsTrainRecordListVO toListVo(TmsPersonFileEntity row,
                                          Map<String, String> deptNames,
                                          Map<String, String> postNames) {
        TmsTrainRecordListVO vo = new TmsTrainRecordListVO();
        vo.setId(row.getId());
        vo.setArchiveNo(row.getArchiveNo());
        Date post = row.getHireDate() != null ? row.getHireDate() : row.getArchiveDate();
        vo.setPostDate(fmtDate(post));
        vo.setUserName(row.getUserName());
        vo.setDeptName(deptNames.getOrDefault(row.getDeptId(), ""));
        vo.setPostName(postNames.getOrDefault(row.getMainPostId(), ""));
        vo.setPartPostName(postNames.getOrDefault(row.getPartPostId(), ""));
        vo.setMajor("");
        vo.setEducation("");
        vo.setTechTitle("");
        vo.setHireDate(fmtDate(row.getHireDate()));
        vo.setLeaveDate(fmtDate(row.getLeaveDate()));
        return vo;
    }
    private Map<String, String> loadDeptNames(Set<String> deptIds) {
        Map<String, String> map = new HashMap<>();
        if (deptIds == null || deptIds.isEmpty()) {
            return map;
        }
        for (String id : deptIds) {
            try {
                OrganizeEntity org = organizeApi.getInfoById(id);
                if (org != null && StringUtil.isNotEmpty(org.getFullName())) {
                    map.put(id, org.getFullName());
                }
            } catch (Exception e) {
                log.debug("resolve dept failed: {}", e.getMessage());
            }
        }
        return map;
    }
    private Map<String, String> loadPostNames(List<TmsPersonFileEntity> rows) {
        Set<String> ids = new HashSet<>();
        for (TmsPersonFileEntity row : rows) {
            if (StringUtil.isNotEmpty(row.getMainPostId())) {
                ids.add(row.getMainPostId());
            }
            if (StringUtil.isNotEmpty(row.getPartPostId())) {
                ids.add(row.getPartPostId());
            }
        }
        Map<String, String> map = new HashMap<>();
        if (ids.isEmpty()) {
            return map;
        }
        try {
            List<PositionEntity> list = positionApi.getListByIds(new ArrayList<>(ids));
            if (list != null) {
                for (PositionEntity p : list) {
                    if (p != null && StringUtil.isNotEmpty(p.getId())) {
                        map.put(p.getId(), p.getFullName());
                    }
                }
            }
        } catch (Exception e) {
            log.debug("resolve posts failed: {}", e.getMessage());
            for (String id : ids) {
                try {
                    PositionEntity p = positionApi.queryInfoById(id);
                    if (p != null) {
                        map.put(id, p.getFullName());
                    }
                } catch (Exception ignored) {
                    // ignore
                }
            }
        }
        return map;
    }
    private Map<String, String> loadUserNames(Set<String> userIds) {
        Map<String, String> map = new HashMap<>();
        if (userIds == null || userIds.isEmpty()) {
            return map;
        }
        try {
            List<UserEntity> users = userApi.getUserName(new ArrayList<>(userIds));
            if (users != null) {
                for (UserEntity u : users) {
                    if (u != null && StringUtil.isNotEmpty(u.getId())) {
                        map.put(u.getId(), StringUtil.isNotEmpty(u.getRealName()) ? u.getRealName() : u.getId());
                    }
                }
            }
        } catch (Exception e) {
            log.debug("resolve users failed: {}", e.getMessage());
        }
        return map;
    }
    private String resolveTeacherNames(String teacherId, Map<String, String> nameMap) {
        if (StringUtil.isEmpty(teacherId)) {
            return "";
        }
        List<String> ids = splitIds(teacherId);
        if (ids.isEmpty()) {
            return teacherId;
        }
        List<String> names = new ArrayList<>();
        for (String id : ids) {
            names.add(nameMap.getOrDefault(id, id));
        }
        return String.join("、", names);
    }
    private List<String> splitIds(String raw) {
        if (StringUtil.isEmpty(raw)) {
            return Collections.emptyList();
        }
        String[] parts = raw.split("[,,;;\\s]+");
        List<String> out = new ArrayList<>();
        for (String p : parts) {
            if (StringUtil.isNotEmpty(p)) {
                out.add(p.trim());
            }
        }
        return out;
    }
    private boolean inDateRange(String trainDate, String startDate, String endDate) {
        if (StringUtil.isEmpty(trainDate)) {
            return StringUtil.isEmpty(startDate) && StringUtil.isEmpty(endDate);
        }
        if (StringUtil.isNotEmpty(startDate) && trainDate.compareTo(startDate) < 0) {
            return false;
        }
        if (StringUtil.isNotEmpty(endDate) && trainDate.compareTo(endDate) > 0) {
            return false;
        }
        return true;
    }
    private String fmtDate(Date date) {
        return date == null ? null : DateUtil.dateToString(date, DATE_FMT);
    }
    private String firstNonEmpty(String a, String b) {
        if (StringUtil.isNotEmpty(a)) {
            return a;
        }
        return b == null ? "" : b;
    }
    private String labelCategory(String code) {
        if (StringUtil.isEmpty(code)) {
            return "";
        }
        switch (code) {
            case "annual_plan":
            case "annual":
                return "年度培训";
            case "temp":
            case "temporary":
                return "临时培训";
            case "post":
                return "岗位培训";
            default:
                return code;
        }
    }
    private String labelTrainMode(String code) {
        if (StringUtil.isEmpty(code)) {
            return "";
        }
        switch (code.toLowerCase()) {
            case "online":
                return "在线学习";
            case "onsite":
                return "集中授课";
            case "practice":
                return "实操培训";
            default:
                return code;
        }
    }
    private String labelEvalMode(String code) {
        if (StringUtil.isEmpty(code)) {
            return "";
        }
        switch (code.toLowerCase()) {
            case "exam":
                return "在线考试";
            case "none":
                return "无需考核";
            case "quiz":
                return "随堂测验";
            case "practice":
                return "实操考核";
            default:
                return code;
        }
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsCourseController.java
New file
@@ -0,0 +1,85 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.course.TmsCourseForm;
import jnpf.tmsEntity.course.TmsCourseQuery;
import jnpf.tmsEntity.course.TmsPaperOption;
import jnpf.tmsService.TmsCourseService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
 * 培训课程。网关 /api/tms/course/** (StripPrefix=2)→ /course/**
 */
@Tag(name = "TMS培训课程", description = "Course")
@RestController
@RequestMapping("/course")
@RequiredArgsConstructor
public class TmsCourseController {
    private final TmsCourseService courseService;
    @Operation(summary = "培训课程分页列表")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsCourseForm>> list(TmsCourseQuery query) {
        List<TmsCourseForm> list = courseService.getList(query);
        PageListVO<TmsCourseForm> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "关联试卷下拉")
    @GetMapping("/paper-options")
    public ActionResult<List<TmsPaperOption>> paperOptions() {
        return ActionResult.success(courseService.paperOptions());
    }
    @Operation(summary = "培训课程详情")
    @GetMapping("/{id}")
    public ActionResult<TmsCourseForm> info(@PathVariable("id") String id) {
        return ActionResult.success(courseService.getInfo(id));
    }
    @Operation(summary = "新增培训课程")
    @PostMapping
    public ActionResult<String> create(@RequestBody TmsCourseForm form) {
        return ActionResult.success(courseService.create(form));
    }
    @Operation(summary = "更新培训课程")
    @PutMapping("/{id}")
    public ActionResult<String> update(@PathVariable("id") String id, @RequestBody TmsCourseForm form) {
        courseService.update(id, form);
        return ActionResult.success("更新成功");
    }
    @Operation(summary = "启用/停用")
    @PutMapping("/{id}/enabled")
    public ActionResult<String> enabled(@PathVariable("id") String id, @RequestBody Map<String, String> body) {
        courseService.setEnabled(id, body == null ? null : body.get("enabled"));
        return ActionResult.success("操作成功");
    }
    @Operation(summary = "删除培训课程")
    @DeleteMapping("/{id}")
    public ActionResult<String> delete(@PathVariable("id") String id) {
        courseService.delete(id);
        return ActionResult.success("删除成功");
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsEvalModeController.java
New file
@@ -0,0 +1,78 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.evalmode.TmsEvalModeForm;
import jnpf.tmsEntity.evalmode.TmsEvalModeQuery;
import jnpf.tmsService.TmsEvalModeService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
 * 考核方式。网关 /api/tms/eval-mode/** (StripPrefix=2)→ /eval-mode/**
 */
@Tag(name = "TMS考核方式", description = "EvalMode")
@RestController
@RequestMapping("/eval-mode")
@RequiredArgsConstructor
public class TmsEvalModeController {
    private final TmsEvalModeService evalModeService;
    @Operation(summary = "考核方式分页列表")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsEvalModeForm>> list(TmsEvalModeQuery query) {
        List<TmsEvalModeForm> list = evalModeService.getList(query);
        PageListVO<TmsEvalModeForm> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "考核方式详情")
    @GetMapping("/{id}")
    public ActionResult<TmsEvalModeForm> info(@PathVariable("id") String id) {
        return ActionResult.success(evalModeService.getInfo(id));
    }
    @Operation(summary = "新增考核方式")
    @PostMapping
    public ActionResult<String> create(@RequestBody TmsEvalModeForm form) {
        return ActionResult.success(evalModeService.create(form));
    }
    @Operation(summary = "更新考核方式")
    @PutMapping("/{id}")
    public ActionResult<String> update(@PathVariable("id") String id, @RequestBody TmsEvalModeForm form) {
        evalModeService.update(id, form);
        return ActionResult.success("更新成功");
    }
    @Operation(summary = "启用/停用")
    @PutMapping("/{id}/enabled")
    public ActionResult<String> enabled(@PathVariable("id") String id, @RequestBody Map<String, String> body) {
        evalModeService.setEnabled(id, body == null ? null : body.get("enabled"));
        return ActionResult.success("操作成功");
    }
    @Operation(summary = "删除考核方式")
    @DeleteMapping("/{id}")
    public ActionResult<String> delete(@PathVariable("id") String id) {
        evalModeService.delete(id);
        return ActionResult.success("删除成功");
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamGradeController.java
New file
@@ -0,0 +1,76 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeDetailVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeExamVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeQuery;
import jnpf.tmsEntity.examgrade.TmsExamGradeSessionVO;
import jnpf.tmsEntity.examgrade.TmsExamGradeSubmitForm;
import jnpf.tmsEntity.examgrade.TmsExamGradeSubmitResultVO;
import jnpf.tmsService.TmsExamGradeService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * 考试阅卷。网关 /api/tms/exam-grade/** (StripPrefix=2)→ /exam-grade/**
 */
@Tag(name = "TMS考试阅卷", description = "ExamGrade")
@RestController
@RequestMapping("/exam-grade")
@RequiredArgsConstructor
public class TmsExamGradeController {
    private final TmsExamGradeService examGradeService;
    @Operation(summary = "阅卷场次列表")
    @GetMapping("/sessions")
    public ActionResult<PageListVO<TmsExamGradeSessionVO>> sessions(TmsExamGradeQuery query) {
        List<TmsExamGradeSessionVO> list = examGradeService.getSessionList(query);
        PageListVO<TmsExamGradeSessionVO> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "阅卷场次信息")
    @GetMapping("/sessions/{id}")
    public ActionResult<TmsExamGradeSessionVO> session(@PathVariable("id") String id) {
        return ActionResult.success(examGradeService.getSession(id));
    }
    @Operation(summary = "场次下答卷列表")
    @GetMapping("/sessions/{id}/exams")
    public ActionResult<List<TmsExamGradeExamVO>> exams(@PathVariable("id") String id) {
        return ActionResult.success(examGradeService.getExamList(id));
    }
    @Operation(summary = "答卷阅卷详情")
    @GetMapping("/exams/{examId}")
    public ActionResult<TmsExamGradeDetailVO> examDetail(@PathVariable("examId") String examId) {
        return ActionResult.success(examGradeService.getExamDetail(examId));
    }
    @Operation(summary = "提交阅卷")
    @PostMapping("/exams/{examId}/submit")
    public ActionResult<TmsExamGradeSubmitResultVO> submit(
            @PathVariable("examId") String examId,
            @RequestBody TmsExamGradeSubmitForm form) {
        if (form == null) {
            form = new TmsExamGradeSubmitForm();
        }
        form.setExamId(examId);
        return ActionResult.success(examGradeService.submitGrade(form));
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamScoreController.java
New file
@@ -0,0 +1,65 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.examscore.TmsExamScoreDetailVO;
import jnpf.tmsEntity.examscore.TmsExamScoreExportForm;
import jnpf.tmsEntity.examscore.TmsExamScorePaperVO;
import jnpf.tmsEntity.examscore.TmsExamScoreQuery;
import jnpf.tmsEntity.examscore.TmsExamScoreRowVO;
import jnpf.tmsEntity.examscore.TmsExamScoreSummaryVO;
import jnpf.tmsService.TmsExamScoreService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * 考试成绩。网关 /api/tms/exam-score/** (StripPrefix=2)→ /exam-score/**
 */
@Tag(name = "TMS考试成绩", description = "ExamScore")
@RestController
@RequestMapping("/exam-score")
@RequiredArgsConstructor
public class TmsExamScoreController {
    private final TmsExamScoreService examScoreService;
    @Operation(summary = "考试成绩汇总列表")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsExamScoreSummaryVO>> list(TmsExamScoreQuery query) {
        List<TmsExamScoreSummaryVO> list = examScoreService.getList(query);
        PageListVO<TmsExamScoreSummaryVO> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "考试成绩详情(考生列表)")
    @GetMapping("/{id}")
    public ActionResult<TmsExamScoreDetailVO> detail(@PathVariable("id") String id) {
        return ActionResult.success(examScoreService.getDetail(id));
    }
    @Operation(summary = "按考核对象导出已勾选试卷成绩")
    @PostMapping("/export")
    public ActionResult<List<TmsExamScoreRowVO>> export(@RequestBody TmsExamScoreExportForm form) {
        List<String> ids = form == null ? null : form.getIds();
        return ActionResult.success(examScoreService.exportRows(ids));
    }
    @Operation(summary = "查看答卷")
    @GetMapping("/paper/{examId}")
    public ActionResult<TmsExamScorePaperVO> paper(@PathVariable("examId") String examId) {
        return ActionResult.success(examScoreService.getPaperView(examId));
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsOnlineExamController.java
New file
@@ -0,0 +1,80 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.exam.TmsOnlineExamDetailVO;
import jnpf.tmsEntity.exam.TmsOnlineExamListVO;
import jnpf.tmsEntity.exam.TmsOnlineExamPaperVO;
import jnpf.tmsEntity.exam.TmsOnlineExamQuery;
import jnpf.tmsEntity.exam.TmsOnlineExamStartForm;
import jnpf.tmsEntity.exam.TmsOnlineExamSubmitForm;
import jnpf.tmsEntity.exam.TmsOnlineExamSubmitResultVO;
import jnpf.tmsService.TmsOnlineExamService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * 在线考试。网关 /api/tms/online-exam/** (StripPrefix=2)→ /online-exam/**
 */
@Tag(name = "TMS在线考试", description = "OnlineExam")
@RestController
@RequestMapping("/online-exam")
@RequiredArgsConstructor
public class TmsOnlineExamController {
    private final TmsOnlineExamService onlineExamService;
    @Operation(summary = "我的试卷")
    @GetMapping("/my-papers")
    public ActionResult<PageListVO<TmsOnlineExamListVO>> myPapers(TmsOnlineExamQuery query) {
        List<TmsOnlineExamListVO> list = onlineExamService.getMyPapers(query);
        PageListVO<TmsOnlineExamListVO> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "试卷考试详情")
    @GetMapping("/my-papers/{id}")
    public ActionResult<TmsOnlineExamDetailVO> detail(@PathVariable("id") String id) {
        return ActionResult.success(onlineExamService.getDetail(id));
    }
    @Operation(summary = "按答卷ID查看考试回顾")
    @GetMapping("/exams/{examId}")
    public ActionResult<TmsOnlineExamDetailVO> examReview(@PathVariable("examId") String examId) {
        return ActionResult.success(onlineExamService.getExamReview(examId));
    }
    @Operation(summary = "开始或继续考试")
    @PostMapping("/start")
    public ActionResult<TmsOnlineExamPaperVO> start(@RequestBody TmsOnlineExamStartForm form) {
        String paperId = form == null ? null : form.getMyPaperId();
        return ActionResult.success(onlineExamService.start(paperId));
    }
    @Operation(summary = "暂存作答")
    @PostMapping("/{id}/save")
    public ActionResult<String> save(@PathVariable("id") String id, @RequestBody TmsOnlineExamSubmitForm form) {
        onlineExamService.saveAnswers(id, form);
        return ActionResult.success("暂存成功");
    }
    @Operation(summary = "交卷")
    @PostMapping("/{id}/submit")
    public ActionResult<TmsOnlineExamSubmitResultVO> submit(@PathVariable("id") String id,
                                                            @RequestBody TmsOnlineExamSubmitForm form) {
        return ActionResult.success(onlineExamService.submit(id, form));
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPaperController.java
New file
@@ -0,0 +1,77 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.paper.TmsPaperForm;
import jnpf.tmsEntity.paper.TmsPaperQuery;
import jnpf.tmsService.TmsPaperService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
 * 试卷管理(含组卷)。网关 /api/tms/paper/** (StripPrefix=2)→ /paper/**
 */
@Tag(name = "TMS试卷", description = "Paper")
@RestController
@RequestMapping("/paper")
@RequiredArgsConstructor
public class TmsPaperController {
    private final TmsPaperService paperService;
    @Operation(summary = "试卷分页列表")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsPaperForm>> list(TmsPaperQuery query) {
        List<TmsPaperForm> list = paperService.getList(query);
        PageListVO<TmsPaperForm> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "试卷详情(含章节与试题)")
    @GetMapping("/{id}")
    public ActionResult<TmsPaperForm> info(@PathVariable("id") String id) {
        return ActionResult.success(paperService.getInfo(id));
    }
    @Operation(summary = "新增试卷(组卷)")
    @PostMapping
    public ActionResult<String> create(@RequestBody TmsPaperForm form) {
        return ActionResult.success(paperService.create(form));
    }
    @Operation(summary = "更新试卷(组卷)")
    @PutMapping("/{id}")
    public ActionResult<String> update(@PathVariable("id") String id, @RequestBody TmsPaperForm form) {
        paperService.update(id, form);
        return ActionResult.success("更新成功");
    }
    @Operation(summary = "废弃试卷")
    @PutMapping("/{id}/invalidate")
    public ActionResult<String> invalidate(@PathVariable("id") String id) {
        paperService.invalidate(id);
        return ActionResult.success("废弃成功");
    }
    @Operation(summary = "启用/停用试卷(开放/不开放)")
    @PutMapping("/{id}/status")
    public ActionResult<String> status(@PathVariable("id") String id, @RequestBody Map<String, String> body) {
        paperService.setBizStatus(id, body == null ? null : body.get("bizStatus"));
        return ActionResult.success("操作成功");
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPersonTaskController.java
New file
@@ -0,0 +1,65 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.person.TmsPersonTaskLearnForm;
import jnpf.tmsEntity.person.TmsPersonTaskLearnResult;
import jnpf.tmsEntity.person.TmsPersonTaskMineForm;
import jnpf.tmsEntity.person.TmsPersonTaskQuery;
import jnpf.tmsEntity.person.TmsPersonTaskSignResult;
import jnpf.tmsService.TmsPersonTaskService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * 个人培训任务(学员端)。网关 /api/tms/person-task/** (StripPrefix=2)→ /person-task/**
 */
@Tag(name = "TMS个人培训任务", description = "PersonTask")
@RestController
@RequestMapping("/person-task")
@RequiredArgsConstructor
public class TmsPersonTaskController {
    private final TmsPersonTaskService personTaskService;
    @Operation(summary = "我的个人培训任务")
    @GetMapping("/mine")
    public ActionResult<PageListVO<TmsPersonTaskMineForm>> mine(TmsPersonTaskQuery query) {
        List<TmsPersonTaskMineForm> list = personTaskService.getMine(query);
        PageListVO<TmsPersonTaskMineForm> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "个人培训任务详情")
    @GetMapping("/{id}")
    public ActionResult<TmsPersonTaskMineForm> info(@PathVariable("id") String id) {
        return ActionResult.success(personTaskService.getInfo(id));
    }
    @Operation(summary = "在线签到")
    @PostMapping("/{id}/sign")
    public ActionResult<TmsPersonTaskSignResult> sign(@PathVariable("id") String id) {
        return ActionResult.success(personTaskService.sign(id));
    }
    @Operation(summary = "累计学习时长(学满后系统自动确认完成并提示)")
    @PostMapping("/{id}/learn")
    public ActionResult<TmsPersonTaskLearnResult> learn(
            @PathVariable("id") String id,
            @RequestBody(required = false) TmsPersonTaskLearnForm form) {
        return ActionResult.success(personTaskService.markLearned(id, form == null ? null : form.getSeconds()));
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPlaceController.java
New file
@@ -0,0 +1,31 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.tmsEntity.place.TmsPlaceOption;
import jnpf.tmsService.TmsPlaceService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * 培训地点。网关 /api/tms/place/** → /place/**
 */
@Tag(name = "TMS培训地点", description = "Place")
@RestController
@RequestMapping("/place")
@RequiredArgsConstructor
public class TmsPlaceController {
    private final TmsPlaceService placeService;
    @Operation(summary = "启用地点下拉")
    @GetMapping("/options")
    public ActionResult<List<TmsPlaceOption>> options() {
        return ActionResult.success(placeService.listOptions());
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsQuestionController.java
New file
@@ -0,0 +1,91 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.question.TmsQuestionBankOption;
import jnpf.tmsEntity.question.TmsQuestionForm;
import jnpf.tmsEntity.question.TmsQuestionQuery;
import jnpf.tmsService.TmsQuestionService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
 * 试题 CRUD。网关 /api/tms/question/** (StripPrefix=2)→ /question/**
 */
@Tag(name = "TMS试题", description = "Question")
@RestController
@RequestMapping("/question")
@RequiredArgsConstructor
public class TmsQuestionController {
    private final TmsQuestionService questionService;
    @Operation(summary = "题库下拉")
    @GetMapping("/banks")
    public ActionResult<List<TmsQuestionBankOption>> banks() {
        return ActionResult.success(questionService.listBanks());
    }
    @Operation(summary = "管理员下拉")
    @GetMapping("/admins")
    public ActionResult<List<Map<String, String>>> admins() {
        return ActionResult.success(questionService.listAdmins());
    }
    @Operation(summary = "试题分页列表")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsQuestionForm>> list(TmsQuestionQuery query) {
        List<TmsQuestionForm> list = questionService.getList(query);
        PageListVO<TmsQuestionForm> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "试题详情")
    @GetMapping("/{id}")
    public ActionResult<TmsQuestionForm> info(@PathVariable("id") String id) {
        return ActionResult.success(questionService.getInfo(id));
    }
    @Operation(summary = "新增试题")
    @PostMapping
    public ActionResult<String> create(@RequestBody TmsQuestionForm form) {
        return ActionResult.success(questionService.create(form));
    }
    @Operation(summary = "更新试题")
    @PutMapping("/{id}")
    public ActionResult<String> update(@PathVariable("id") String id, @RequestBody TmsQuestionForm form) {
        questionService.update(id, form);
        return ActionResult.success("更新成功");
    }
    @Operation(summary = "废弃试题(逻辑删除)")
    @PutMapping("/{id}/invalidate")
    public ActionResult<String> invalidate(@PathVariable("id") String id) {
        questionService.invalidate(id);
        return ActionResult.success("废弃成功");
    }
    @Operation(summary = "废弃试题(逻辑删除)")
    @DeleteMapping("/{id}")
    public ActionResult<String> delete(@PathVariable("id") String id) {
        questionService.invalidate(id);
        return ActionResult.success("废弃成功");
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsRecordController.java
New file
@@ -0,0 +1,72 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.record.TmsRecordForm;
import jnpf.tmsEntity.record.TmsRecordQuery;
import jnpf.tmsService.TmsRecordService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
 * 培训记录。网关 /api/tms/record/** (StripPrefix=2)→ /record/**
 */
@Tag(name = "TMS培训记录", description = "Record")
@RestController
@RequestMapping("/record")
@RequiredArgsConstructor
public class TmsRecordController {
    private final TmsRecordService recordService;
    @Operation(summary = "培训记录分页列表")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsRecordForm>> list(TmsRecordQuery query) {
        List<TmsRecordForm> list = recordService.getList(query);
        PageListVO<TmsRecordForm> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "培训记录详情")
    @GetMapping("/{id}")
    public ActionResult<TmsRecordForm> info(@PathVariable("id") String id) {
        return ActionResult.success(recordService.getInfo(id));
    }
    @Operation(summary = "更新培训记录")
    @PutMapping("/{id}")
    public ActionResult<String> update(@PathVariable("id") String id, @RequestBody TmsRecordForm form) {
        recordService.update(id, form);
        return ActionResult.success("更新成功");
    }
    @Operation(summary = "归档培训记录")
    @PostMapping("/archive")
    public ActionResult<Integer> archive(@RequestBody Map<String, List<String>> body) {
        int count = recordService.archive(body == null ? null : body.get("ids"));
        return ActionResult.success(count);
    }
    @Operation(summary = "删除培训记录")
    @DeleteMapping
    public ActionResult<String> delete(@RequestBody Map<String, List<String>> body) {
        recordService.delete(body == null ? null : body.get("ids"));
        return ActionResult.success("删除成功");
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSelfTestController.java
New file
@@ -0,0 +1,73 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.selftest.TmsSelfTestPaperVO;
import jnpf.tmsEntity.selftest.TmsSelfTestQuery;
import jnpf.tmsEntity.selftest.TmsSelfTestRecordVO;
import jnpf.tmsEntity.selftest.TmsSelfTestStartForm;
import jnpf.tmsEntity.selftest.TmsSelfTestSubmitForm;
import jnpf.tmsEntity.selftest.TmsSelfTestSubmitResultVO;
import jnpf.tmsService.TmsSelfTestService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * 自我检测。网关 /api/tms/self-test/** (StripPrefix=2)→ /self-test/**
 */
@Tag(name = "TMS自我检测", description = "SelfTest")
@RestController
@RequestMapping("/self-test")
@RequiredArgsConstructor
public class TmsSelfTestController {
    private final TmsSelfTestService selfTestService;
    @Operation(summary = "开始自我检测(抽题)")
    @PostMapping("/start")
    public ActionResult<TmsSelfTestPaperVO> start(@RequestBody TmsSelfTestStartForm form) {
        return ActionResult.success(selfTestService.start(form));
    }
    @Operation(summary = "暂存作答(不交卷)")
    @PostMapping("/{id}/save")
    public ActionResult<String> save(@PathVariable("id") String id,
                                     @RequestBody TmsSelfTestSubmitForm form) {
        selfTestService.saveAnswers(id, form);
        return ActionResult.success("暂存成功");
    }
    @Operation(summary = "交卷")
    @PostMapping("/{id}/submit")
    public ActionResult<TmsSelfTestSubmitResultVO> submit(@PathVariable("id") String id,
                                                          @RequestBody TmsSelfTestSubmitForm form) {
        return ActionResult.success(selfTestService.submit(id, form));
    }
    @Operation(summary = "我的检测记录")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsSelfTestRecordVO>> list(TmsSelfTestQuery query) {
        List<TmsSelfTestRecordVO> list = selfTestService.getList(query);
        PageListVO<TmsSelfTestRecordVO> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "检测详情(重做回顾)")
    @GetMapping("/{id}")
    public ActionResult<TmsSelfTestPaperVO> info(@PathVariable("id") String id) {
        return ActionResult.success(selfTestService.getInfo(id));
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSignModeController.java
New file
@@ -0,0 +1,78 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.signmode.TmsSignModeForm;
import jnpf.tmsEntity.signmode.TmsSignModeQuery;
import jnpf.tmsService.TmsSignModeService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
 * 签到方式。网关 /api/tms/sign-mode/** (StripPrefix=2)→ /sign-mode/**
 */
@Tag(name = "TMS签到方式", description = "SignMode")
@RestController
@RequestMapping("/sign-mode")
@RequiredArgsConstructor
public class TmsSignModeController {
    private final TmsSignModeService signModeService;
    @Operation(summary = "签到方式分页列表")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsSignModeForm>> list(TmsSignModeQuery query) {
        List<TmsSignModeForm> list = signModeService.getList(query);
        PageListVO<TmsSignModeForm> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "签到方式详情")
    @GetMapping("/{id}")
    public ActionResult<TmsSignModeForm> info(@PathVariable("id") String id) {
        return ActionResult.success(signModeService.getInfo(id));
    }
    @Operation(summary = "新增签到方式")
    @PostMapping
    public ActionResult<String> create(@RequestBody TmsSignModeForm form) {
        return ActionResult.success(signModeService.create(form));
    }
    @Operation(summary = "更新签到方式")
    @PutMapping("/{id}")
    public ActionResult<String> update(@PathVariable("id") String id, @RequestBody TmsSignModeForm form) {
        signModeService.update(id, form);
        return ActionResult.success("更新成功");
    }
    @Operation(summary = "启用/停用")
    @PutMapping("/{id}/enabled")
    public ActionResult<String> enabled(@PathVariable("id") String id, @RequestBody Map<String, String> body) {
        signModeService.setEnabled(id, body == null ? null : body.get("enabled"));
        return ActionResult.success("操作成功");
    }
    @Operation(summary = "删除签到方式")
    @DeleteMapping("/{id}")
    public ActionResult<String> delete(@PathVariable("id") String id) {
        signModeService.delete(id);
        return ActionResult.success("删除成功");
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTaskController.java
New file
@@ -0,0 +1,180 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.task.TmsFromPlanResult;
import jnpf.tmsEntity.task.TmsTaskForm;
import jnpf.tmsEntity.task.TmsTaskQuery;
import jnpf.tmsService.TmsTaskService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import jnpf.util.StringUtil;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
 * 培训任务管理。网关 /api/tms/task/** (StripPrefix=2)→ /task/**
 */
@Tag(name = "TMS培训任务", description = "Task")
@RestController
@RequestMapping("/task")
@RequiredArgsConstructor
public class TmsTaskController {
    private final TmsTaskService taskService;
    @Operation(summary = "培训任务分页列表")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsTaskForm>> list(TmsTaskQuery query) {
        List<TmsTaskForm> list = taskService.getList(query);
        PageListVO<TmsTaskForm> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "培训任务详情")
    @GetMapping("/{id}")
    public ActionResult<TmsTaskForm> info(@PathVariable("id") String id) {
        return ActionResult.success(taskService.getInfo(id));
    }
    @Operation(summary = "新增培训任务")
    @PostMapping
    public ActionResult<String> create(@RequestBody TmsTaskForm form) {
        return ActionResult.success(taskService.create(form));
    }
    @Operation(summary = "更新培训任务")
    @PutMapping("/{id}")
    public ActionResult<String> update(@PathVariable("id") String id, @RequestBody TmsTaskForm form) {
        taskService.update(id, form);
        return ActionResult.success("更新成功");
    }
    @Operation(summary = "删除培训任务(仅草稿)")
    @DeleteMapping("/{id}")
    public ActionResult<String> delete(@PathVariable("id") String id) {
        taskService.delete(id);
        return ActionResult.success("删除成功");
    }
    @Operation(summary = "发布培训任务")
    @PutMapping("/{id}/publish")
    public ActionResult<String> publish(@PathVariable("id") String id) {
        taskService.publish(id);
        return ActionResult.success("发布成功");
    }
    @Operation(summary = "取消/终止培训任务")
    @PutMapping("/{id}/cancel")
    public ActionResult<String> cancel(@PathVariable("id") String id) {
        taskService.cancel(id);
        return ActionResult.success("取消成功");
    }
    @Operation(summary = "批量取消培训任务")
    @PutMapping("/cancelBatch")
    public ActionResult<String> cancelBatch(@RequestBody Map<String, List<String>> body) {
        taskService.cancelBatch(body == null ? null : body.get("ids"));
        return ActionResult.success("取消成功");
    }
    @Operation(summary = "恢复为草稿")
    @PutMapping("/{id}/restore")
    public ActionResult<String> restore(@PathVariable("id") String id) {
        taskService.restore(id);
        return ActionResult.success("已恢复为草稿");
    }
    @Operation(summary = "年计划审批通过:按子表生成未发布培训任务")
    @PostMapping("/from-annual/{planId}")
    public ActionResult<TmsFromPlanResult> fromAnnual(@PathVariable("planId") String planId) {
        return ActionResult.success(taskService.createFromAnnual(planId));
    }
    @Operation(summary = "年计划审批通过")
    @PostMapping("/from-annual")
    public ActionResult<TmsFromPlanResult> fromAnnualBody(
            @RequestBody(required = false) Map<String, Object> body,
            @RequestParam(value = "id", required = false) String id,
            @RequestParam(value = "planId", required = false) String planIdParam,
            @RequestParam(value = "f_id", required = false) String fId) {
        Map<String, Object> merged = new LinkedHashMap<>();
        if (body != null) {
            merged.putAll(body);
        }
        // 数据接口预览常把「接口参数」放 query,不进 JSON Body
        putIfAbsent(merged, "id", id);
        putIfAbsent(merged, "planId", planIdParam);
        putIfAbsent(merged, "f_id", fId);
        return ActionResult.success(taskService.createFromAnnual(pickPlanId(merged)));
    }
    @Operation(summary = "岗位培训计划审批通过:按子表生成未发布培训任务")
    @PostMapping("/from-post/{planId}")
    public ActionResult<TmsFromPlanResult> fromPost(@PathVariable("planId") String planId) {
        return ActionResult.success(taskService.createFromPost(planId));
    }
    @Operation(summary = "岗位培训计划审批通过")
    @PostMapping("/from-post")
    public ActionResult<TmsFromPlanResult> fromPostBody(
            @RequestBody(required = false) Map<String, Object> body,
            @RequestParam(value = "id", required = false) String id,
            @RequestParam(value = "planId", required = false) String planIdParam,
            @RequestParam(value = "f_id", required = false) String fId) {
        Map<String, Object> merged = new LinkedHashMap<>();
        if (body != null) {
            merged.putAll(body);
        }
        putIfAbsent(merged, "id", id);
        putIfAbsent(merged, "planId", planIdParam);
        putIfAbsent(merged, "f_id", fId);
        return ActionResult.success(taskService.createFromPost(pickPlanId(merged)));
    }
    private static void putIfAbsent(Map<String, Object> map, String key, String value) {
        if (StringUtil.isEmpty(value)) {
            return;
        }
        Object existed = map.get(key);
        if (existed == null || StringUtil.isEmpty(String.valueOf(existed))
                || "null".equalsIgnoreCase(String.valueOf(existed))
                || "value".equalsIgnoreCase(String.valueOf(existed).trim())) {
            map.put(key, value.trim());
        }
    }
    private static String pickPlanId(Map<String, Object> body) {
        if (body == null || body.isEmpty()) {
            return null;
        }
        for (String key : new String[]{"id", "planId", "f_id", "F_Id", "formId"}) {
            Object v = body.get(key);
            if (v == null) {
                continue;
            }
            String text = String.valueOf(v).trim();
            if (StringUtil.isEmpty(text) || "null".equalsIgnoreCase(text) || "value".equalsIgnoreCase(text)) {
                continue;
            }
            return text;
        }
        return null;
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainArchiveController.java
New file
@@ -0,0 +1,29 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.tmsEntity.archive.TmsTrainArchiveDetailVO;
import jnpf.tmsService.TmsTrainArchiveService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 个人培训档案。网关 /api/tms/train-archive/** (StripPrefix=2)→ /train-archive/**
 */
@Tag(name = "TMS个人培训档案", description = "TrainArchive")
@RestController
@RequestMapping("/train-archive")
@RequiredArgsConstructor
public class TmsTrainArchiveController {
    private final TmsTrainArchiveService trainArchiveService;
    @Operation(summary = "我的个人培训档案")
    @GetMapping("/mine")
    public ActionResult<TmsTrainArchiveDetailVO> mine() {
        return ActionResult.success(trainArchiveService.getMine());
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainModeController.java
New file
@@ -0,0 +1,78 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.trainmode.TmsTrainModeForm;
import jnpf.tmsEntity.trainmode.TmsTrainModeQuery;
import jnpf.tmsService.TmsTrainModeService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
 * 培训方式。网关 /api/tms/train-mode/** (StripPrefix=2)→ /train-mode/**
 */
@Tag(name = "TMS培训方式", description = "TrainMode")
@RestController
@RequestMapping("/train-mode")
@RequiredArgsConstructor
public class TmsTrainModeController {
    private final TmsTrainModeService trainModeService;
    @Operation(summary = "培训方式分页列表")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsTrainModeForm>> list(TmsTrainModeQuery query) {
        List<TmsTrainModeForm> list = trainModeService.getList(query);
        PageListVO<TmsTrainModeForm> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "培训方式详情")
    @GetMapping("/{id}")
    public ActionResult<TmsTrainModeForm> info(@PathVariable("id") String id) {
        return ActionResult.success(trainModeService.getInfo(id));
    }
    @Operation(summary = "新增培训方式")
    @PostMapping
    public ActionResult<String> create(@RequestBody TmsTrainModeForm form) {
        return ActionResult.success(trainModeService.create(form));
    }
    @Operation(summary = "更新培训方式")
    @PutMapping("/{id}")
    public ActionResult<String> update(@PathVariable("id") String id, @RequestBody TmsTrainModeForm form) {
        trainModeService.update(id, form);
        return ActionResult.success("更新成功");
    }
    @Operation(summary = "启用/停用")
    @PutMapping("/{id}/enabled")
    public ActionResult<String> enabled(@PathVariable("id") String id, @RequestBody Map<String, String> body) {
        trainModeService.setEnabled(id, body == null ? null : body.get("enabled"));
        return ActionResult.success("操作成功");
    }
    @Operation(summary = "删除培训方式")
    @DeleteMapping("/{id}")
    public ActionResult<String> delete(@PathVariable("id") String id) {
        trainModeService.delete(id);
        return ActionResult.success("删除成功");
    }
}
jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainRecordController.java
New file
@@ -0,0 +1,50 @@
package jnpf.tmsController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.ActionResult;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.tmsEntity.trainrecord.TmsTrainRecordCatalogVO;
import jnpf.tmsEntity.trainrecord.TmsTrainRecordListVO;
import jnpf.tmsEntity.trainrecord.TmsTrainRecordQuery;
import jnpf.tmsService.TmsTrainRecordService;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * 个人培训记录。网关 /api/tms/train-record/** (StripPrefix=2)→ /train-record/**
 */
@Tag(name = "TMS个人培训记录", description = "TrainRecord")
@RestController
@RequestMapping("/train-record")
@RequiredArgsConstructor
public class TmsTrainRecordController {
    private final TmsTrainRecordService trainRecordService;
    @Operation(summary = "个人培训记录分页列表(按人员档案)")
    @GetMapping("/list")
    public ActionResult<PageListVO<TmsTrainRecordListVO>> list(TmsTrainRecordQuery query) {
        List<TmsTrainRecordListVO> list = trainRecordService.getList(query);
        PageListVO<TmsTrainRecordListVO> vo = new PageListVO<>();
        vo.setList(list);
        vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
        return ActionResult.success(vo);
    }
    @Operation(summary = "个人培训目录")
    @GetMapping("/{id}/catalog")
    public ActionResult<TmsTrainRecordCatalogVO> catalog(@PathVariable("id") String id,
                                                         @RequestParam(value = "startDate", required = false) String startDate,
                                                         @RequestParam(value = "endDate", required = false) String endDate) {
        return ActionResult.success(trainRecordService.getCatalog(id, startDate, endDate));
    }
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanEntity.java
New file
@@ -0,0 +1,49 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 年度培训计划 tms_annual_plan
 */
@Data
@TableName("tms_annual_plan")
public class TmsAnnualPlanEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("plan_year")
    private String planYear;
    @TableField("plan_no")
    private String planNo;
    @TableField("plan_type")
    private String planType;
    @TableField("biz_status")
    private String bizStatus;
    @TableField("remark")
    private String remark;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanItemEntity.java
New file
@@ -0,0 +1,68 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 年度培训计划子表 tms_annual_plan_item
 */
@Data
@TableName("tms_annual_plan_item")
public class TmsAnnualPlanItemEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("f_foreign_id")
    private String foreignId;
    @TableField("train_month")
    private String trainMonth;
    @TableField("train_subject")
    private String trainSubject;
    @TableField("train_mode")
    private String trainMode;
    @TableField("trainees")
    private String trainees;
    @TableField("hours")
    private java.math.BigDecimal hours;
    @TableField("teacher_id")
    private String teacherId;
    @TableField("eval_mode")
    private String evalMode;
    @TableField("course_id")
    private String courseId;
    /** pending / tasked / done / delayed */
    @TableField("line_status")
    private String lineStatus;
    @TableField("task_id")
    private String taskId;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsCourseEntity.java
New file
@@ -0,0 +1,74 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 培训课程 tms_course
 */
@Data
@TableName("tms_course")
public class TmsCourseEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("course_no")
    private String courseNo;
    @TableField("course_name")
    private String courseName;
    @TableField("category")
    private String category;
    @TableField("train_mode")
    private String trainMode;
    @TableField("eval_mode")
    private String evalMode;
    @TableField("paper_id")
    private String paperId;
    @TableField("hours")
    private BigDecimal hours;
    @TableField("enabled")
    private String enabled;
    @TableField("remark")
    private String remark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsEvalModeEntity.java
New file
@@ -0,0 +1,67 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 考核方式配置 tms_eval_mode
 */
@Data
@TableName("tms_eval_mode")
public class TmsEvalModeEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("mode_code")
    private String modeCode;
    @TableField("mode_name")
    private String modeName;
    @TableField("need_paper")
    private String needPaper;
    @TableField("need_quiz")
    private String needQuiz;
    @TableField("need_practice")
    private String needPractice;
    @TableField("enabled")
    private String enabled;
    @TableField("remark")
    private String remark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamEntity.java
New file
@@ -0,0 +1,106 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 在线考试答卷 tms_exam
 */
@Data
@TableName("tms_exam")
public class TmsExamEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("task_id")
    private String taskId;
    @TableField("person_task_id")
    private String personTaskId;
    @TableField("record_id")
    private String recordId;
    @TableField("paper_id")
    private String paperId;
    @TableField("paper_name")
    private String paperName;
    @TableField("user_id")
    private String userId;
    @TableField("attempt_no")
    private Integer attemptNo;
    @TableField("start_time")
    private Date startTime;
    @TableField("submit_time")
    private Date submitTime;
    @TableField("duration_min")
    private Integer durationMin;
    @TableField("objective_score")
    private BigDecimal objectiveScore;
    @TableField("subjective_score")
    private BigDecimal subjectiveScore;
    @TableField("total_score")
    private BigDecimal totalScore;
    @TableField("pass_score")
    private BigDecimal passScore;
    @TableField("pass_flag")
    private String passFlag;
    /** auto=已自动阅卷 / pending=待阅卷 */
    @TableField("grade_status")
    private String gradeStatus;
    @TableField("grader_id")
    private String graderId;
    @TableField("grade_time")
    private Date gradeTime;
    /** doing / submitted */
    @TableField("exam_status")
    private String examStatus;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamItemEntity.java
New file
@@ -0,0 +1,78 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 答卷试题快照 tms_exam_item
 */
@Data
@TableName("tms_exam_item")
public class TmsExamItemEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("f_foreign_id")
    private String foreignId;
    @TableField("question_id")
    private String questionId;
    @TableField("question_type")
    private String questionType;
    @TableField("stem")
    private String stem;
    @TableField("options_json")
    private String optionsJson;
    @TableField("correct_answer")
    private String correctAnswer;
    @TableField("user_answer")
    private String userAnswer;
    @TableField("score")
    private BigDecimal score;
    @TableField("got_score")
    private BigDecimal gotScore;
    /** 1=主观题 */
    @TableField("is_subjective")
    private String isSubjective;
    @TableField("analysis")
    private String analysis;
    @TableField("sort_no")
    private Integer sortNo;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsMaterialEntity.java
New file
@@ -0,0 +1,34 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
 * 培训教材 tms_material(任务回填编号用)
 */
@Data
@TableName("tms_material")
public class TmsMaterialEntity {
    @TableId("f_id")
    private String id;
    @TableField("material_no")
    private String materialNo;
    @TableField("material_name")
    private String materialName;
    @TableField("file_type")
    private String fileType;
    @TableField("file_json")
    private String fileJson;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperEntity.java
New file
@@ -0,0 +1,86 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 试卷主表 tms_paper
 */
@Data
@TableName("tms_paper")
public class TmsPaperEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    /** 试卷编号:年份后两位 + 4 位流水,如 260001 */
    @TableField("paper_no")
    private String paperNo;
    @TableField("paper_name")
    private String paperName;
    @TableField("biz_status")
    private String bizStatus;
    @TableField("duration_min")
    private Integer durationMin;
    @TableField("exam_start")
    private Date examStart;
    @TableField("exam_end")
    private Date examEnd;
    @TableField("score_publish_time")
    private Date scorePublishTime;
    @TableField("pass_score")
    private BigDecimal passScore;
    @TableField("total_score")
    private BigDecimal totalScore;
    /** paper=试卷顺序 / random=随机 */
    @TableField("sort_mode")
    private String sortMode;
    /** 1=含主观题 / 0=否 */
    @TableField("has_subjective")
    private String hasSubjective;
    @TableField("remark")
    private String remark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperQuestionEntity.java
New file
@@ -0,0 +1,57 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 试卷试题 tms_paper_question
 */
@Data
@TableName("tms_paper_question")
public class TmsPaperQuestionEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    /** 试卷ID */
    @TableField("f_foreign_id")
    private String foreignId;
    @TableField("section_id")
    private String sectionId;
    @TableField("question_id")
    private String questionId;
    @TableField("score")
    private BigDecimal score;
    @TableField("sort_no")
    private Integer sortNo;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperSectionEntity.java
New file
@@ -0,0 +1,53 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 试卷章节 tms_paper_section
 */
@Data
@TableName("tms_paper_section")
public class TmsPaperSectionEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    /** 试卷ID */
    @TableField("f_foreign_id")
    private String foreignId;
    @TableField("section_name")
    private String sectionName;
    @TableField("question_type")
    private String questionType;
    @TableField("sort_no")
    private Integer sortNo;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonFileEntity.java
New file
@@ -0,0 +1,77 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 个人培训档案 tms_person_file
 */
@Data
@TableName("tms_person_file")
public class TmsPersonFileEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("archive_no")
    private String archiveNo;
    @TableField("archive_date")
    private Date archiveDate;
    @TableField("user_id")
    private String userId;
    @TableField("user_name")
    private String userName;
    @TableField("dept_id")
    private String deptId;
    @TableField("hire_date")
    private Date hireDate;
    @TableField("leave_date")
    private Date leaveDate;
    @TableField("main_post_id")
    private String mainPostId;
    @TableField("part_post_id")
    private String partPostId;
    /** valid / leave 等 */
    @TableField("biz_status")
    private String bizStatus;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTaskEntity.java
New file
@@ -0,0 +1,84 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 个人培训任务 tms_person_task
 */
@Data
@TableName("tms_person_task")
public class TmsPersonTaskEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("task_id")
    private String taskId;
    @TableField("task_no")
    private String taskNo;
    @TableField("user_id")
    private String userId;
    @TableField("dept_id")
    private String deptId;
    @TableField("hours")
    private BigDecimal hours;
    /** todo / signed / done / expired / cancelled */
    @TableField("biz_status")
    private String bizStatus;
    @TableField("sign_time")
    private Date signTime;
    @TableField("sign_mode")
    private String signMode;
    @TableField("learn_seconds")
    private Integer learnSeconds;
    @TableField("exam_score")
    private BigDecimal examScore;
    @TableField("pass_flag")
    private String passFlag;
    @TableField("guest_flag")
    private String guestFlag;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTrainEntity.java
New file
@@ -0,0 +1,87 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 个人培训目录 tms_person_train
 */
@Data
@TableName("tms_person_train")
public class TmsPersonTrainEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("archive_id")
    private String archiveId;
    @TableField("user_id")
    private String userId;
    @TableField("record_id")
    private String recordId;
    @TableField("record_no")
    private String recordNo;
    @TableField("category")
    private String category;
    @TableField("subject")
    private String subject;
    @TableField("train_mode")
    private String trainMode;
    /** 培训师(可为姓名或用户 ID,多选文本) */
    @TableField("teacher_id")
    private String teacherId;
    @TableField("eval_mode")
    private String evalMode;
    @TableField("pass_flag")
    private String passFlag;
    @TableField("start_time")
    private Date startTime;
    @TableField("end_time")
    private Date endTime;
    @TableField("hours")
    private BigDecimal hours;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonWorkEntity.java
New file
@@ -0,0 +1,55 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 个人培训档案-工作履历 tms_person_work
 */
@Data
@TableName("tms_person_work")
public class TmsPersonWorkEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("f_foreign_id")
    private String foreignId;
    @TableField("company_name")
    private String companyName;
    @TableField("post_name")
    private String postName;
    @TableField("hire_date")
    private Date hireDate;
    @TableField("leave_date")
    private Date leaveDate;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPlaceEntity.java
New file
@@ -0,0 +1,61 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 培训地点 tms_place
 */
@Data
@TableName("tms_place")
public class TmsPlaceEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("place_no")
    private String placeNo;
    @TableField("place_name")
    private String placeName;
    @TableField("place_group")
    private String placeGroup;
    @TableField("enabled")
    private String enabled;
    @TableField("remark")
    private String remark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanEntity.java
New file
@@ -0,0 +1,64 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 岗位培训计划 tms_post_plan
 */
@Data
@TableName("tms_post_plan")
public class TmsPostPlanEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("plan_no")
    private String planNo;
    @TableField("post_id")
    private String postId;
    @TableField("post_name")
    private String postName;
    @TableField("dept_id")
    private String deptId;
    @TableField("plan_end_time")
    private Date planEndTime;
    @TableField("post_desc")
    private String postDesc;
    @TableField("trainees")
    private String trainees;
    @TableField("catalog_id")
    private String catalogId;
    @TableField("publish_status")
    private String publishStatus;
    @TableField("remark")
    private String remark;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanItemEntity.java
New file
@@ -0,0 +1,66 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 岗位培训计划子表 tms_post_plan_item
 */
@Data
@TableName("tms_post_plan_item")
public class TmsPostPlanItemEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("f_foreign_id")
    private String foreignId;
    @TableField("plan_time")
    private Date planTime;
    @TableField("teacher_id")
    private String teacherId;
    @TableField("subject")
    private String subject;
    @TableField("train_mode")
    private String trainMode;
    @TableField("hours")
    private BigDecimal hours;
    @TableField("eval_mode")
    private String evalMode;
    @TableField("material_id")
    private String materialId;
    @TableField("task_id")
    private String taskId;
    /** pending / tasked / done */
    @TableField("line_status")
    private String lineStatus;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionBankEntity.java
New file
@@ -0,0 +1,58 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 题库 tms_question_bank
 */
@Data
@TableName("tms_question_bank")
public class TmsQuestionBankEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("bank_name")
    private String bankName;
    @TableField("question_count")
    private Integer questionCount;
    @TableField("biz_status")
    private String bizStatus;
    @TableField("remark")
    private String remark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionEntity.java
New file
@@ -0,0 +1,73 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 试题主表 tms_question
 */
@Data
@TableName("tms_question")
public class TmsQuestionEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("question_no")
    private String questionNo;
    @TableField("bank_id")
    private String bankId;
    @TableField("question_type")
    private String questionType;
    @TableField("difficulty")
    private String difficulty;
    @TableField("source_type")
    private String sourceType;
    @TableField("stem")
    private String stem;
    @TableField("analysis")
    private String analysis;
    @TableField("admin_user_id")
    private String adminUserId;
    @TableField("biz_status")
    private String bizStatus;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionOptionEntity.java
New file
@@ -0,0 +1,57 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 试题选项子表 tms_question_option
 */
@Data
@TableName("tms_question_option")
public class TmsQuestionOptionEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    /** 试题主键 */
    @TableField("f_foreign_id")
    private String foreignId;
    @TableField("sort_no")
    private Integer sortNo;
    @TableField("option_label")
    private String optionLabel;
    @TableField("option_content")
    private String optionContent;
    /** 1 正确 / 0 错误 */
    @TableField("is_correct")
    private String isCorrect;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsRecordEntity.java
New file
@@ -0,0 +1,94 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 培训记录 tms_record(发布任务时生成骨架)
 */
@Data
@TableName("tms_record")
public class TmsRecordEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("record_no")
    private String recordNo;
    @TableField("task_id")
    private String taskId;
    @TableField("task_no")
    private String taskNo;
    @TableField("category")
    private String category;
    @TableField("subject")
    private String subject;
    @TableField("teacher_id")
    private String teacherId;
    @TableField("train_type")
    private String trainType;
    @TableField("train_mode")
    private String trainMode;
    @TableField("eval_mode")
    private String evalMode;
    @TableField("start_time")
    private Date startTime;
    @TableField("end_time")
    private Date endTime;
    @TableField("close_time")
    private Date closeTime;
    @TableField("archive_status")
    private String archiveStatus;
    @TableField("sign_rate")
    private Integer signRate;
    @TableField("pass_rate")
    private Integer passRate;
    @TableField("remark")
    private String remark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestEntity.java
New file
@@ -0,0 +1,93 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 自我检测主表 tms_self_test
 */
@Data
@TableName("tms_self_test")
public class TmsSelfTestEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("bank_id")
    private String bankId;
    @TableField("bank_name")
    private String bankName;
    @TableField("user_id")
    private String userId;
    @TableField("single_count")
    private Integer singleCount;
    @TableField("multi_count")
    private Integer multiCount;
    @TableField("judge_count")
    private Integer judgeCount;
    @TableField("single_difficulty")
    private String singleDifficulty;
    @TableField("multi_difficulty")
    private String multiDifficulty;
    @TableField("judge_difficulty")
    private String judgeDifficulty;
    @TableField("total_count")
    private Integer totalCount;
    @TableField("correct_count")
    private Integer correctCount;
    @TableField("score_rate")
    private BigDecimal scoreRate;
    @TableField("start_time")
    private Date startTime;
    @TableField("submit_time")
    private Date submitTime;
    /** doing / submitted */
    @TableField("test_status")
    private String testStatus;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestItemEntity.java
New file
@@ -0,0 +1,74 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 自我检测试题快照 tms_self_test_item
 */
@Data
@TableName("tms_self_test_item")
public class TmsSelfTestItemEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("f_foreign_id")
    private String foreignId;
    @TableField("question_id")
    private String questionId;
    @TableField("question_no")
    private String questionNo;
    @TableField("question_type")
    private String questionType;
    @TableField("difficulty")
    private String difficulty;
    @TableField("stem")
    private String stem;
    @TableField("options_json")
    private String optionsJson;
    @TableField("correct_answer")
    private String correctAnswer;
    @TableField("user_answer")
    private String userAnswer;
    /** 1 / 0 */
    @TableField("is_right")
    private String isRight;
    @TableField("sort_no")
    private Integer sortNo;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSignModeEntity.java
New file
@@ -0,0 +1,64 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 签到方式配置 tms_sign_mode
 */
@Data
@TableName("tms_sign_mode")
public class TmsSignModeEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("mode_code")
    private String modeCode;
    @TableField("mode_name")
    private String modeName;
    @TableField("tip_seconds")
    private Integer tipSeconds;
    @TableField("match_roster")
    private String matchRoster;
    @TableField("enabled")
    private String enabled;
    @TableField("remark")
    private String remark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskEntity.java
New file
@@ -0,0 +1,175 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 培训任务 tms_task
 */
@Data
@TableName("tms_task")
public class TmsTaskEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("task_no")
    private String taskNo;
    @TableField("category")
    private String category;
    @TableField("train_type")
    private String trainType;
    @TableField("source_type")
    private String sourceType;
    @TableField("source_plan_no")
    private String sourcePlanNo;
    @TableField("source_id")
    private String sourceId;
    @TableField("source_item_id")
    private String sourceItemId;
    @TableField("subject")
    private String subject;
    @TableField("key_points")
    private String keyPoints;
    @TableField("material_id")
    private String materialId;
    @TableField("material_no")
    private String materialNo;
    @TableField("place_id")
    private String placeId;
    @TableField("place_no")
    private String placeNo;
    @TableField("place_name")
    private String placeName;
    @TableField("start_time")
    private Date startTime;
    @TableField("end_time")
    private Date endTime;
    @TableField("close_time")
    private Date closeTime;
    @TableField("paper_id")
    private String paperId;
    @TableField("paper_name")
    private String paperName;
    @TableField("exam_start")
    private Date examStart;
    @TableField("exam_end")
    private Date examEnd;
    @TableField("teacher_id")
    private String teacherId;
    @TableField("train_mode")
    private String trainMode;
    @TableField("eval_mode")
    private String evalMode;
    /** 培训对象用户ID,逗号分隔 */
    @TableField("trainees")
    private String trainees;
    @TableField("task_kind")
    private String taskKind;
    @TableField("prev_task_no")
    private String prevTaskNo;
    /** draft / published / cancelled / done */
    @TableField("publish_status")
    private String publishStatus;
    @TableField("publish_time")
    private Date publishTime;
    @TableField("retake_limit")
    private Integer retakeLimit;
    @TableField("pass_score")
    private BigDecimal passScore;
    @TableField("quiz_ratio")
    private Integer quizRatio;
    @TableField("sign_rate")
    private Integer signRate;
    @TableField("pass_rate")
    private Integer passRate;
    @TableField("unfinished_rate")
    private Integer unfinishedRate;
    @TableField("overdue_remind")
    private String overdueRemind;
    @TableField("invite_flag")
    private String inviteFlag;
    @TableField("dms_file_id")
    private String dmsFileId;
    @TableField("remark")
    private String remark;
    @TableField("f_flow_id")
    private String flowId;
    @TableField("f_flow_task_id")
    private String flowTaskId;
    @TableField("f_flow_state")
    private Integer flowState;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskFileEntity.java
New file
@@ -0,0 +1,52 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 培训任务教材/附件 tms_task_file
 */
@Data
@TableName("tms_task_file")
public class TmsTaskFileEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("f_foreign_id")
    private String foreignId;
    @TableField("file_no")
    private String fileNo;
    @TableField("file_name")
    private String fileName;
    @TableField("material_id")
    private String materialId;
    @TableField("file_json")
    private String fileJson;
    @TableField("can_download")
    private String canDownload;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainModeEntity.java
New file
@@ -0,0 +1,75 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
 * 培训方式配置 tms_train_mode
 */
@Data
@TableName("tms_train_mode")
public class TmsTrainModeEntity {
    @TableId("f_id")
    private String id;
    @TableField(value = "f_tenant_id", fill = FieldFill.INSERT)
    private String tenantId;
    @TableField("mode_code")
    private String modeCode;
    @TableField("mode_name")
    private String modeName;
    /** scan / both */
    @TableField("sign_rule")
    private String signRule;
    @TableField("allow_guest_sign")
    private String allowGuestSign;
    @TableField("same_day_required")
    private String sameDayRequired;
    /** 发布任务时是否必须选择培训地点(地点数据来自 tms_place) */
    @TableField("place_required")
    private String placeRequired;
    @TableField("sort_no")
    private Integer sortNo;
    @TableField("enabled")
    private String enabled;
    @TableField("remark")
    private String remark;
    @TableField(value = "f_creator_user_id", fill = FieldFill.INSERT)
    private String creatorUserId;
    @TableField(value = "f_creator_time", fill = FieldFill.INSERT)
    private Date creatorTime;
    @TableField(value = "f_last_modify_user_id", fill = FieldFill.UPDATE)
    private String lastModifyUserId;
    @TableField(value = "f_last_modify_time", fill = FieldFill.UPDATE)
    private Date lastModifyTime;
    @TableLogic
    @TableField("f_delete_mark")
    private Integer deleteMark;
    @TableField("f_delete_user_id")
    private String deleteUserId;
    @TableField("f_delete_time")
    private Date deleteTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainerEntity.java
New file
@@ -0,0 +1,24 @@
package jnpf.tmsEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
 * 培训师档案。年计划子表 teacher_id 存的是本表 f_id,系统用户在 trainer_id。
 */
@Data
@TableName("tms_trainer")
public class TmsTrainerEntity {
    @TableId("f_id")
    private String id;
    /** 对应系统用户 */
    @TableField("trainer_id")
    private String userId;
    @TableField("trainer_name")
    private String trainerName;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveCatalogVO.java
New file
@@ -0,0 +1,41 @@
package jnpf.tmsEntity.archive;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * 个人培训档案-培训目录
 */
@Data
public class TmsTrainArchiveCatalogVO {
    @Schema(description = "目录ID")
    private String id;
    @Schema(description = "培训记录编号")
    private String recordNo;
    @Schema(description = "培训类型")
    private String trainType;
    @Schema(description = "培训内容")
    private String trainContent;
    @Schema(description = "培训方式")
    private String trainMode;
    @Schema(description = "培训师")
    private String trainerName;
    @Schema(description = "考核方式")
    private String evalMode;
    @Schema(description = "是否合格 0/1")
    private String passFlag;
    @Schema(description = "培训开始时间")
    private String trainStart;
    @Schema(description = "培训结束时间")
    private String trainEnd;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveDetailVO.java
New file
@@ -0,0 +1,23 @@
package jnpf.tmsEntity.archive;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
 * 个人培训档案详情
 */
@Data
public class TmsTrainArchiveDetailVO {
    @Schema(description = "基本信息")
    private TmsTrainArchiveProfileVO profile;
    @Schema(description = "工作履历")
    private List<TmsTrainArchiveWorkVO> workList = new ArrayList<>();
    @Schema(description = "培训目录")
    private List<TmsTrainArchiveCatalogVO> catalogList = new ArrayList<>();
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveProfileVO.java
New file
@@ -0,0 +1,35 @@
package jnpf.tmsEntity.archive;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * 个人培训档案-基本信息
 */
@Data
public class TmsTrainArchiveProfileVO {
    @Schema(description = "档案ID")
    private String id;
    @Schema(description = "档案编号")
    private String archiveNo;
    @Schema(description = "建档时间 yyyy-MM-dd")
    private String archiveDate;
    @Schema(description = "员工ID")
    private String userId;
    @Schema(description = "员工姓名")
    private String userName;
    @Schema(description = "所属部门")
    private String deptName;
    @Schema(description = "主要岗位")
    private String mainPostName;
    @Schema(description = "兼职岗位")
    private String partPostName;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveWorkVO.java
New file
@@ -0,0 +1,26 @@
package jnpf.tmsEntity.archive;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * 个人培训档案-工作履历
 */
@Data
public class TmsTrainArchiveWorkVO {
    @Schema(description = "履历ID")
    private String id;
    @Schema(description = "入职时间")
    private String hireDate;
    @Schema(description = "离职时间")
    private String leaveDate;
    @Schema(description = "公司名称")
    private String companyName;
    @Schema(description = "任职岗位")
    private String postName;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseForm.java
New file
@@ -0,0 +1,43 @@
package jnpf.tmsEntity.course;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsCourseForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "课程编号")
    private String courseNo;
    @Schema(description = "课程名称")
    private String courseName;
    @Schema(description = "课程分类")
    private String category;
    @Schema(description = "培训方式编码")
    private String trainMode;
    @Schema(description = "考核方式编码")
    private String evalMode;
    @Schema(description = "关联试卷ID")
    private String paperId;
    @Schema(description = "关联试卷名称")
    private String paperName;
    @Schema(description = "学时")
    private BigDecimal hours;
    @Schema(description = "启用状态 0/1")
    private String enabled;
    @Schema(description = "备注")
    private String remark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseQuery.java
New file
@@ -0,0 +1,26 @@
package jnpf.tmsEntity.course;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsCourseQuery extends Pagination {
    @Schema(description = "关键词(编号/名称)")
    private String keyword;
    @Schema(description = "课程分类")
    private String category;
    @Schema(description = "培训方式")
    private String trainMode;
    @Schema(description = "考核方式")
    private String evalMode;
    @Schema(description = "启用状态 0/1")
    private String enabled;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsPaperOption.java
New file
@@ -0,0 +1,14 @@
package jnpf.tmsEntity.course;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsPaperOption {
    @Schema(description = "试卷ID")
    private String id;
    @Schema(description = "试卷名称")
    private String fullName;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeForm.java
New file
@@ -0,0 +1,32 @@
package jnpf.tmsEntity.evalmode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsEvalModeForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "方式编码")
    private String modeCode;
    @Schema(description = "方式名称")
    private String modeName;
    @Schema(description = "需要试卷 0/1")
    private String needPaper;
    @Schema(description = "需要提问预设 0/1")
    private String needQuiz;
    @Schema(description = "需要实操评分 0/1")
    private String needPractice;
    @Schema(description = "启用状态 0/1")
    private String enabled;
    @Schema(description = "备注")
    private String remark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeQuery.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.evalmode;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsEvalModeQuery extends Pagination {
    @Schema(description = "关键词(编码/名称)")
    private String keyword;
    @Schema(description = "启用状态 0/1")
    private String enabled;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamAttemptVO.java
New file
@@ -0,0 +1,30 @@
package jnpf.tmsEntity.exam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsOnlineExamAttemptVO {
    @Schema(description = "答卷ID")
    private String examId;
    @Schema(description = "考试序号,1=首考")
    private Integer attemptNo;
    @Schema(description = "考试类型文案")
    private String attemptLabel;
    @Schema(description = "状态 notStarted/doing/submitted")
    private String status;
    @Schema(description = "阅卷状态 auto/pending/graded")
    private String gradeStatus;
    private String startTime;
    private String submitTime;
    private BigDecimal gotScore;
    private String passFlag;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamDetailVO.java
New file
@@ -0,0 +1,50 @@
package jnpf.tmsEntity.exam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsOnlineExamDetailVO {
    private String id;
    private String paperId;
    private String paperName;
    private String status;
    private String examTimeText;
    private BigDecimal totalScore;
    private BigDecimal passScore;
    private Integer durationMin;
    private BigDecimal gotScore;
    private String startTime;
    private String submitTime;
    private String passFlag;
    private String gradeStatus;
    private String examId;
    @Schema(description = "当前/最近一次考试序号,1=首考")
    private Integer attemptNo;
    @Schema(description = "考试类型文案:首考/补考")
    private String attemptLabel;
    @Schema(description = "已交卷次数")
    private Integer submittedCount;
    @Schema(description = "允许补考次数上限(不含首考)")
    private Integer retakeLimit;
    @Schema(description = "剩余可补考次数")
    private Integer remainingRetakes;
    @Schema(description = "当前是否可发起补考")
    private Boolean canRetake;
    @Schema(description = "历史答卷(按 attemptNo 升序)")
    private List<TmsOnlineExamAttemptVO> attempts;
    /** 已交卷时返回答题回顾 */
    private List<TmsOnlineExamQuestionVO> questions;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamGradeItemVO.java
New file
@@ -0,0 +1,16 @@
package jnpf.tmsEntity.exam;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsOnlineExamGradeItemVO {
    private String id;
    private String correctAnswer;
    private BigDecimal gotScore;
    /** 客观题对错;主观题为空 */
    private Boolean right;
    private Boolean subjective;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamListVO.java
New file
@@ -0,0 +1,44 @@
package jnpf.tmsEntity.exam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsOnlineExamListVO {
    @Schema(description = "列表主键,当前为试卷ID")
    private String id;
    private String paperId;
    private String paperName;
    private String status;
    private String examStart;
    private String examEnd;
    private String examTimeText;
    private BigDecimal totalScore;
    private BigDecimal passScore;
    private Integer durationMin;
    private BigDecimal gotScore;
    private String passFlag;
    private String examId;
    @Schema(description = "当前/最近一次考试序号,1=首考")
    private Integer attemptNo;
    @Schema(description = "考试类型文案:首考/补考")
    private String attemptLabel;
    @Schema(description = "已交卷次数")
    private Integer submittedCount;
    @Schema(description = "允许补考次数上限(不含首考)")
    private Integer retakeLimit;
    @Schema(description = "剩余可补考次数")
    private Integer remainingRetakes;
    @Schema(description = "当前是否可发起补考")
    private Boolean canRetake;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamOptionVO.java
New file
@@ -0,0 +1,12 @@
package jnpf.tmsEntity.exam;
import lombok.Data;
@Data
public class TmsOnlineExamOptionVO {
    private String optionLabel;
    private String optionContent;
    /** 仅交卷回顾时回传 */
    private String isCorrect;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamPaperVO.java
New file
@@ -0,0 +1,33 @@
package jnpf.tmsEntity.exam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsOnlineExamPaperVO {
    private String examId;
    private String paperId;
    private String paperName;
    private BigDecimal totalScore;
    private BigDecimal passScore;
    private Integer durationMin;
    /** 开考时间 yyyy-MM-dd HH:mm:ss,倒计时按此续算 */
    private String startTime;
    /** 剩余秒数,null=不限时 */
    private Integer remainSeconds;
    @Schema(description = "开考时发现已超时并已自动交卷")
    private Boolean autoSubmitted;
    @Schema(description = "考试序号,1=首考")
    private Integer attemptNo;
    @Schema(description = "考试类型文案:首考/补考")
    private String attemptLabel;
    private List<TmsOnlineExamQuestionVO> questions;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuery.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.exam;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsOnlineExamQuery extends Pagination {
    @Schema(description = "试卷名称")
    private String keyword;
    @Schema(description = "notStarted/doing/submitted")
    private String status;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuestionVO.java
New file
@@ -0,0 +1,24 @@
package jnpf.tmsEntity.exam;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsOnlineExamQuestionVO {
    /** 答卷快照行ID,作答按此提交 */
    private String id;
    private String questionId;
    private String questionType;
    private String stem;
    private BigDecimal score;
    private BigDecimal gotScore;
    private String userAnswer;
    private String correctAnswer;
    /** 客观题对错;主观题为空 */
    private Boolean right;
    private Boolean subjective;
    private List<TmsOnlineExamOptionVO> options;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamStartForm.java
New file
@@ -0,0 +1,10 @@
package jnpf.tmsEntity.exam;
import lombok.Data;
@Data
public class TmsOnlineExamStartForm {
    /** 列表行ID,当前为试卷ID */
    private String myPaperId;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitForm.java
New file
@@ -0,0 +1,12 @@
package jnpf.tmsEntity.exam;
import lombok.Data;
import java.util.Map;
@Data
public class TmsOnlineExamSubmitForm {
    /** 快照行ID -> 选项标签 / 标签数组 / 填空文本 */
    private Map<String, Object> answers;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitResultVO.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.exam;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsOnlineExamSubmitResultVO {
    private BigDecimal objectiveScore;
    private BigDecimal totalScore;
    private String passFlag;
    private String gradeStatus;
    private Boolean pendingGrade;
    private List<TmsOnlineExamGradeItemVO> items;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeDetailVO.java
New file
@@ -0,0 +1,22 @@
package jnpf.tmsEntity.examgrade;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsExamGradeDetailVO {
    private String examId;
    private String sessionId;
    private String paperName;
    private String userName;
    private String deptName;
    private String submitTime;
    private BigDecimal totalScore;
    private BigDecimal passScore;
    private BigDecimal objectiveScore;
    private BigDecimal subjectiveScore;
    private String gradeStatus;
    private List<TmsExamGradeItemVO> items;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeExamVO.java
New file
@@ -0,0 +1,21 @@
package jnpf.tmsEntity.examgrade;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsExamGradeExamVO {
    private String id;
    private String sessionId;
    private String userId;
    private String userName;
    private String deptName;
    private String submitTime;
    private BigDecimal objectiveScore;
    private BigDecimal subjectiveScore;
    private BigDecimal totalScore;
    private BigDecimal passScore;
    private String gradeStatus;
    private String passFlag;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeItemVO.java
New file
@@ -0,0 +1,20 @@
package jnpf.tmsEntity.examgrade;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsExamGradeItemVO {
    private String id;
    private String questionType;
    private String stem;
    private List<TmsExamGradeOptionVO> options;
    private String correctAnswer;
    private String userAnswer;
    private BigDecimal score;
    private BigDecimal gotScore;
    private String isSubjective;
    private String analysis;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeOptionVO.java
New file
@@ -0,0 +1,9 @@
package jnpf.tmsEntity.examgrade;
import lombok.Data;
@Data
public class TmsExamGradeOptionVO {
    private String optionLabel;
    private String optionContent;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeQuery.java
New file
@@ -0,0 +1,12 @@
package jnpf.tmsEntity.examgrade;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsExamGradeQuery extends Pagination {
    private String keyword;
    private String taskNo;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSessionVO.java
New file
@@ -0,0 +1,22 @@
package jnpf.tmsEntity.examgrade;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsExamGradeSessionVO {
    /** 场次ID,当前用试卷ID */
    private String id;
    private String taskNo;
    private String taskSubject;
    private String paperId;
    private String paperName;
    private Integer durationMin;
    private String examStart;
    private String examEnd;
    private BigDecimal totalScore;
    private BigDecimal passScore;
    private Integer pendingCount;
    private Integer submittedCount;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitForm.java
New file
@@ -0,0 +1,18 @@
package jnpf.tmsEntity.examgrade;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsExamGradeSubmitForm {
    private String examId;
    private List<ItemScore> items;
    @Data
    public static class ItemScore {
        private String id;
        private BigDecimal gotScore;
    }
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitResultVO.java
New file
@@ -0,0 +1,16 @@
package jnpf.tmsEntity.examgrade;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsExamGradeSubmitResultVO {
    private String examId;
    private BigDecimal subjectiveScore;
    private BigDecimal totalScore;
    private String passFlag;
    private String gradeStatus;
    /** 同场次下一份待批改答卷,无则空 */
    private String nextExamId;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreDetailVO.java
New file
@@ -0,0 +1,12 @@
package jnpf.tmsEntity.examscore;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsExamScoreDetailVO extends TmsExamScoreSummaryVO {
    private List<TmsExamScoreRowVO> rows;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreExportForm.java
New file
@@ -0,0 +1,12 @@
package jnpf.tmsEntity.examscore;
import lombok.Data;
import java.util.List;
@Data
public class TmsExamScoreExportForm {
    /** 汇总行ID,当前为试卷ID */
    private List<String> ids;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreOptionVO.java
New file
@@ -0,0 +1,10 @@
package jnpf.tmsEntity.examscore;
import lombok.Data;
@Data
public class TmsExamScoreOptionVO {
    private String optionLabel;
    private String optionContent;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperItemVO.java
New file
@@ -0,0 +1,21 @@
package jnpf.tmsEntity.examscore;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsExamScorePaperItemVO {
    private String id;
    private String questionType;
    private String stem;
    private List<TmsExamScoreOptionVO> options;
    private String correctAnswer;
    private String userAnswer;
    private BigDecimal score;
    private BigDecimal gotScore;
    private Boolean isCorrect;
    private String isSubjective;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperVO.java
New file
@@ -0,0 +1,27 @@
package jnpf.tmsEntity.examscore;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsExamScorePaperVO {
    private String examId;
    private String summaryId;
    private String taskSubject;
    private String taskNo;
    private String paperName;
    private String userId;
    private String userName;
    private String deptName;
    private String examStart;
    private String examEnd;
    private BigDecimal score;
    private BigDecimal passScore;
    private BigDecimal totalScore;
    private String passFlag;
    private String sourceIp;
    private List<TmsExamScorePaperItemVO> items;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreQuery.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.examscore;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsExamScoreQuery extends Pagination {
    @Schema(description = "关键词:试卷名称/主题")
    private String keyword;
    @Schema(description = "培训编号(当前无任务时按试卷ID匹配)")
    private String taskNo;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreRowVO.java
New file
@@ -0,0 +1,23 @@
package jnpf.tmsEntity.examscore;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsExamScoreRowVO {
    private String id;
    private String summaryId;
    private String taskSubject;
    private String taskNo;
    private String userId;
    private String userName;
    private String deptName;
    private String examStart;
    private String examEnd;
    private BigDecimal score;
    private BigDecimal passScore;
    private String passFlag;
    private String sourceIp;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreSummaryVO.java
New file
@@ -0,0 +1,23 @@
package jnpf.tmsEntity.examscore;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsExamScoreSummaryVO {
    private String id;
    private String taskSubject;
    private String taskNo;
    private String paperId;
    private String paperName;
    private Boolean paperInvalid;
    private Integer participantCount;
    private Integer failCount;
    private BigDecimal maxScore;
    private BigDecimal minScore;
    private BigDecimal avgScore;
    private BigDecimal passScore;
    private BigDecimal totalScore;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperForm.java
New file
@@ -0,0 +1,59 @@
package jnpf.tmsEntity.paper;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsPaperForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "试卷编号")
    private String paperNo;
    @Schema(description = "试卷名称")
    private String paperName;
    @Schema(description = "状态 open/closed/invalid")
    private String bizStatus;
    @Schema(description = "考试时长(分钟)")
    private Integer durationMin;
    @Schema(description = "开考时间 yyyy-MM-dd HH:mm:ss")
    private String examStart;
    @Schema(description = "结考时间 yyyy-MM-dd HH:mm:ss")
    private String examEnd;
    @Schema(description = "成绩公布时间 yyyy-MM-dd HH:mm:ss")
    private String scorePublishTime;
    @Schema(description = "合格分数")
    private BigDecimal passScore;
    @Schema(description = "试卷总分")
    private BigDecimal totalScore;
    @Schema(description = "试题排序 paper/random")
    private String sortMode;
    @Schema(description = "含主观题 1/0")
    private String hasSubjective;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private String creatorTime;
    @Schema(description = "试题数量(列表用)")
    private Integer questionCount;
    @Schema(description = "章节及试题")
    private List<TmsPaperSectionForm> sections;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuery.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.paper;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsPaperQuery extends Pagination {
    @Schema(description = "状态 open/closed/invalid")
    private String bizStatus;
    @Schema(description = "关键词(试卷编号/名称)")
    private String keyword;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuestionForm.java
New file
@@ -0,0 +1,37 @@
package jnpf.tmsEntity.paper;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsPaperQuestionForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "章节ID(前端临时 key 或已保存 ID)")
    private String sectionId;
    @Schema(description = "试题ID")
    private String questionId;
    @Schema(description = "试题编号")
    private String questionNo;
    @Schema(description = "题干")
    private String stem;
    @Schema(description = "题型")
    private String questionType;
    @Schema(description = "题库名称")
    private String bankName;
    @Schema(description = "本题分值")
    private BigDecimal score;
    @Schema(description = "排序")
    private Integer sortNo;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperSectionForm.java
New file
@@ -0,0 +1,25 @@
package jnpf.tmsEntity.paper;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class TmsPaperSectionForm {
    @Schema(description = "主键(前端临时 key 或已保存 ID)")
    private String id;
    @Schema(description = "章节名称")
    private String sectionName;
    @Schema(description = "默认题型")
    private String questionType;
    @Schema(description = "排序")
    private Integer sortNo;
    @Schema(description = "章节内试题")
    private List<TmsPaperQuestionForm> questions;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskFileForm.java
New file
@@ -0,0 +1,23 @@
package jnpf.tmsEntity.person;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsPersonTaskFileForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "文件名")
    private String name;
    @Schema(description = "预览地址")
    private String url;
    @Schema(description = "文件扩展名")
    private String fileExt;
    @Schema(description = "预览类型 image/video/audio/pdf/office/other")
    private String previewType;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnForm.java
New file
@@ -0,0 +1,11 @@
package jnpf.tmsEntity.person;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsPersonTaskLearnForm {
    @Schema(description = "本次累计增加的学习秒数")
    private Integer seconds;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnResult.java
New file
@@ -0,0 +1,32 @@
package jnpf.tmsEntity.person;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsPersonTaskLearnResult {
    @Schema(description = "已学秒数")
    private Integer learnedSeconds;
    @Schema(description = "要求秒数")
    private Integer requiredSeconds;
    @Schema(description = "学习进度")
    private String learnStatus;
    @Schema(description = "个人状态")
    private String bizStatus;
    @Schema(description = "是否可考")
    private Boolean canExam;
    @Schema(description = "考试提示")
    private String examTip;
    @Schema(description = "本次是否刚自动完成")
    private Boolean autoCompleted;
    @Schema(description = "系统提示")
    private String message;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskMineForm.java
New file
@@ -0,0 +1,98 @@
package jnpf.tmsEntity.person;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsPersonTaskMineForm {
    @Schema(description = "个人任务ID")
    private String id;
    @Schema(description = "任务ID")
    private String taskId;
    @Schema(description = "培训编号")
    private String taskNo;
    @Schema(description = "培训主题")
    private String subject;
    @Schema(description = "培训分类")
    private String category;
    @Schema(description = "培训方式")
    private String trainMode;
    @Schema(description = "考核方式")
    private String evalMode;
    @Schema(description = "开始时间")
    private String startTime;
    @Schema(description = "结束时间")
    private String endTime;
    @Schema(description = "关闭时间")
    private String closeTime;
    @Schema(description = "地点")
    private String placeName;
    @Schema(description = "培训要点")
    private String keyPoints;
    @Schema(description = "个人状态")
    private String bizStatus;
    @Schema(description = "签到时间")
    private String signTime;
    @Schema(description = "是否已签到")
    private Boolean signed;
    @Schema(description = "当前是否可在线签到")
    private Boolean canSign;
    @Schema(description = "不可签到原因")
    private String signTip;
    @Schema(description = "学习秒数")
    private Integer learnSeconds;
    @Schema(description = "学习进度:未学习/学习中/已学满")
    private String learnStatus;
    @Schema(description = "要求课时")
    private java.math.BigDecimal requiredHours;
    @Schema(description = "要求学习秒数")
    private Integer requiredSeconds;
    @Schema(description = "已学秒数")
    private Integer learnedSeconds;
    @Schema(description = "成绩")
    private BigDecimal examScore;
    @Schema(description = "是否合格")
    private String passFlag;
    @Schema(description = "试卷ID")
    private String paperId;
    @Schema(description = "试卷名称")
    private String paperName;
    @Schema(description = "是否可考")
    private Boolean canExam;
    @Schema(description = "不可考原因")
    private String examTip;
    @Schema(description = "教材/附件")
    private List<TmsPersonTaskFileForm> files;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskQuery.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.person;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsPersonTaskQuery extends Pagination {
    @Schema(description = "关键词(编号/主题)")
    private String keyword;
    @Schema(description = "个人任务状态")
    private String bizStatus;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskSignResult.java
New file
@@ -0,0 +1,32 @@
package jnpf.tmsEntity.person;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsPersonTaskSignResult {
    @Schema(description = "签到时间")
    private String signTime;
    @Schema(description = "个人状态")
    private String bizStatus;
    @Schema(description = "是否已签到")
    private Boolean signed;
    @Schema(description = "当前是否可在线签到")
    private Boolean canSign;
    @Schema(description = "不可签到原因")
    private String signTip;
    @Schema(description = "是否可考")
    private Boolean canExam;
    @Schema(description = "考试提示")
    private String examTip;
    @Schema(description = "系统提示")
    private String message;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/place/TmsPlaceOption.java
New file
@@ -0,0 +1,23 @@
package jnpf.tmsEntity.place;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * 培训地点下拉选项
 */
@Data
public class TmsPlaceOption {
    @Schema(description = "地点ID")
    private String id;
    @Schema(description = "地点编号")
    private String placeNo;
    @Schema(description = "地点名称(下拉展示)")
    private String fullName;
    @Schema(description = "地点组别")
    private String placeGroup;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionBankOption.java
New file
@@ -0,0 +1,14 @@
package jnpf.tmsEntity.question;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsQuestionBankOption {
    @Schema(description = "题库ID")
    private String id;
    @Schema(description = "题库名称")
    private String fullName;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionForm.java
New file
@@ -0,0 +1,58 @@
package jnpf.tmsEntity.question;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
 * 试题详情/保存体(与前端 QuestionEntity 对齐)
 */
@Data
public class TmsQuestionForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "试题编号")
    private String questionNo;
    @Schema(description = "题库ID")
    private String bankId;
    @Schema(description = "题库名称")
    private String bankName;
    @Schema(description = "题型 single/multi/judge/blank/essay")
    private String questionType;
    @Schema(description = "难度")
    private String difficulty;
    @Schema(description = "来源")
    private String sourceType;
    @Schema(description = "题干")
    private String stem;
    @Schema(description = "解析")
    private String analysis;
    @Schema(description = "管理员用户ID")
    private String adminUserId;
    @Schema(description = "管理员姓名")
    private String adminUserName;
    @Schema(description = "状态")
    private String bizStatus;
    @Schema(description = "创建时间")
    private String creatorTime;
    @Schema(description = "填空混杂批改(前端字段,当前库表未落库)")
    private Boolean blankMixMode;
    @Schema(description = "选项")
    private List<TmsQuestionOptionForm> options;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionOptionForm.java
New file
@@ -0,0 +1,23 @@
package jnpf.tmsEntity.question;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsQuestionOptionForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "序号")
    private Integer sortNo;
    @Schema(description = "选项标识 A/B/T/F/1")
    private String optionLabel;
    @Schema(description = "选项内容")
    private String optionContent;
    @Schema(description = "是否正确 1/0")
    private String isCorrect;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionQuery.java
New file
@@ -0,0 +1,26 @@
package jnpf.tmsEntity.question;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsQuestionQuery extends Pagination {
    @Schema(description = "题库ID")
    private String bankId;
    @Schema(description = "题型")
    private String questionType;
    @Schema(description = "状态 open/closed/invalid")
    private String bizStatus;
    @Schema(description = "管理员用户ID")
    private String adminUserId;
    @Schema(description = "关键词(编号/题干)")
    private String keyword;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordAttachmentForm.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.record;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsRecordAttachmentForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "文件名")
    private String name;
    @Schema(description = "预览/下载地址")
    private String url;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordForm.java
New file
@@ -0,0 +1,82 @@
package jnpf.tmsEntity.record;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class TmsRecordForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "记录编号")
    private String recordNo;
    @Schema(description = "任务ID")
    private String taskId;
    @Schema(description = "任务编号")
    private String taskNo;
    @Schema(description = "培训分类")
    private String category;
    @Schema(description = "培训主题")
    private String subject;
    @Schema(description = "培训师用户ID")
    private String teacherId;
    @Schema(description = "培训师姓名")
    private String trainerName;
    @Schema(description = "培训类型")
    private String trainType;
    @Schema(description = "培训方式")
    private String trainMode;
    @Schema(description = "考核方式")
    private String evalMode;
    @Schema(description = "开始时间")
    private String startTime;
    @Schema(description = "结束时间")
    private String endTime;
    @Schema(description = "关闭时间")
    private String closeTime;
    @Schema(description = "归档状态")
    private String archiveStatus;
    @Schema(description = "签到率")
    private Integer signRate;
    @Schema(description = "合格率")
    private Integer passRate;
    @Schema(description = "培训要点")
    private String keyPoints;
    @Schema(description = "培训地点")
    private String placeName;
    @Schema(description = "培训对象姓名串")
    private String trainees;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "附件")
    private List<TmsRecordAttachmentForm> attachments;
    @Schema(description = "参加人员")
    private List<TmsRecordParticipantForm> participants;
    @Schema(description = "签到名单")
    private List<TmsRecordSignForm> signList;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordParticipantForm.java
New file
@@ -0,0 +1,31 @@
package jnpf.tmsEntity.record;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsRecordParticipantForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "用户ID")
    private String userId;
    @Schema(description = "姓名")
    private String userName;
    @Schema(description = "部门")
    private String deptName;
    @Schema(description = "签到日期")
    private String signDate;
    @Schema(description = "考试成绩")
    private BigDecimal examScore;
    @Schema(description = "是否合格 1/0")
    private String passFlag;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordQuery.java
New file
@@ -0,0 +1,29 @@
package jnpf.tmsEntity.record;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsRecordQuery extends Pagination {
    @Schema(description = "关键词(编号/主题)")
    private String keyword;
    @Schema(description = "编号")
    private String recordNo;
    @Schema(description = "培训分类")
    private String category;
    @Schema(description = "培训师姓名(模糊,后置过滤)")
    private String trainerName;
    @Schema(description = "列表模式 main/ready/archived")
    private String listMode;
    @Schema(description = "归档状态")
    private String archiveStatus;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordSignForm.java
New file
@@ -0,0 +1,26 @@
package jnpf.tmsEntity.record;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsRecordSignForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "姓名")
    private String userName;
    @Schema(description = "部门")
    private String deptName;
    @Schema(description = "签到时间")
    private String signTime;
    @Schema(description = "签到方式")
    private String signMode;
    @Schema(description = "旁听标志")
    private String guestFlag;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestOptionVO.java
New file
@@ -0,0 +1,20 @@
package jnpf.tmsEntity.selftest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * 试卷选项(与前端对齐)
 */
@Data
public class TmsSelfTestOptionVO {
    @Schema(description = "选项标识")
    private String optionLabel;
    @Schema(description = "选项内容")
    private String optionContent;
    @Schema(description = "是否正确 1/0")
    private String isCorrect;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestPaperVO.java
New file
@@ -0,0 +1,44 @@
package jnpf.tmsEntity.selftest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
 * 抽题结果 / 检测详情(回顾)
 */
@Data
public class TmsSelfTestPaperVO {
    @Schema(description = "答卷ID(即 paperId)")
    private String paperId;
    @Schema(description = "题库ID")
    private String bankId;
    @Schema(description = "题库名称")
    private String bankName;
    @Schema(description = "状态 doing/submitted")
    private String testStatus;
    @Schema(description = "总题数")
    private Integer totalCount;
    @Schema(description = "正确题数")
    private Integer correctCount;
    @Schema(description = "正确率")
    private BigDecimal scoreRate;
    @Schema(description = "开始时间")
    private String startTime;
    @Schema(description = "交卷时间")
    private String submitTime;
    @Schema(description = "题目列表")
    private List<TmsSelfTestQuestionVO> questions;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuery.java
New file
@@ -0,0 +1,29 @@
package jnpf.tmsEntity.selftest;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * 自我检测记录分页查询
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsSelfTestQuery extends Pagination {
    @Schema(description = "题库ID")
    private String bankId;
    @Schema(description = "状态 doing/submitted")
    private String testStatus;
    @Schema(description = "开始时间起 yyyy-MM-dd HH:mm:ss")
    private String startTimeBegin;
    @Schema(description = "开始时间止 yyyy-MM-dd HH:mm:ss")
    private String startTimeEnd;
    @Schema(description = "关键词(题库名称)")
    private String keyword;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuestionVO.java
New file
@@ -0,0 +1,43 @@
package jnpf.tmsEntity.selftest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
 * 试卷题目(含作答回顾字段)
 */
@Data
public class TmsSelfTestQuestionVO {
    @Schema(description = "试题ID")
    private String id;
    @Schema(description = "试题编号")
    private String questionNo;
    @Schema(description = "题型")
    private String questionType;
    @Schema(description = "难度")
    private String difficulty;
    @Schema(description = "题干")
    private String stem;
    @Schema(description = "选项")
    private List<TmsSelfTestOptionVO> options;
    @Schema(description = "正确答案(逗号分隔标签)")
    private String correctAnswer;
    @Schema(description = "用户作答(逗号分隔标签)")
    private String userAnswer;
    @Schema(description = "是否正确 1/0")
    private String isRight;
    @Schema(description = "排序号")
    private Integer sortNo;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestRecordVO.java
New file
@@ -0,0 +1,40 @@
package jnpf.tmsEntity.selftest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * 自我检测记录列表项
 */
@Data
public class TmsSelfTestRecordVO {
    @Schema(description = "答卷ID")
    private String id;
    @Schema(description = "题库ID")
    private String bankId;
    @Schema(description = "题库名称")
    private String bankName;
    @Schema(description = "总题数")
    private Integer totalCount;
    @Schema(description = "正确题数")
    private Integer correctCount;
    @Schema(description = "正确率")
    private BigDecimal scoreRate;
    @Schema(description = "状态")
    private String testStatus;
    @Schema(description = "开始时间")
    private String startTime;
    @Schema(description = "交卷时间")
    private String submitTime;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestStartForm.java
New file
@@ -0,0 +1,26 @@
package jnpf.tmsEntity.selftest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * 开始自我检测请求
 */
@Data
public class TmsSelfTestStartForm {
    @Schema(description = "题库ID")
    private String bankId;
    @Schema(description = "题库名称(可选)")
    private String bankName;
    @Schema(description = "单选题设置")
    private TmsSelfTestTypeSetting single;
    @Schema(description = "多选题设置")
    private TmsSelfTestTypeSetting multi;
    @Schema(description = "判断题设置")
    private TmsSelfTestTypeSetting judge;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitForm.java
New file
@@ -0,0 +1,16 @@
package jnpf.tmsEntity.selftest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Map;
/**
 * 交卷请求
 */
@Data
public class TmsSelfTestSubmitForm {
    @Schema(description = "作答:questionId -> 选项标签(单选/判断) 或 标签数组(多选)")
    private Map<String, Object> answers;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitResultVO.java
New file
@@ -0,0 +1,25 @@
package jnpf.tmsEntity.selftest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * 交卷结果
 */
@Data
public class TmsSelfTestSubmitResultVO {
    @Schema(description = "答卷ID")
    private String paperId;
    @Schema(description = "总题数")
    private Integer totalCount;
    @Schema(description = "正确题数")
    private Integer correctCount;
    @Schema(description = "正确率 0-100")
    private BigDecimal scoreRate;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestTypeSetting.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.selftest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * 题型抽题条件
 */
@Data
public class TmsSelfTestTypeSetting {
    @Schema(description = "抽题数量")
    private Integer count;
    @Schema(description = "难度 veryEasy/easier/normal/harder/veryHard")
    private String difficulty;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeForm.java
New file
@@ -0,0 +1,29 @@
package jnpf.tmsEntity.signmode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsSignModeForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "方式编码")
    private String modeCode;
    @Schema(description = "方式名称")
    private String modeName;
    @Schema(description = "要点阅读秒数")
    private Integer tipSeconds;
    @Schema(description = "须匹配名单 0/1")
    private String matchRoster;
    @Schema(description = "启用状态 0/1")
    private String enabled;
    @Schema(description = "备注")
    private String remark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeQuery.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.signmode;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsSignModeQuery extends Pagination {
    @Schema(description = "关键词(编码/名称)")
    private String keyword;
    @Schema(description = "启用状态 0/1")
    private String enabled;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsFromPlanResult.java
New file
@@ -0,0 +1,23 @@
package jnpf.tmsEntity.task;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class TmsFromPlanResult {
    @Schema(description = "来源计划ID")
    private String planId;
    @Schema(description = "新生成任务数")
    private int created;
    @Schema(description = "已存在而跳过的行数")
    private int skipped;
    @Schema(description = "本次新生成的任务ID")
    private List<String> taskIds = new ArrayList<>();
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsPersonTaskForm.java
New file
@@ -0,0 +1,34 @@
package jnpf.tmsEntity.task;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TmsPersonTaskForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "用户ID")
    private String userId;
    @Schema(description = "部门ID")
    private String deptId;
    @Schema(description = "业务状态")
    private String bizStatus;
    @Schema(description = "签到时间")
    private String signTime;
    @Schema(description = "考试成绩")
    private BigDecimal examScore;
    @Schema(description = "是否合格")
    private String passFlag;
    @Schema(description = "旁听标志")
    private String guestFlag;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskFileForm.java
New file
@@ -0,0 +1,26 @@
package jnpf.tmsEntity.task;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsTaskFileForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "编号")
    private String fileNo;
    @Schema(description = "文件名称")
    private String fileName;
    @Schema(description = "教材ID")
    private String materialId;
    @Schema(description = "附件")
    private String fileJson;
    @Schema(description = "允许下载 1/0")
    private String canDownload;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskForm.java
New file
@@ -0,0 +1,137 @@
package jnpf.tmsEntity.task;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class TmsTaskForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "培训编号")
    private String taskNo;
    @Schema(description = "培训分类")
    private String category;
    @Schema(description = "培训类型")
    private String trainType;
    @Schema(description = "来源类型")
    private String sourceType;
    @Schema(description = "来源计划号")
    private String sourcePlanNo;
    @Schema(description = "来源单据ID")
    private String sourceId;
    @Schema(description = "来源明细ID")
    private String sourceItemId;
    @Schema(description = "培训主题")
    private String subject;
    @Schema(description = "培训要点")
    private String keyPoints;
    @Schema(description = "教材ID")
    private String materialId;
    @Schema(description = "教材编号")
    private String materialNo;
    @Schema(description = "地点ID")
    private String placeId;
    @Schema(description = "地点编号")
    private String placeNo;
    @Schema(description = "地点名称")
    private String placeName;
    @Schema(description = "开始时间")
    private String startTime;
    @Schema(description = "结束时间")
    private String endTime;
    @Schema(description = "关闭时间")
    private String closeTime;
    @Schema(description = "试卷ID")
    private String paperId;
    @Schema(description = "试卷名称")
    private String paperName;
    @Schema(description = "考试开始")
    private String examStart;
    @Schema(description = "考试结束")
    private String examEnd;
    @Schema(description = "培训师用户ID")
    private String teacherId;
    @Schema(description = "培训方式")
    private String trainMode;
    @Schema(description = "考核方式")
    private String evalMode;
    @Schema(description = "培训对象用户ID列表(表单)")
    private List<String> traineeIds;
    @Schema(description = "培训对象(逗号串,列表回显)")
    private String trainees;
    @Schema(description = "任务类型 new/continue")
    private String taskKind;
    @Schema(description = "上次任务编号")
    private String prevTaskNo;
    @Schema(description = "发布状态")
    private String publishStatus;
    @Schema(description = "发布时间")
    private String publishTime;
    @Schema(description = "重考次数上限")
    private Integer retakeLimit;
    @Schema(description = "合格分")
    private BigDecimal passScore;
    @Schema(description = "提问占比")
    private Integer quizRatio;
    @Schema(description = "逾期提醒 1/0")
    private String overdueRemind;
    @Schema(description = "可邀请旁听 1/0")
    private String inviteFlag;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private String creatorTime;
    @Schema(description = "创建人")
    private String creatorUserId;
    @Schema(description = "个人任务人数(详情)")
    private Integer personCount;
    @Schema(description = "个人任务列表(详情)")
    private List<TmsPersonTaskForm> personTasks;
    @Schema(description = "教材/附件清单")
    private List<TmsTaskFileForm> files;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskQuery.java
New file
@@ -0,0 +1,29 @@
package jnpf.tmsEntity.task;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsTaskQuery extends Pagination {
    @Schema(description = "发布状态 draft/published/cancelled/done")
    private String publishStatus;
    @Schema(description = "培训方式")
    private String trainMode;
    @Schema(description = "来源类型")
    private String sourceType;
    @Schema(description = "关键词(编号/主题)")
    private String keyword;
    @Schema(description = "开始时间起 yyyy-MM-dd")
    private String startTimeFrom;
    @Schema(description = "开始时间止 yyyy-MM-dd")
    private String startTimeTo;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeForm.java
New file
@@ -0,0 +1,41 @@
package jnpf.tmsEntity.trainmode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * 培训方式表单 / 列表项
 */
@Data
public class TmsTrainModeForm {
    @Schema(description = "主键")
    private String id;
    @Schema(description = "方式编码")
    private String modeCode;
    @Schema(description = "方式名称")
    private String modeName;
    @Schema(description = "签到规则 scan/both")
    private String signRule;
    @Schema(description = "允许名单外签到 0/1")
    private String allowGuestSign;
    @Schema(description = "起止须同一天 0/1")
    private String sameDayRequired;
    @Schema(description = "地点必填 0/1(任务选地点来自培训地点管理)")
    private String placeRequired;
    @Schema(description = "排序")
    private Integer sortNo;
    @Schema(description = "启用状态 0/1")
    private String enabled;
    @Schema(description = "备注")
    private String remark;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeQuery.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.trainmode;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsTrainModeQuery extends Pagination {
    @Schema(description = "关键词(编码/名称)")
    private String keyword;
    @Schema(description = "启用状态 0/1")
    private String enabled;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogItemVO.java
New file
@@ -0,0 +1,38 @@
package jnpf.tmsEntity.trainrecord;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsTrainRecordCatalogItemVO {
    @Schema(description = "目录行ID")
    private String id;
    @Schema(description = "培训记录编号")
    private String recordNo;
    @Schema(description = "培训类型")
    private String trainType;
    @Schema(description = "培训内容")
    private String trainContent;
    @Schema(description = "培训方式")
    private String trainMode;
    @Schema(description = "培训师")
    private String trainerName;
    @Schema(description = "考核方式")
    private String evalMode;
    @Schema(description = "培训结果(成绩)")
    private Object trainResult;
    @Schema(description = "是否合格 0/1/空")
    private String passFlag;
    @Schema(description = "具体培训时间 yyyy-MM-dd")
    private String trainDate;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogVO.java
New file
@@ -0,0 +1,22 @@
package jnpf.tmsEntity.trainrecord;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class TmsTrainRecordCatalogVO {
    @Schema(description = "档案ID")
    private String id;
    @Schema(description = "姓名")
    private String userName;
    @Schema(description = "档案编号")
    private String archiveNo;
    @Schema(description = "培训目录")
    private List<TmsTrainRecordCatalogItemVO> list;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordListVO.java
New file
@@ -0,0 +1,44 @@
package jnpf.tmsEntity.trainrecord;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TmsTrainRecordListVO {
    @Schema(description = "档案ID")
    private String id;
    @Schema(description = "档案编号")
    private String archiveNo;
    @Schema(description = "进岗时间 yyyy-MM-dd")
    private String postDate;
    @Schema(description = "姓名")
    private String userName;
    @Schema(description = "部门")
    private String deptName;
    @Schema(description = "岗位")
    private String postName;
    @Schema(description = "兼职岗位")
    private String partPostName;
    @Schema(description = "专业")
    private String major;
    @Schema(description = "学历")
    private String education;
    @Schema(description = "技术职称")
    private String techTitle;
    @Schema(description = "入职时间")
    private String hireDate;
    @Schema(description = "离职日期")
    private String leaveDate;
}
jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordQuery.java
New file
@@ -0,0 +1,17 @@
package jnpf.tmsEntity.trainrecord;
import io.swagger.v3.oas.annotations.media.Schema;
import jnpf.base.Pagination;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TmsTrainRecordQuery extends Pagination {
    @Schema(description = "关键词(姓名/部门/档案编号)")
    private String keyword;
    @Schema(description = "档案编号")
    private String archiveNo;
}
jnpf-tms/jnpf-tms-server/src/main/resources/application.yml
@@ -18,6 +18,8 @@
      - file:${JNPF_CONFIG_DIR:config}/shared/mq.yaml
      - file:${JNPF_CONFIG_DIR:config}/shared/discovery.yaml
      - optional:file:${JNPF_CONFIG_DIR:config}/host-override/discovery.yaml
      # 放在公共 datasource.yaml 之后,覆盖库名到 jnpf_tms
      - optional:classpath:tms-datasource.yaml
# 应用服务器
server:
jnpf-tms/jnpf-tms-server/src/main/resources/tms-datasource.yaml
New file
@@ -0,0 +1,6 @@
# jnpf-tms 独立业务库(在 application.yml 中于公共 datasource.yaml 之后 import)
spring:
  datasource:
    db-name: jnpf_tms
    db-schema: public
    prepare-url: jdbc:postgresql://{host}:{port}/jnpf_tms?currentSchema=public