From 6eef0b6730d940ef44e0f66dd8e7d178f1cd2d1f Mon Sep 17 00:00:00 2001
From: liuyu <yu.liu@cqgbkj.com>
Date: 星期二, 22 九月 2026 09:25:18 +0800
Subject: [PATCH] feat(tms): 实现培训管理业务模块

---
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskQuery.java                       |   29 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuery.java                     |   17 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskFileMapper.java                          |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSessionVO.java         |   22 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordForm.java                    |   82 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitResultVO.java      |   25 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSelfTestServiceImpl.java               |  484 ++
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamGradeController.java          |   76 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTrainEntity.java                    |   87 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionForm.java                |   58 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeExamVO.java            |   21 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/place/TmsPlaceOption.java                    |   23 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPlaceMapper.java                             |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPlaceEntity.java                          |   61 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeItemVO.java            |   20 
 jnpf-tms/jnpf-tms-server/src/main/resources/tms-datasource.yaml                                    |    6 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperForm.java                      |   59 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperItemVO.java       |   21 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreExportForm.java        |   12 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperQuestionEntity.java                  |   57 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamScoreServiceImpl.java              |  376 +
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamDetailVO.java              |   50 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsQuestionServiceImpl.java               |  378 +
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskEntity.java                           |  175 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestMapper.java                          |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestEntity.java                       |   93 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeDetailVO.java          |   22 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsCourseService.java                          |   24 
 jnpf-tms/jnpf-tms-biz/pom.xml                                                                      |    5 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskSignResult.java          |   32 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordListVO.java        |   44 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestItemMapper.java                      |    9 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamScoreController.java          |   65 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuestionVO.java            |   24 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainModeMapper.java                         |    9 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainModeService.java                       |   24 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsCourseServiceImpl.java                 |  245 +
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsMaterialMapper.java                          |    9 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamGradeService.java                       |   23 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionQuery.java               |   26 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPersonTaskController.java         |   65 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamListVO.java                |   44 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsRecordServiceImpl.java                 |  388 +
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestItemEntity.java                   |   74 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsEvalModeEntity.java                       |   67 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogItemVO.java |   38 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsEvalModeMapper.java                          |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionBankEntity.java                   |   58 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsMaterialEntity.java                       |   34 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreOptionVO.java          |   10 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsRecordMapper.java                            |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperEntity.java                          |   86 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeQuery.java               |   17 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanItemMapper.java                      |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuestionVO.java          |   43 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamPaperVO.java               |   33 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveWorkVO.java           |   26 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeForm.java              |   41 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperSectionMapper.java                      |    9 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainArchiveController.java       |   29 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsCourseMapper.java                            |   14 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanItemEntity.java                 |   68 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTaskController.java               |  180 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitForm.java          |   16 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamGradeItemVO.java           |   16 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsEvalModeServiceImpl.java               |  166 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuery.java               |   29 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeForm.java                |   29 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonWorkEntity.java                     |   55 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainRecordService.java                     |   14 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamItemEntity.java                       |   78 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveDetailVO.java         |   23 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnForm.java           |   11 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskQuery.java               |   17 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreDetailVO.java          |   12 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsOnlineExamServiceImpl.java             | 1030 ++++
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsCourseController.java             |   85 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamScoreService.java                       |   21 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogVO.java     |   22 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSelfTestService.java                        |   24 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsEvalModeService.java                        |   21 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskMapper.java                              |   18 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPaperServiceImpl.java                  |  558 ++
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsCourseEntity.java                         |   74 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPlaceService.java                           |   14 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamEntity.java                           |  106 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskMineForm.java            |   98 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestPaperVO.java             |   44 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskForm.java                        |  137 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanItemMapper.java                    |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperVO.java           |   27 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanMapper.java                          |    9 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPaperService.java                           |   23 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionEntity.java                       |   73 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSelfTestController.java           |   73 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseQuery.java                   |   26 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuestionForm.java              |   37 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreRowVO.java             |   23 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanMapper.java                        |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitForm.java        |   18 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSignModeMapper.java                          |    9 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTrainMapper.java                       |    9 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPlaceController.java              |   31 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsRecordController.java             |   72 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionOptionForm.java          |   23 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPersonTaskServiceImpl.java             |  543 ++
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreQuery.java             |   17 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSignModeService.java                        |   21 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperSectionForm.java               |   25 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSignModeServiceImpl.java               |  164 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsRecordEntity.java                         |   94 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonFileMapper.java                        |    9 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionOptionMapper.java                    |    9 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsQuestionService.java                        |   25 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordQuery.java                   |   29 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTaskServiceImpl.java                   | 1069 ++++
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainerMapper.java                           |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeQuery.java             |   17 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitForm.java            |   12 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeQuery.java             |   12 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainModeController.java          |   78 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskFileForm.java                    |   26 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperQuestionMapper.java                     |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsFromPlanResult.java                  |   23 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordAttachmentForm.java          |   17 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseForm.java                    |   43 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestOptionVO.java            |   20 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeForm.java                |   32 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuery.java                 |   17 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsPersonTaskForm.java                  |   34 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainModeServiceImpl.java              |  174 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnResult.java         |   32 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTaskEntity.java                     |   84 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsRecordService.java                          |   19 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamAttemptVO.java             |   30 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPersonTaskService.java                      |   20 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestRecordVO.java            |   40 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanItemEntity.java                   |   66 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainerEntity.java                        |   24 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainRecordController.java        |   50 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsEvalModeController.java           |   78 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionBankMapper.java                      |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperSectionEntity.java                   |   53 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanEntity.java                     |   49 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsOnlineExamController.java         |   80 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperMapper.java                             |   18 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordParticipantForm.java         |   31 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestStartForm.java           |   26 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainArchiveService.java                    |   12 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainArchiveServiceImpl.java           |  268 +
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionBankOption.java          |   14 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPlaceServiceImpl.java                  |   37 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamItemMapper.java                          |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreSummaryVO.java         |   23 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsQuestionController.java           |   91 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionOptionEntity.java                 |   57 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamGradeServiceImpl.java              |  421 +
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskFileForm.java            |   23 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonFileEntity.java                     |   77 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainModeEntity.java                      |   75 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordQuery.java         |   17 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeQuery.java               |   17 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTaskMapper.java                        |    9 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeOptionVO.java          |    9 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsOnlineExamService.java                      |   26 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitResultVO.java        |   17 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonWorkMapper.java                        |    9 
 jnpf-tms/jnpf-tms-server/src/main/resources/application.yml                                        |    2 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamMapper.java                              |    9 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSignModeController.java           |   78 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTaskService.java                            |   44 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamOptionVO.java              |   12 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordSignForm.java                |   26 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsPaperOption.java                   |   14 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitResultVO.java    |   16 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainRecordServiceImpl.java            |  446 ++
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskFileEntity.java                       |   52 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveCatalogVO.java        |   41 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSignModeEntity.java                       |   64 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamStartForm.java             |   10 
 jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPaperController.java              |   77 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestTypeSetting.java         |   17 
 jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionMapper.java                          |   18 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanEntity.java                       |   64 
 jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveProfileVO.java        |   35 
 185 files changed, 12,833 insertions(+), 0 deletions(-)

diff --git a/jnpf-tms/jnpf-tms-biz/pom.xml b/jnpf-tms/jnpf-tms-biz/pom.xml
index 0b7bb2e..421ca71 100644
--- a/jnpf-tms/jnpf-tms-biz/pom.xml
+++ b/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>
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanItemMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanItemMapper.java
new file mode 100644
index 0000000..831dced
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanItemMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanMapper.java
new file mode 100644
index 0000000..5efd353
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsAnnualPlanMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsCourseMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsCourseMapper.java
new file mode 100644
index 0000000..82bb439
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsCourseMapper.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsEvalModeMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsEvalModeMapper.java
new file mode 100644
index 0000000..d98bb71
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsEvalModeMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamItemMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamItemMapper.java
new file mode 100644
index 0000000..9ebe623
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamItemMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamMapper.java
new file mode 100644
index 0000000..8cee0b3
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsExamMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsMaterialMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsMaterialMapper.java
new file mode 100644
index 0000000..c9a9341
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsMaterialMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperMapper.java
new file mode 100644
index 0000000..85dc9ec
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperMapper.java
@@ -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> {
+
+    /**
+     * 褰撳勾鏈�澶ц瘯鍗风紪鍙凤紙鍚凡閫昏緫鍒犻櫎锛岄伩鍏嶆祦姘村彿鍥為��澶嶇敤锛夈��
+     * 鏍煎紡锛歽y + 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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperQuestionMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperQuestionMapper.java
new file mode 100644
index 0000000..f5b3a38
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperQuestionMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperSectionMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperSectionMapper.java
new file mode 100644
index 0000000..f5158a4
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPaperSectionMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonFileMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonFileMapper.java
new file mode 100644
index 0000000..5b75af1
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonFileMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTaskMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTaskMapper.java
new file mode 100644
index 0000000..74ae6f6
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTaskMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTrainMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTrainMapper.java
new file mode 100644
index 0000000..1552d1a
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonTrainMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonWorkMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonWorkMapper.java
new file mode 100644
index 0000000..69e04e4
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPersonWorkMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPlaceMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPlaceMapper.java
new file mode 100644
index 0000000..b2a5422
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPlaceMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanItemMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanItemMapper.java
new file mode 100644
index 0000000..dc98b86
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanItemMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanMapper.java
new file mode 100644
index 0000000..7970cd5
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsPostPlanMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionBankMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionBankMapper.java
new file mode 100644
index 0000000..8c7efa7
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionBankMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionMapper.java
new file mode 100644
index 0000000..12de634
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionMapper.java
@@ -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> {
+
+    /**
+     * 褰撳勾鏈�澶ц瘯棰樼紪鍙凤紙鍚凡閫昏緫鍒犻櫎锛岄伩鍏嶆祦姘村彿鍥為��澶嶇敤锛夈��
+     * 鏍煎紡锛歽y + 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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionOptionMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionOptionMapper.java
new file mode 100644
index 0000000..d59f778
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsQuestionOptionMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsRecordMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsRecordMapper.java
new file mode 100644
index 0000000..72f7d34
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsRecordMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestItemMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestItemMapper.java
new file mode 100644
index 0000000..e23606f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestItemMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestMapper.java
new file mode 100644
index 0000000..44f8304
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSelfTestMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSignModeMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSignModeMapper.java
new file mode 100644
index 0000000..759917c
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsSignModeMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskFileMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskFileMapper.java
new file mode 100644
index 0000000..9ec1534
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskFileMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskMapper.java
new file mode 100644
index 0000000..4067b7a
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTaskMapper.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainModeMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainModeMapper.java
new file mode 100644
index 0000000..ee51432
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainModeMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainerMapper.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainerMapper.java
new file mode 100644
index 0000000..43ba1de
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsMapper/TmsTrainerMapper.java
@@ -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> {
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsCourseService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsCourseService.java
new file mode 100644
index 0000000..d65bf22
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsCourseService.java
@@ -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();
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsEvalModeService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsEvalModeService.java
new file mode 100644
index 0000000..84247ca
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsEvalModeService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamGradeService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamGradeService.java
new file mode 100644
index 0000000..661cec8
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamGradeService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamScoreService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamScoreService.java
new file mode 100644
index 0000000..da6618f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsExamScoreService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsOnlineExamService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsOnlineExamService.java
new file mode 100644
index 0000000..a5e5f20
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsOnlineExamService.java
@@ -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);
+
+    /** 鎸夌瓟鍗稩D鏌ョ湅宸蹭氦鍗峰洖椤� */
+    TmsOnlineExamDetailVO getExamReview(String examId);
+
+    TmsOnlineExamPaperVO start(String paperId);
+
+    void saveAnswers(String examId, TmsOnlineExamSubmitForm form);
+
+    TmsOnlineExamSubmitResultVO submit(String examId, TmsOnlineExamSubmitForm form);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPaperService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPaperService.java
new file mode 100644
index 0000000..cc5c6a1
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPaperService.java
@@ -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);
+
+    /** 搴熷純锛歜iz_status=invalid */
+    void invalidate(String id);
+
+    /** 鍚敤/鍋滅敤锛歰pen 鈫� closed锛堝簾寮冨嵎涓嶅彲锛� */
+    void setBizStatus(String id, String bizStatus);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPersonTaskService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPersonTaskService.java
new file mode 100644
index 0000000..1b14965
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPersonTaskService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPlaceService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPlaceService.java
new file mode 100644
index 0000000..a4f4a62
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsPlaceService.java
@@ -0,0 +1,14 @@
+package jnpf.tmsService;
+
+import jnpf.tmsEntity.place.TmsPlaceOption;
+
+import java.util.List;
+
+/**
+ * 鍩硅鍦扮偣
+ */
+public interface TmsPlaceService {
+
+    /** 鍚敤涓殑鍦扮偣涓嬫媺锛堜緵浠诲姟鍙戝竷銆佸煿璁柟寮忋�屽湴鐐瑰繀濉�嶈仈鍔ㄩ�夌敤锛� */
+    List<TmsPlaceOption> listOptions();
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsQuestionService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsQuestionService.java
new file mode 100644
index 0000000..4c46eea
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsQuestionService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsRecordService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsRecordService.java
new file mode 100644
index 0000000..8dd2113
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsRecordService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSelfTestService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSelfTestService.java
new file mode 100644
index 0000000..eee04ab
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSelfTestService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSignModeService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSignModeService.java
new file mode 100644
index 0000000..fbdedd3
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsSignModeService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTaskService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTaskService.java
new file mode 100644
index 0000000..ddf3490
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTaskService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainArchiveService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainArchiveService.java
new file mode 100644
index 0000000..378c92c
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainArchiveService.java
@@ -0,0 +1,12 @@
+package jnpf.tmsService;
+
+import jnpf.tmsEntity.archive.TmsTrainArchiveDetailVO;
+
+/**
+ * 涓汉鍩硅妗f
+ */
+public interface TmsTrainArchiveService {
+
+    /** 褰撳墠鐧诲綍鐢ㄦ埛鐨勪釜浜哄煿璁。妗堬紙鏃犳。鍒欒嚜鍔ㄥ缓妗o級 */
+    TmsTrainArchiveDetailVO getMine();
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainModeService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainModeService.java
new file mode 100644
index 0000000..7b9cbdd
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainModeService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainRecordService.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainRecordService.java
new file mode 100644
index 0000000..dd6c9c8
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/TmsTrainRecordService.java
@@ -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);
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsCourseServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsCourseServiceImpl.java
new file mode 100644
index 0000000..fdc83fe
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsCourseServiceImpl.java
@@ -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;
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsEvalModeServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsEvalModeServiceImpl.java
new file mode 100644
index 0000000..1f832a9
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsEvalModeServiceImpl.java
@@ -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;
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamGradeServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamGradeServiceImpl.java
new file mode 100644
index 0000000..be925c0
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamGradeServiceImpl.java
@@ -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;
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamScoreServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamScoreServiceImpl.java
new file mode 100644
index 0000000..1a9d484
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsExamScoreServiceImpl.java
@@ -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;
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsOnlineExamServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsOnlineExamServiceImpl.java
new file mode 100644
index 0000000..a867c6a
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsOnlineExamServiceImpl.java
@@ -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() {
+        }
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPaperServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPaperServiceImpl.java
new file mode 100644
index 0000000..f638ccc
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPaperServiceImpl.java
@@ -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");
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPersonTaskServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPersonTaskServiceImpl.java
new file mode 100644
index 0000000..406133f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPersonTaskServiceImpl.java
@@ -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);
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPlaceServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPlaceServiceImpl.java
new file mode 100644
index 0000000..6f5ae7f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsPlaceServiceImpl.java
@@ -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;
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsQuestionServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsQuestionServiceImpl.java
new file mode 100644
index 0000000..f6c427f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsQuestionServiceImpl.java
@@ -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();
+        }
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsRecordServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsRecordServiceImpl.java
new file mode 100644
index 0000000..e57ab28
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsRecordServiceImpl.java
@@ -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("璇烽�夋嫨瑕佸綊妗g殑璁板綍");
+        }
+        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);
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSelfTestServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSelfTestServiceImpl.java
new file mode 100644
index 0000000..34e25d0
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSelfTestServiceImpl.java
@@ -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(","));
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSignModeServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSignModeServiceImpl.java
new file mode 100644
index 0000000..1795830
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsSignModeServiceImpl.java
@@ -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;
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTaskServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTaskServiceImpl.java
new file mode 100644
index 0000000..834dce2
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTaskServiceImpl.java
@@ -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);
+        // 娓呯┖鍙戝竷鏃堕棿瀛楁锛坲pdateById 鍙兘璺宠繃 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("骞磋鍒扞D涓嶈兘涓虹┖");
+        }
+        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銆亂yyy-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();
+        }
+        // 宸叉槸绯荤粺鐢ㄦ埛锛屾垨妗f灏氭湭鍏宠仈鐢ㄦ埛
+        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);
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainArchiveServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainArchiveServiceImpl.java
new file mode 100644
index 0000000..0533b6c
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainArchiveServiceImpl.java
@@ -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();
+    }
+
+    /** 妗f缂栧彿锛欰R + 骞翠唤 + 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;
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainModeServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainModeServiceImpl.java
new file mode 100644
index 0000000..36092cc
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainModeServiceImpl.java
@@ -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;
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainRecordServiceImpl.java b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainRecordServiceImpl.java
new file mode 100644
index 0000000..b7d0498
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-biz/src/main/java/jnpf/tmsService/impl/TmsTrainRecordServiceImpl.java
@@ -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("妗fID涓嶈兘涓虹┖");
+        }
+        TmsPersonFileEntity file = personFileMapper.selectById(archiveId);
+        if (file == null) {
+            throw new DataException("涓汉鍩硅璁板綍涓嶅瓨鍦�");
+        }
+
+        List<TmsTrainRecordCatalogItemVO> items = new ArrayList<>();
+        Set<String> seenNos = new HashSet<>();
+
+        // 1) 妗f鐩綍琛紙鑻ュ凡鍚屾锛�
+        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;
+        }
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsCourseController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsCourseController.java
new file mode 100644
index 0000000..bde34a9
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsCourseController.java
@@ -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/** 锛圫tripPrefix=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("鍒犻櫎鎴愬姛");
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsEvalModeController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsEvalModeController.java
new file mode 100644
index 0000000..f870a85
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsEvalModeController.java
@@ -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/** 锛圫tripPrefix=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("鍒犻櫎鎴愬姛");
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamGradeController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamGradeController.java
new file mode 100644
index 0000000..aa86e74
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamGradeController.java
@@ -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/** 锛圫tripPrefix=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));
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamScoreController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamScoreController.java
new file mode 100644
index 0000000..9b0d711
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsExamScoreController.java
@@ -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/** 锛圫tripPrefix=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));
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsOnlineExamController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsOnlineExamController.java
new file mode 100644
index 0000000..b59a436
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsOnlineExamController.java
@@ -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/** 锛圫tripPrefix=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 = "鎸夌瓟鍗稩D鏌ョ湅鑰冭瘯鍥為【")
+    @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));
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPaperController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPaperController.java
new file mode 100644
index 0000000..1e75651
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPaperController.java
@@ -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/** 锛圫tripPrefix=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("鎿嶄綔鎴愬姛");
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPersonTaskController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPersonTaskController.java
new file mode 100644
index 0000000..68593f7
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPersonTaskController.java
@@ -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/** 锛圫tripPrefix=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()));
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPlaceController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPlaceController.java
new file mode 100644
index 0000000..15b6b2f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsPlaceController.java
@@ -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());
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsQuestionController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsQuestionController.java
new file mode 100644
index 0000000..9bde7b8
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsQuestionController.java
@@ -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/** 锛圫tripPrefix=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("搴熷純鎴愬姛");
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsRecordController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsRecordController.java
new file mode 100644
index 0000000..f98d126
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsRecordController.java
@@ -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/** 锛圫tripPrefix=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("鍒犻櫎鎴愬姛");
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSelfTestController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSelfTestController.java
new file mode 100644
index 0000000..78757c1
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSelfTestController.java
@@ -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/** 锛圫tripPrefix=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));
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSignModeController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSignModeController.java
new file mode 100644
index 0000000..5994c99
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsSignModeController.java
@@ -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/** 锛圫tripPrefix=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("鍒犻櫎鎴愬姛");
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTaskController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTaskController.java
new file mode 100644
index 0000000..16e9b8c
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTaskController.java
@@ -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/** 锛圫tripPrefix=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);
+        }
+        // 鏁版嵁鎺ュ彛棰勮甯告妸銆屾帴鍙e弬鏁般�嶆斁 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;
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainArchiveController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainArchiveController.java
new file mode 100644
index 0000000..5dfa295
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainArchiveController.java
@@ -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;
+
+/**
+ * 涓汉鍩硅妗f銆傜綉鍏� /api/tms/train-archive/** 锛圫tripPrefix=2锛夆啋 /train-archive/**
+ */
+@Tag(name = "TMS涓汉鍩硅妗f", description = "TrainArchive")
+@RestController
+@RequestMapping("/train-archive")
+@RequiredArgsConstructor
+public class TmsTrainArchiveController {
+
+    private final TmsTrainArchiveService trainArchiveService;
+
+    @Operation(summary = "鎴戠殑涓汉鍩硅妗f")
+    @GetMapping("/mine")
+    public ActionResult<TmsTrainArchiveDetailVO> mine() {
+        return ActionResult.success(trainArchiveService.getMine());
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainModeController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainModeController.java
new file mode 100644
index 0000000..fe9fa8f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainModeController.java
@@ -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/** 锛圫tripPrefix=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("鍒犻櫎鎴愬姛");
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainRecordController.java b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainRecordController.java
new file mode 100644
index 0000000..fa3a6f1
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-controller/src/main/java/jnpf/tmsController/TmsTrainRecordController.java
@@ -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/** 锛圫tripPrefix=2锛夆啋 /train-record/**
+ */
+@Tag(name = "TMS涓汉鍩硅璁板綍", description = "TrainRecord")
+@RestController
+@RequestMapping("/train-record")
+@RequiredArgsConstructor
+public class TmsTrainRecordController {
+
+    private final TmsTrainRecordService trainRecordService;
+
+    @Operation(summary = "涓汉鍩硅璁板綍鍒嗛〉鍒楄〃锛堟寜浜哄憳妗f锛�")
+    @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));
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanEntity.java
new file mode 100644
index 0000000..2ccbe45
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanItemEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanItemEntity.java
new file mode 100644
index 0000000..3964eab
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsAnnualPlanItemEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsCourseEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsCourseEntity.java
new file mode 100644
index 0000000..9cc701c
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsCourseEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsEvalModeEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsEvalModeEntity.java
new file mode 100644
index 0000000..4af9823
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsEvalModeEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamEntity.java
new file mode 100644
index 0000000..685b6e4
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamItemEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamItemEntity.java
new file mode 100644
index 0000000..53b57a7
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsExamItemEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsMaterialEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsMaterialEntity.java
new file mode 100644
index 0000000..18c462e
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsMaterialEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperEntity.java
new file mode 100644
index 0000000..20436e1
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperQuestionEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperQuestionEntity.java
new file mode 100644
index 0000000..71f6666
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperQuestionEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperSectionEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperSectionEntity.java
new file mode 100644
index 0000000..30060ac
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPaperSectionEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonFileEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonFileEntity.java
new file mode 100644
index 0000000..da7e796
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonFileEntity.java
@@ -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;
+
+/**
+ * 涓汉鍩硅妗f 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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTaskEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTaskEntity.java
new file mode 100644
index 0000000..f9aa3af
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTaskEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTrainEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTrainEntity.java
new file mode 100644
index 0000000..e9546e3
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonTrainEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonWorkEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonWorkEntity.java
new file mode 100644
index 0000000..3b12a2b
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPersonWorkEntity.java
@@ -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;
+
+/**
+ * 涓汉鍩硅妗f-宸ヤ綔灞ュ巻 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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPlaceEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPlaceEntity.java
new file mode 100644
index 0000000..cfbe763
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPlaceEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanEntity.java
new file mode 100644
index 0000000..7b64c12
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanItemEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanItemEntity.java
new file mode 100644
index 0000000..da6b668
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsPostPlanItemEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionBankEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionBankEntity.java
new file mode 100644
index 0000000..96979fb
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionBankEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionEntity.java
new file mode 100644
index 0000000..3528a3b
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionOptionEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionOptionEntity.java
new file mode 100644
index 0000000..3d78698
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsQuestionOptionEntity.java
@@ -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 姝g‘ / 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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsRecordEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsRecordEntity.java
new file mode 100644
index 0000000..d8c0898
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsRecordEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestEntity.java
new file mode 100644
index 0000000..e824483
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestItemEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestItemEntity.java
new file mode 100644
index 0000000..f80a41f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSelfTestItemEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSignModeEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSignModeEntity.java
new file mode 100644
index 0000000..7c2b657
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsSignModeEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskEntity.java
new file mode 100644
index 0000000..98ffe6e
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskFileEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskFileEntity.java
new file mode 100644
index 0000000..8ee3df4
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTaskFileEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainModeEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainModeEntity.java
new file mode 100644
index 0000000..f568f5f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainModeEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainerEntity.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainerEntity.java
new file mode 100644
index 0000000..d4d27b2
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/TmsTrainerEntity.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveCatalogVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveCatalogVO.java
new file mode 100644
index 0000000..e1f85f7
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveCatalogVO.java
@@ -0,0 +1,41 @@
+package jnpf.tmsEntity.archive;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 涓汉鍩硅妗f-鍩硅鐩綍
+ */
+@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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveDetailVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveDetailVO.java
new file mode 100644
index 0000000..1241be2
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveDetailVO.java
@@ -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;
+
+/**
+ * 涓汉鍩硅妗f璇︽儏
+ */
+@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<>();
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveProfileVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveProfileVO.java
new file mode 100644
index 0000000..86f933f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveProfileVO.java
@@ -0,0 +1,35 @@
+package jnpf.tmsEntity.archive;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 涓汉鍩硅妗f-鍩烘湰淇℃伅
+ */
+@Data
+public class TmsTrainArchiveProfileVO {
+
+    @Schema(description = "妗fID")
+    private String id;
+
+    @Schema(description = "妗f缂栧彿")
+    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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveWorkVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveWorkVO.java
new file mode 100644
index 0000000..c51de4c
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/archive/TmsTrainArchiveWorkVO.java
@@ -0,0 +1,26 @@
+package jnpf.tmsEntity.archive;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 涓汉鍩硅妗f-宸ヤ綔灞ュ巻
+ */
+@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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseForm.java
new file mode 100644
index 0000000..d40d692
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseQuery.java
new file mode 100644
index 0000000..f9f7eac
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsCourseQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsPaperOption.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsPaperOption.java
new file mode 100644
index 0000000..c45260d
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/course/TmsPaperOption.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeForm.java
new file mode 100644
index 0000000..6e91560
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeQuery.java
new file mode 100644
index 0000000..bdf84d0
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/evalmode/TmsEvalModeQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamAttemptVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamAttemptVO.java
new file mode 100644
index 0000000..95508e5
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamAttemptVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamDetailVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamDetailVO.java
new file mode 100644
index 0000000..cb8e76f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamDetailVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamGradeItemVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamGradeItemVO.java
new file mode 100644
index 0000000..b082d2e
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamGradeItemVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamListVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamListVO.java
new file mode 100644
index 0000000..382fea3
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamListVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamOptionVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamOptionVO.java
new file mode 100644
index 0000000..f645d5d
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamOptionVO.java
@@ -0,0 +1,12 @@
+package jnpf.tmsEntity.exam;
+
+import lombok.Data;
+
+@Data
+public class TmsOnlineExamOptionVO {
+
+    private String optionLabel;
+    private String optionContent;
+    /** 浠呬氦鍗峰洖椤炬椂鍥炰紶 */
+    private String isCorrect;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamPaperVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamPaperVO.java
new file mode 100644
index 0000000..237b9a1
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamPaperVO.java
@@ -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;
+    /** 鍓╀綑绉掓暟锛宯ull=涓嶉檺鏃� */
+    private Integer remainSeconds;
+
+    @Schema(description = "寮�鑰冩椂鍙戠幇宸茶秴鏃跺苟宸茶嚜鍔ㄤ氦鍗�")
+    private Boolean autoSubmitted;
+
+    @Schema(description = "鑰冭瘯搴忓彿锛�1=棣栬��")
+    private Integer attemptNo;
+
+    @Schema(description = "鑰冭瘯绫诲瀷鏂囨锛氶鑰�/琛ヨ��")
+    private String attemptLabel;
+
+    private List<TmsOnlineExamQuestionVO> questions;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuery.java
new file mode 100644
index 0000000..03cb3e1
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuestionVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuestionVO.java
new file mode 100644
index 0000000..bae45d8
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamQuestionVO.java
@@ -0,0 +1,24 @@
+package jnpf.tmsEntity.exam;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class TmsOnlineExamQuestionVO {
+
+    /** 绛斿嵎蹇収琛孖D锛屼綔绛旀寜姝ゆ彁浜� */
+    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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamStartForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamStartForm.java
new file mode 100644
index 0000000..2a20bae
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamStartForm.java
@@ -0,0 +1,10 @@
+package jnpf.tmsEntity.exam;
+
+import lombok.Data;
+
+@Data
+public class TmsOnlineExamStartForm {
+
+    /** 鍒楄〃琛孖D锛屽綋鍓嶄负璇曞嵎ID */
+    private String myPaperId;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitForm.java
new file mode 100644
index 0000000..a30af8c
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitForm.java
@@ -0,0 +1,12 @@
+package jnpf.tmsEntity.exam;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class TmsOnlineExamSubmitForm {
+
+    /** 蹇収琛孖D -> 閫夐」鏍囩 / 鏍囩鏁扮粍 / 濉┖鏂囨湰 */
+    private Map<String, Object> answers;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitResultVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitResultVO.java
new file mode 100644
index 0000000..00f0874
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/exam/TmsOnlineExamSubmitResultVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeDetailVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeDetailVO.java
new file mode 100644
index 0000000..2e0ac5d
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeDetailVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeExamVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeExamVO.java
new file mode 100644
index 0000000..01c739b
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeExamVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeItemVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeItemVO.java
new file mode 100644
index 0000000..336acaa
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeItemVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeOptionVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeOptionVO.java
new file mode 100644
index 0000000..5359c51
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeOptionVO.java
@@ -0,0 +1,9 @@
+package jnpf.tmsEntity.examgrade;
+
+import lombok.Data;
+
+@Data
+public class TmsExamGradeOptionVO {
+    private String optionLabel;
+    private String optionContent;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeQuery.java
new file mode 100644
index 0000000..ccf4834
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSessionVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSessionVO.java
new file mode 100644
index 0000000..6537a77
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSessionVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitForm.java
new file mode 100644
index 0000000..dd60823
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitForm.java
@@ -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;
+    }
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitResultVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitResultVO.java
new file mode 100644
index 0000000..617e7f0
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examgrade/TmsExamGradeSubmitResultVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreDetailVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreDetailVO.java
new file mode 100644
index 0000000..6a7759d
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreDetailVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreExportForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreExportForm.java
new file mode 100644
index 0000000..fb77809
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreExportForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreOptionVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreOptionVO.java
new file mode 100644
index 0000000..5f231ce
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreOptionVO.java
@@ -0,0 +1,10 @@
+package jnpf.tmsEntity.examscore;
+
+import lombok.Data;
+
+@Data
+public class TmsExamScoreOptionVO {
+
+    private String optionLabel;
+    private String optionContent;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperItemVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperItemVO.java
new file mode 100644
index 0000000..381a7b1
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperItemVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperVO.java
new file mode 100644
index 0000000..504c9e5
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScorePaperVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreQuery.java
new file mode 100644
index 0000000..ad82b12
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreRowVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreRowVO.java
new file mode 100644
index 0000000..574e5b0
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreRowVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreSummaryVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreSummaryVO.java
new file mode 100644
index 0000000..b08cae6
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/examscore/TmsExamScoreSummaryVO.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperForm.java
new file mode 100644
index 0000000..1d33b46
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuery.java
new file mode 100644
index 0000000..f353e21
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuestionForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuestionForm.java
new file mode 100644
index 0000000..23f9bff
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperQuestionForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperSectionForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperSectionForm.java
new file mode 100644
index 0000000..e8b4be9
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/paper/TmsPaperSectionForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskFileForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskFileForm.java
new file mode 100644
index 0000000..c55155b
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskFileForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnForm.java
new file mode 100644
index 0000000..4020eae
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnResult.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnResult.java
new file mode 100644
index 0000000..a1a917e
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskLearnResult.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskMineForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskMineForm.java
new file mode 100644
index 0000000..744e31b
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskMineForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskQuery.java
new file mode 100644
index 0000000..c577e39
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskSignResult.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskSignResult.java
new file mode 100644
index 0000000..c3fffbd
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/person/TmsPersonTaskSignResult.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/place/TmsPlaceOption.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/place/TmsPlaceOption.java
new file mode 100644
index 0000000..525ffac
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/place/TmsPlaceOption.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionBankOption.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionBankOption.java
new file mode 100644
index 0000000..5bb3c82
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionBankOption.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionForm.java
new file mode 100644
index 0000000..9439eb6
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionForm.java
@@ -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 = "瑙f瀽")
+    private String analysis;
+
+    @Schema(description = "绠$悊鍛樼敤鎴稩D")
+    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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionOptionForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionOptionForm.java
new file mode 100644
index 0000000..2961c06
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionOptionForm.java
@@ -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 = "鏄惁姝g‘ 1/0")
+    private String isCorrect;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionQuery.java
new file mode 100644
index 0000000..11e3756
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/question/TmsQuestionQuery.java
@@ -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 = "绠$悊鍛樼敤鎴稩D")
+    private String adminUserId;
+
+    @Schema(description = "鍏抽敭璇嶏紙缂栧彿/棰樺共锛�")
+    private String keyword;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordAttachmentForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordAttachmentForm.java
new file mode 100644
index 0000000..e80b459
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordAttachmentForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordForm.java
new file mode 100644
index 0000000..ea0bb76
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordForm.java
@@ -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 = "鍩硅甯堢敤鎴稩D")
+    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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordParticipantForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordParticipantForm.java
new file mode 100644
index 0000000..dd7d3ed
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordParticipantForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordQuery.java
new file mode 100644
index 0000000..7dbbe55
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordSignForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordSignForm.java
new file mode 100644
index 0000000..6a99a8a
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/record/TmsRecordSignForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestOptionVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestOptionVO.java
new file mode 100644
index 0000000..fca225d
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestOptionVO.java
@@ -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 = "鏄惁姝g‘ 1/0")
+    private String isCorrect;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestPaperVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestPaperVO.java
new file mode 100644
index 0000000..97b38fc
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestPaperVO.java
@@ -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 = "姝g‘棰樻暟")
+    private Integer correctCount;
+
+    @Schema(description = "姝g‘鐜�")
+    private BigDecimal scoreRate;
+
+    @Schema(description = "寮�濮嬫椂闂�")
+    private String startTime;
+
+    @Schema(description = "浜ゅ嵎鏃堕棿")
+    private String submitTime;
+
+    @Schema(description = "棰樼洰鍒楄〃")
+    private List<TmsSelfTestQuestionVO> questions;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuery.java
new file mode 100644
index 0000000..7f9d296
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuestionVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuestionVO.java
new file mode 100644
index 0000000..c8364c7
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestQuestionVO.java
@@ -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 = "姝g‘绛旀锛堥�楀彿鍒嗛殧鏍囩锛�")
+    private String correctAnswer;
+
+    @Schema(description = "鐢ㄦ埛浣滅瓟锛堥�楀彿鍒嗛殧鏍囩锛�")
+    private String userAnswer;
+
+    @Schema(description = "鏄惁姝g‘ 1/0")
+    private String isRight;
+
+    @Schema(description = "鎺掑簭鍙�")
+    private Integer sortNo;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestRecordVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestRecordVO.java
new file mode 100644
index 0000000..49c88d3
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestRecordVO.java
@@ -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 = "姝g‘棰樻暟")
+    private Integer correctCount;
+
+    @Schema(description = "姝g‘鐜�")
+    private BigDecimal scoreRate;
+
+    @Schema(description = "鐘舵��")
+    private String testStatus;
+
+    @Schema(description = "寮�濮嬫椂闂�")
+    private String startTime;
+
+    @Schema(description = "浜ゅ嵎鏃堕棿")
+    private String submitTime;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestStartForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestStartForm.java
new file mode 100644
index 0000000..2707529
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestStartForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitForm.java
new file mode 100644
index 0000000..7d81fc9
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitForm.java
@@ -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 = "浣滅瓟锛歲uestionId -> 閫夐」鏍囩(鍗曢��/鍒ゆ柇) 鎴� 鏍囩鏁扮粍(澶氶��)")
+    private Map<String, Object> answers;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitResultVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitResultVO.java
new file mode 100644
index 0000000..49c7db2
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestSubmitResultVO.java
@@ -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 = "姝g‘棰樻暟")
+    private Integer correctCount;
+
+    @Schema(description = "姝g‘鐜� 0-100")
+    private BigDecimal scoreRate;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestTypeSetting.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestTypeSetting.java
new file mode 100644
index 0000000..bf08fb9
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/selftest/TmsSelfTestTypeSetting.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeForm.java
new file mode 100644
index 0000000..35e7770
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeQuery.java
new file mode 100644
index 0000000..1f62501
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/signmode/TmsSignModeQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsFromPlanResult.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsFromPlanResult.java
new file mode 100644
index 0000000..438da27
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsFromPlanResult.java
@@ -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<>();
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsPersonTaskForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsPersonTaskForm.java
new file mode 100644
index 0000000..46987aa
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsPersonTaskForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskFileForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskFileForm.java
new file mode 100644
index 0000000..b0c5a7b
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskFileForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskForm.java
new file mode 100644
index 0000000..4d4c9c2
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskForm.java
@@ -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 = "鍩硅甯堢敤鎴稩D")
+    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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskQuery.java
new file mode 100644
index 0000000..39dcad5
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/task/TmsTaskQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeForm.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeForm.java
new file mode 100644
index 0000000..312c17d
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeForm.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeQuery.java
new file mode 100644
index 0000000..719a86c
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainmode/TmsTrainModeQuery.java
@@ -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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogItemVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogItemVO.java
new file mode 100644
index 0000000..c5b0279
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogItemVO.java
@@ -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 = "鐩綍琛孖D")
+    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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogVO.java
new file mode 100644
index 0000000..65c9721
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordCatalogVO.java
@@ -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 = "妗fID")
+    private String id;
+
+    @Schema(description = "濮撳悕")
+    private String userName;
+
+    @Schema(description = "妗f缂栧彿")
+    private String archiveNo;
+
+    @Schema(description = "鍩硅鐩綍")
+    private List<TmsTrainRecordCatalogItemVO> list;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordListVO.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordListVO.java
new file mode 100644
index 0000000..9e3d193
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordListVO.java
@@ -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 = "妗fID")
+    private String id;
+
+    @Schema(description = "妗f缂栧彿")
+    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;
+}
diff --git a/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordQuery.java b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordQuery.java
new file mode 100644
index 0000000..6f3478a
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-entity/src/main/java/jnpf/tmsEntity/trainrecord/TmsTrainRecordQuery.java
@@ -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 = "鍏抽敭璇嶏紙濮撳悕/閮ㄩ棬/妗f缂栧彿锛�")
+    private String keyword;
+
+    @Schema(description = "妗f缂栧彿")
+    private String archiveNo;
+}
diff --git a/jnpf-tms/jnpf-tms-server/src/main/resources/application.yml b/jnpf-tms/jnpf-tms-server/src/main/resources/application.yml
index 5c14594..3bbe549 100644
--- a/jnpf-tms/jnpf-tms-server/src/main/resources/application.yml
+++ b/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:
diff --git a/jnpf-tms/jnpf-tms-server/src/main/resources/tms-datasource.yaml b/jnpf-tms/jnpf-tms-server/src/main/resources/tms-datasource.yaml
new file mode 100644
index 0000000..48d080f
--- /dev/null
+++ b/jnpf-tms/jnpf-tms-server/src/main/resources/tms-datasource.yaml
@@ -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

--
Gitblit v1.8.0