Jelajahi Sumber

Merge branch 'dev_20260630' into dev_20260630_cxf

chenxiaofei 4 hari lalu
induk
melakukan
d1e1a3428a
99 mengubah file dengan 5350 tambahan dan 35 penghapusan
  1. 2 2
      sckw-auth/src/main/resources/bootstrap-prod.yml
  2. 2 2
      sckw-gateway/src/main/resources/bootstrap-prod.yml
  3. 2 2
      sckw-modules/sckw-contract/src/main/resources/bootstrap-prod.yml
  4. 2 2
      sckw-modules/sckw-contract/src/main/resources/bootstrap.yml
  5. 2 2
      sckw-modules/sckw-example/src/main/resources/bootstrap-prod.yml
  6. 2 2
      sckw-modules/sckw-example/src/main/resources/bootstrap-test.yml
  7. 1 1
      sckw-modules/sckw-example/src/main/resources/bootstrap-xcq.yml
  8. 2 2
      sckw-modules/sckw-file/src/main/resources/bootstrap-prod.yml
  9. 2 2
      sckw-modules/sckw-fleet/src/main/resources/bootstrap-prod.yml
  10. 2 2
      sckw-modules/sckw-manage/src/main/resources/bootstrap-prod.yml
  11. 2 2
      sckw-modules/sckw-message/src/main/resources/bootstrap-prod.yml
  12. 2 2
      sckw-modules/sckw-operation/src/main/resources/bootstrap-prod.yml
  13. 2 2
      sckw-modules/sckw-order/src/main/resources/bootstrap-prod.yml
  14. 2 2
      sckw-modules/sckw-payment/src/main/resources/bootstrap-prod.yml
  15. 2 2
      sckw-modules/sckw-product/src/main/resources/bootstrap-prod.yml
  16. 2 2
      sckw-modules/sckw-report/src/main/resources/bootstrap-prod.yml
  17. 74 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/controller/KwsCourseController.java
  18. 73 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/controller/KwsExamController.java
  19. 59 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/controller/KwsTrainController.java
  20. 77 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/controller/app/TrainingsController.java
  21. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TCourseMapper.java
  22. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TCourseRecordMapper.java
  23. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TExamLogMapper.java
  24. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TExamMapper.java
  25. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TExamRecordMapper.java
  26. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TQuestionItemMapper.java
  27. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TQuestionMapper.java
  28. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TTrainCourseMapper.java
  29. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TTrainExamMapper.java
  30. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TTrainMapper.java
  31. 13 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TTrainRecordMapper.java
  32. 119 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TCourse.java
  33. 105 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TCourseRecord.java
  34. 112 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TExam.java
  35. 126 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TExamLog.java
  36. 91 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TExamRecord.java
  37. 91 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TQuestion.java
  38. 105 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TQuestionItem.java
  39. 113 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TTrain.java
  40. 41 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TTrainCourse.java
  41. 41 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TTrainExam.java
  42. 112 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TTrainRecord.java
  43. 28 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/pojo/CourseStatusEnum.java
  44. 28 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/pojo/CourseTypeEnum.java
  45. 28 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/pojo/ExamStatusEnum.java
  46. 29 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/pojo/QuestionTypeEnum.java
  47. 29 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/pojo/TrainStatusEnum.java
  48. 31 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/CourseManagePageReqVo.java
  49. 40 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/CourseSaveReqVo.java
  50. 22 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/CourseStatusReqVo.java
  51. 28 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/ExamManagePageReqVo.java
  52. 81 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/ExamSaveReqVo.java
  53. 22 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/ExamStatusReqVo.java
  54. 42 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/TrainAppExamSubmitReqVo.java
  55. 27 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/TrainAppListReqVo.java
  56. 25 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/TrainCourseStudyReqVo.java
  57. 28 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/TrainManagePageReqVo.java
  58. 51 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/TrainSaveReqVo.java
  59. 56 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/CourseDetailResVo.java
  60. 46 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/CourseManageItemResVo.java
  61. 100 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/ExamDetailResVo.java
  62. 43 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/ExamManageItemResVo.java
  63. 80 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppDetailResVo.java
  64. 75 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppExamPaperResVo.java
  65. 40 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppExamStartResVo.java
  66. 28 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppExamSubmitResVo.java
  67. 26 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppListResVo.java
  68. 31 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppTaskCourseResVo.java
  69. 51 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppTaskResVo.java
  70. 162 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainDetailResVo.java
  71. 46 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainManageItemResVo.java
  72. 284 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsCourseService.java
  73. 516 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsExamService.java
  74. 839 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsTrainAppService.java
  75. 436 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsTrainService.java
  76. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TCourseRecordService.java
  77. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TCourseService.java
  78. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TExamLogService.java
  79. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TExamRecordService.java
  80. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TExamService.java
  81. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TQuestionItemService.java
  82. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TQuestionService.java
  83. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TTrainCourseService.java
  84. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TTrainExamService.java
  85. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TTrainRecordService.java
  86. 16 0
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TTrainService.java
  87. 2 2
      sckw-modules/sckw-system/src/main/resources/bootstrap-prod.yml
  88. 27 0
      sckw-modules/sckw-system/src/main/resources/mapper/TCourseMapper.xml
  89. 25 0
      sckw-modules/sckw-system/src/main/resources/mapper/TCourseRecordMapper.xml
  90. 28 0
      sckw-modules/sckw-system/src/main/resources/mapper/TExamLogMapper.xml
  91. 26 0
      sckw-modules/sckw-system/src/main/resources/mapper/TExamMapper.xml
  92. 23 0
      sckw-modules/sckw-system/src/main/resources/mapper/TExamRecordMapper.xml
  93. 25 0
      sckw-modules/sckw-system/src/main/resources/mapper/TQuestionItemMapper.xml
  94. 23 0
      sckw-modules/sckw-system/src/main/resources/mapper/TQuestionMapper.xml
  95. 15 0
      sckw-modules/sckw-system/src/main/resources/mapper/TTrainCourseMapper.xml
  96. 15 0
      sckw-modules/sckw-system/src/main/resources/mapper/TTrainExamMapper.xml
  97. 26 0
      sckw-modules/sckw-system/src/main/resources/mapper/TTrainMapper.xml
  98. 26 0
      sckw-modules/sckw-system/src/main/resources/mapper/TTrainRecordMapper.xml
  99. 2 2
      sckw-modules/sckw-transport/src/main/resources/bootstrap-prod.yml

+ 2 - 2
sckw-auth/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-gateway/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-contract/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-contract/src/main/resources/bootstrap.yml

@@ -5,7 +5,7 @@ spring:
   application:
     name: sckw-ng-contract
   profiles:
-    active:  @profiles.active@
+    active: @profiles.active@
   main:
     allow-bean-definition-overriding: true
-    allow-circular-references: true
+    allow-circular-references: true

+ 2 - 2
sckw-modules/sckw-example/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-example/src/main/resources/bootstrap-test.yml

@@ -19,11 +19,11 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
 
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-service-platform
-            refresh: true
+            refresh: true

+ 1 - 1
sckw-modules/sckw-example/src/main/resources/bootstrap-xcq.yml

@@ -19,7 +19,7 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
 
         #可以读多个配置文件  需要在同一个命名空间下面可以是不同的组

+ 2 - 2
sckw-modules/sckw-file/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-fleet/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-manage/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-message/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-operation/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-order/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-payment/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-product/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 2 - 2
sckw-modules/sckw-report/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 74 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/controller/KwsCourseController.java

@@ -0,0 +1,74 @@
+package com.sckw.system.controller;
+
+import com.sckw.core.web.response.BaseResult;
+import com.sckw.core.web.response.result.PageDataResult;
+import com.sckw.system.model.vo.req.CourseManagePageReqVo;
+import com.sckw.system.model.vo.req.CourseSaveReqVo;
+import com.sckw.system.model.vo.req.CourseStatusReqVo;
+import com.sckw.system.model.vo.req.IdReqVo;
+import com.sckw.system.model.vo.res.CourseDetailResVo;
+import com.sckw.system.model.vo.res.CourseManageItemResVo;
+import com.sckw.system.service.KwsCourseService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/kwsCourse")
+@Tag(name = "培训课程(PC)")
+@RequiredArgsConstructor
+public class KwsCourseController {
+
+    private final KwsCourseService kwsCourseService;
+
+    @GetMapping("/list")
+    @Operation(summary = "课程列表", description = "PC端课程列表(仅启用)")
+    public BaseResult<List<CourseManageItemResVo>> list() {
+        return BaseResult.success(kwsCourseService.listEnabledCourses());
+    }
+
+    @PostMapping("/page")
+    @Operation(summary = "课程分页查询", description = "PC端培训课程列表分页查询")
+    public BaseResult<PageDataResult<CourseManageItemResVo>> page(@RequestBody CourseManagePageReqVo reqVo) {
+        return BaseResult.success(kwsCourseService.manageCoursePage(reqVo));
+    }
+
+    @GetMapping("/detail")
+    @Operation(summary = "课程详情", description = "PC端培训课程详情")
+    public BaseResult<CourseDetailResVo> detail(@RequestParam("id") Long id) {
+        return BaseResult.success(kwsCourseService.manageCourseDetail(id));
+    }
+
+    @PostMapping("/add")
+    @Operation(summary = "新增课程", description = "PC端新增培训课程")
+    public BaseResult<Long> add(@RequestBody CourseSaveReqVo reqVo) {
+        return BaseResult.success(kwsCourseService.addCourse(reqVo));
+    }
+
+    @PostMapping("/update")
+    @Operation(summary = "编辑课程", description = "PC端编辑培训课程")
+    public BaseResult<Boolean> update(@RequestBody CourseSaveReqVo reqVo) {
+        return BaseResult.success(kwsCourseService.updateCourse(reqVo));
+    }
+
+    @PostMapping("/updateStatus")
+    @Operation(summary = "启用停用课程", description = "PC端启用/停用培训课程")
+    public BaseResult<Boolean> updateStatus(@RequestBody CourseStatusReqVo reqVo) {
+        return BaseResult.success(kwsCourseService.updateCourseStatus(reqVo));
+    }
+
+    @PostMapping("/delete")
+    @Operation(summary = "删除课程", description = "PC端删除培训课程(逻辑删除)")
+    public BaseResult<Boolean> delete(@RequestBody IdReqVo reqVo) {
+        return BaseResult.success(kwsCourseService.deleteCourse(reqVo));
+    }
+}
+

+ 73 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/controller/KwsExamController.java

@@ -0,0 +1,73 @@
+package com.sckw.system.controller;
+
+import com.sckw.core.web.response.BaseResult;
+import com.sckw.core.web.response.result.PageDataResult;
+import com.sckw.system.model.vo.req.ExamManagePageReqVo;
+import com.sckw.system.model.vo.req.ExamSaveReqVo;
+import com.sckw.system.model.vo.req.ExamStatusReqVo;
+import com.sckw.system.model.vo.req.IdReqVo;
+import com.sckw.system.model.vo.res.ExamDetailResVo;
+import com.sckw.system.model.vo.res.ExamManageItemResVo;
+import com.sckw.system.service.KwsExamService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/kwsExam")
+@Tag(name = "考试管理(PC)")
+@RequiredArgsConstructor
+public class KwsExamController {
+
+    private final KwsExamService kwsExamService;
+
+    @GetMapping("/list")
+    @Operation(summary = "考试列表", description = "PC端考试列表(仅上架)")
+    public BaseResult<List<ExamManageItemResVo>> list() {
+        return BaseResult.success(kwsExamService.listOnShelfExams());
+    }
+
+    @PostMapping("/page")
+    @Operation(summary = "考试分页查询", description = "PC端考试管理列表分页查询")
+    public BaseResult<PageDataResult<ExamManageItemResVo>> page(@RequestBody ExamManagePageReqVo reqVo) {
+        return BaseResult.success(kwsExamService.manageExamPage(reqVo));
+    }
+
+    @GetMapping("/detail")
+    @Operation(summary = "考试详情", description = "PC端考试详情(包含题目、选项及答案标记)")
+    public BaseResult<ExamDetailResVo> detail(@RequestParam("id") Long id) {
+        return BaseResult.success(kwsExamService.manageExamDetail(id));
+    }
+
+    @PostMapping("/add")
+    @Operation(summary = "新增考试", description = "PC端新增考试(包含题目、选项)")
+    public BaseResult<Long> add(@RequestBody ExamSaveReqVo reqVo) {
+        return BaseResult.success(kwsExamService.addExam(reqVo));
+    }
+
+    @PostMapping("/update")
+    @Operation(summary = "编辑考试", description = "PC端编辑考试(包含题目、选项)")
+    public BaseResult<Boolean> update(@RequestBody ExamSaveReqVo reqVo) {
+        return BaseResult.success(kwsExamService.updateExam(reqVo));
+    }
+
+    @PostMapping("/updateStatus")
+    @Operation(summary = "上架下架考试", description = "PC端上架/下架考试")
+    public BaseResult<Boolean> updateStatus(@RequestBody ExamStatusReqVo reqVo) {
+        return BaseResult.success(kwsExamService.updateExamStatus(reqVo));
+    }
+
+    @PostMapping("/delete")
+    @Operation(summary = "删除考试", description = "PC端删除考试(逻辑删除,包含题目与选项)")
+    public BaseResult<Boolean> delete(@RequestBody IdReqVo reqVo) {
+        return BaseResult.success(kwsExamService.deleteExam(reqVo));
+    }
+}

+ 59 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/controller/KwsTrainController.java

@@ -0,0 +1,59 @@
+package com.sckw.system.controller;
+
+import com.sckw.core.web.response.BaseResult;
+import com.sckw.core.web.response.result.PageDataResult;
+import com.sckw.system.model.vo.req.IdReqVo;
+import com.sckw.system.model.vo.req.TrainManagePageReqVo;
+import com.sckw.system.model.vo.req.TrainSaveReqVo;
+import com.sckw.system.model.vo.res.TrainDetailResVo;
+import com.sckw.system.model.vo.res.TrainManageItemResVo;
+import com.sckw.system.service.KwsTrainService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/kwsTrain")
+@Tag(name = "培训管理(PC)")
+@RequiredArgsConstructor
+public class KwsTrainController {
+
+    private final KwsTrainService kwsTrainService;
+
+    @PostMapping("/page")
+    @Operation(summary = "培训分页查询", description = "PC端培训管理列表分页查询")
+    public BaseResult<PageDataResult<TrainManageItemResVo>> page(@RequestBody TrainManagePageReqVo reqVo) {
+        return BaseResult.success(kwsTrainService.manageTrainPage(reqVo));
+    }
+
+    @GetMapping("/detail")
+    @Operation(summary = "培训详情", description = "PC端培训详情(包含课程、考试、参与司机及进度统计)")
+    public BaseResult<TrainDetailResVo> detail(@RequestParam("id") Long id) {
+        return BaseResult.success(kwsTrainService.manageTrainDetail(id));
+    }
+
+    @PostMapping("/add")
+    @Operation(summary = "新增培训", description = "PC端新增培训计划(选择司机、课程、考试与时间)")
+    public BaseResult<Long> add(@RequestBody TrainSaveReqVo reqVo) {
+        return BaseResult.success(kwsTrainService.addTrain(reqVo));
+    }
+
+    @PostMapping("/update")
+    @Operation(summary = "编辑培训", description = "PC端编辑培训计划(覆盖更新司机、课程、考试与时间)")
+    public BaseResult<Boolean> update(@RequestBody TrainSaveReqVo reqVo) {
+        return BaseResult.success(kwsTrainService.updateTrain(reqVo));
+    }
+
+    @PostMapping("/delete")
+    @Operation(summary = "删除培训", description = "PC端删除培训计划(逻辑删除)")
+    public BaseResult<Boolean> delete(@RequestBody IdReqVo reqVo) {
+        return BaseResult.success(kwsTrainService.deleteTrain(reqVo));
+    }
+}
+

+ 77 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/controller/app/TrainingsController.java

@@ -0,0 +1,77 @@
+package com.sckw.system.controller.app;
+
+import com.sckw.core.web.response.BaseResult;
+import com.sckw.system.model.vo.req.TrainAppExamSubmitReqVo;
+import com.sckw.system.model.vo.req.TrainAppListReqVo;
+import com.sckw.system.model.vo.req.TrainCourseStudyReqVo;
+import com.sckw.system.model.vo.res.*;
+import com.sckw.system.service.KwsTrainAppService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/train")
+@Tag(name = "培训学习(App)")
+@RequiredArgsConstructor
+public class TrainingsController {
+
+    private final KwsTrainAppService kwsTrainAppService;
+
+    @PostMapping("/list")
+    @Operation(summary = "培训任务列表", description = "分页查询当前司机的培训任务,包含整体学习进度与任务列表")
+    public BaseResult<TrainAppListResVo> list(@RequestBody @Valid TrainAppListReqVo reqVo) {
+        return BaseResult.success(kwsTrainAppService.listTrainTasks(reqVo));
+    }
+
+    @GetMapping("/detail")
+    @Operation(summary = "培训任务详情", description = "查询当前司机指定培训任务详情,包含课程学习状态与考试信息")
+    public BaseResult<TrainAppDetailResVo> detail(@RequestParam("trainId") Long trainId) {
+        return BaseResult.success(kwsTrainAppService.trainDetail(trainId));
+    }
+
+    @GetMapping("/course/detail")
+    @Operation(summary = "培训课程详情", description = "查询培训课程详情")
+    public BaseResult<CourseDetailResVo> courseDetail(@RequestParam("courseId") Long courseId) {
+        return BaseResult.success(kwsTrainAppService.courseDetail(courseId));
+    }
+
+    @PostMapping("/course/start")
+    @Operation(summary = "开始学习课程", description = "写入课程学习开始记录,并维护培训记录状态")
+    public BaseResult<Boolean> startCourse(@RequestBody @Valid TrainCourseStudyReqVo reqVo) {
+        return BaseResult.success(kwsTrainAppService.startCourse(reqVo));
+    }
+
+    @PostMapping("/course/finish")
+    @Operation(summary = "完成课程学习", description = "写入课程学习完成记录,并维护培训记录完成课程数/状态")
+    public BaseResult<Boolean> finishCourse(@RequestBody @Valid TrainCourseStudyReqVo reqVo) {
+        return BaseResult.success(kwsTrainAppService.finishCourse(reqVo));
+    }
+
+    @GetMapping("/exam/paper")
+    @Operation(summary = "考试试卷", description = "查询培训对应考试试卷(不包含正确答案)")
+    public BaseResult<TrainAppExamPaperResVo> examPaper(@RequestParam("trainId") Long trainId, @RequestParam(required = false) Long logId) {
+        return BaseResult.success(kwsTrainAppService.examPaper(trainId, logId));
+    }
+
+    @PostMapping("/exam/start")
+    @Operation(summary = "开始考试", description = "创建考试日志,返回考试基本信息与日志ID")
+    public BaseResult<TrainAppExamStartResVo> startExam(@RequestParam("trainId") Long trainId) {
+        return BaseResult.success(kwsTrainAppService.startExam(trainId));
+    }
+
+
+    @PostMapping("/exam/submit")
+    @Operation(summary = "提交考试", description = "提交考试答案,写入考试记录并更新考试日志/培训记录")
+    public BaseResult<TrainAppExamSubmitResVo> submitExam(@RequestBody @Valid TrainAppExamSubmitReqVo reqVo) {
+        return BaseResult.success(kwsTrainAppService.submitExam(reqVo));
+    }
+}
+

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TCourseMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TCourse;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TCourseMapper extends BaseMapper<TCourse> {
+}

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TCourseRecordMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TCourseRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TCourseRecordMapper extends BaseMapper<TCourseRecord> {
+}

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TExamLogMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TExamLog;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TExamLogMapper extends BaseMapper<TExamLog> {
+}

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TExamMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TExam;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TExamMapper extends BaseMapper<TExam> {
+}

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TExamRecordMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TExamRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TExamRecordMapper extends BaseMapper<TExamRecord> {
+}

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TQuestionItemMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TQuestionItem;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TQuestionItemMapper extends BaseMapper<TQuestionItem> {
+}

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TQuestionMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TQuestion;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TQuestionMapper extends BaseMapper<TQuestion> {
+}

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TTrainCourseMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TTrainCourse;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TTrainCourseMapper extends BaseMapper<TTrainCourse> {
+}

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TTrainExamMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TTrainExam;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TTrainExamMapper extends BaseMapper<TTrainExam> {
+}

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TTrainMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TTrain;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TTrainMapper extends BaseMapper<TTrain> {
+}

+ 13 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/dao/TTrainRecordMapper.java

@@ -0,0 +1,13 @@
+package com.sckw.system.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sckw.system.model.TTrainRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Mapper
+public interface TTrainRecordMapper extends BaseMapper<TTrainRecord> {
+}

+ 119 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TCourse.java

@@ -0,0 +1,119 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 培训课程
+ */
+@Schema(description="培训课程")
+@Data
+@TableName(value = "t_course")
+public class TCourse {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 企业id
+     */
+    @TableField(value = "ent_id")
+    @Schema(description="企业id")
+    private Long entId;
+
+    /**
+     * 课程名称
+     */
+    @TableField(value = "course_name")
+    @Schema(description="课程名称")
+    private String courseName;
+
+    /**
+     * 课程类型 1-图文课程 2-视频课程
+     */
+    @TableField(value = "course_type")
+    @Schema(description="课程类型 1-图文课程 2-视频课程")
+    private Integer courseType;
+
+    /**
+     * 课程时间
+     */
+    @TableField(value = "course_time")
+    @Schema(description="课程时间")
+    private Integer courseTime;
+
+    /**
+     * 内容
+     */
+    @TableField(value = "content")
+    @Schema(description="内容")
+    private String content;
+
+    /**
+     * 视频文件
+     */
+    @TableField(value = "file_url")
+    @Schema(description="视频文件")
+    private String fileUrl;
+
+    /**
+     * 描述
+     */
+    @TableField(value = "description")
+    @Schema(description="描述")
+    private String description;
+
+    /**
+     * 状态:0-启用  1-停用
+     */
+    @TableField(value = "`status`")
+    @Schema(description="状态:0-启用  1-停用")
+    private Integer status;
+
+    /**
+     * 创建人
+     */
+    @TableField(value = "create_by")
+    @Schema(description="创建人")
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time")
+    @Schema(description="创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新人
+     */
+    @TableField(value = "update_by")
+    @Schema(description="更新人")
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time")
+    @Schema(description="更新时间")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除标识(0正常/1删除)
+     */
+    @TableField(value = "del_flag")
+    @Schema(description="删除标识(0正常/1删除)")
+    private Integer delFlag;
+}

+ 105 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TCourseRecord.java

@@ -0,0 +1,105 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 培训课程记录
+ */
+@Schema(description="培训课程记录")
+@Data
+@TableName(value = "t_course_record")
+public class TCourseRecord {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 培训计划id
+     */
+    @TableField(value = "train_id")
+    @Schema(description="培训计划id")
+    private Long trainId;
+
+    /**
+     * 课程id
+     */
+    @TableField(value = "course_id")
+    @Schema(description="课程id")
+    private Long courseId;
+
+    /**
+     * 司机id
+     */
+    @TableField(value = "driver_id")
+    @Schema(description="司机id")
+    private Long driverId;
+
+    /**
+     * 开始时间
+     */
+    @TableField(value = "start_time")
+    @Schema(description="开始时间")
+    private LocalDateTime startTime;
+
+    /**
+     * 结束时间
+     */
+    @TableField(value = "end_time")
+    @Schema(description="结束时间")
+    private LocalDateTime endTime;
+
+    /**
+     * 状态:0-待学习 1-已学习
+     */
+    @TableField(value = "`status`")
+    @Schema(description="状态:0-待学习 1-已学习")
+    private Integer status;
+
+    /**
+     * 创建人
+     */
+    @TableField(value = "create_by")
+    @Schema(description="创建人")
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time")
+    @Schema(description="创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新人
+     */
+    @TableField(value = "update_by")
+    @Schema(description="更新人")
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time")
+    @Schema(description="更新时间")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除标识(0正常/1删除)
+     */
+    @TableField(value = "del_flag")
+    @Schema(description="删除标识(0正常/1删除)")
+    private Integer delFlag;
+}

+ 112 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TExam.java

@@ -0,0 +1,112 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 考试
+ */
+@Schema(description="考试")
+@Data
+@TableName(value = "t_exam")
+public class TExam {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 企业id
+     */
+    @TableField(value = "ent_id")
+    @Schema(description="企业id")
+    private Long entId;
+
+    /**
+     * 考试名称
+     */
+    @TableField(value = "`name`")
+    @Schema(description="考试名称")
+    private String name;
+
+    /**
+     * 考试时长
+     */
+    @TableField(value = "exam_duration")
+    @Schema(description="考试时长")
+    private Integer examDuration;
+
+    /**
+     * 及格率
+     */
+    @TableField(value = "pass_rate")
+    @Schema(description="及格率")
+    private Integer passRate;
+
+    /**
+     * 题目数量
+     */
+    @TableField(value = "amount")
+    @Schema(description="题目数量")
+    private Integer amount;
+
+    /**
+     * 描述
+     */
+    @TableField(value = "description")
+    @Schema(description="描述")
+    private String description;
+
+    /**
+     * 状态 1-上架 2-下架
+     */
+    @TableField(value = "`status`")
+    @Schema(description="状态 1-上架 2-下架")
+    private Integer status;
+
+    /**
+     * 创建者
+     */
+    @TableField(value = "create_by")
+    @Schema(description="创建者")
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time")
+    @Schema(description="创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    @TableField(value = "update_by")
+    @Schema(description="更新者")
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time")
+    @Schema(description="更新时间")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除标志
+     */
+    @TableField(value = "del_flag")
+    @Schema(description="删除标志")
+    private Integer delFlag;
+}

+ 126 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TExamLog.java

@@ -0,0 +1,126 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 考试日志
+ */
+@Schema(description="考试日志")
+@Data
+@TableName(value = "t_exam_log")
+public class TExamLog {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 培训计划id
+     */
+    @TableField(value = "train_id")
+    @Schema(description="培训计划id")
+    private Long trainId;
+
+    /**
+     * 考试id
+     */
+    @TableField(value = "exam_id")
+    @Schema(description="考试id")
+    private Long examId;
+
+    /**
+     * 司机id
+     */
+    @TableField(value = "driver_id")
+    @Schema(description="司机id")
+    private Long driverId;
+
+    /**
+     * 考试状态 1-进行中 2-已完成
+     */
+    @TableField(value = "exam_flag")
+    @Schema(description="考试状态 1-进行中 2-已完成")
+    private Integer examFlag;
+
+    /**
+     * 开始时间
+     */
+    @TableField(value = "start_time")
+    @Schema(description="开始时间")
+    private LocalDateTime startTime;
+
+    /**
+     * 完成时间
+     */
+    @TableField(value = "end_time")
+    @Schema(description="完成时间")
+    private LocalDateTime endTime;
+
+    /**
+     * 正确题目
+     */
+    @TableField(value = "right_amount")
+    @Schema(description="正确题目")
+    private Integer rightAmount;
+
+    /**
+     * 总题
+     */
+    @TableField(value = "amount")
+    @Schema(description="总题")
+    private Integer amount;
+
+    /**
+     * 正确率
+     */
+    @TableField(value = "right_rate")
+    @Schema(description="正确率")
+    private Integer rightRate;
+
+    /**
+     * 创建者
+     */
+    @TableField(value = "create_by")
+    @Schema(description="创建者")
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time")
+    @Schema(description="创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    @TableField(value = "update_by")
+    @Schema(description="更新者")
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time")
+    @Schema(description="更新时间")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除标志
+     */
+    @TableField(value = "del_flag")
+    @Schema(description="删除标志")
+    private Integer delFlag;
+}

+ 91 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TExamRecord.java

@@ -0,0 +1,91 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 考试记录
+ */
+@Schema(description="考试记录")
+@Data
+@TableName(value = "t_exam_record")
+public class TExamRecord {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 日志id
+     */
+    @TableField(value = "log_id")
+    @Schema(description="日志id")
+    private Long logId;
+
+    /**
+     * 题目id
+     */
+    @TableField(value = "question_id")
+    @Schema(description="题目id")
+    private Long questionId;
+
+    /**
+     * 用户选择的答案 A,B,C
+     */
+    @TableField(value = "answer")
+    @Schema(description="用户选择的答案 A,B,C")
+    private String answer;
+
+    /**
+     * 是否正确 1-是 2-否 3-部分正确
+     */
+    @TableField(value = "correct_flag")
+    @Schema(description="是否正确 1-是 2-否 3-部分正确")
+    private Integer correctFlag;
+
+    /**
+     * 创建者
+     */
+    @TableField(value = "create_by")
+    @Schema(description="创建者")
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time")
+    @Schema(description="创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    @TableField(value = "update_by")
+    @Schema(description="更新者")
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time")
+    @Schema(description="更新时间")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除标志
+     */
+    @TableField(value = "del_flag")
+    @Schema(description="删除标志")
+    private Integer delFlag;
+}

+ 91 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TQuestion.java

@@ -0,0 +1,91 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 考题
+ */
+@Schema(description="考题")
+@Data
+@TableName(value = "t_question")
+public class TQuestion {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 企业id
+     */
+    @TableField(value = "ent_id")
+    @Schema(description="企业id")
+    private Long entId;
+
+    /**
+     * 考试id
+     */
+    @TableField(value = "exam_id")
+    @Schema(description="考试id")
+    private Long examId;
+
+    /**
+     * 考题名称
+     */
+    @TableField(value = "`name`")
+    @Schema(description="考题名称")
+    private String name;
+
+    /**
+     * 类型 1-单选 2-多选 3-问答
+     */
+    @TableField(value = "`type`")
+    @Schema(description="类型 1-单选 2-多选 3-问答")
+    private Integer type;
+
+    /**
+     * 创建者
+     */
+    @TableField(value = "create_by")
+    @Schema(description="创建者")
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time")
+    @Schema(description="创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    @TableField(value = "update_by")
+    @Schema(description="更新者")
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time")
+    @Schema(description="更新时间")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除标志
+     */
+    @TableField(value = "del_flag")
+    @Schema(description="删除标志")
+    private Integer delFlag;
+}

+ 105 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TQuestionItem.java

@@ -0,0 +1,105 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 选项
+ */
+@Schema(description="选项")
+@Data
+@TableName(value = "t_question_item")
+public class TQuestionItem {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 企业id
+     */
+    @TableField(value = "ent_id")
+    @Schema(description="企业id")
+    private Long entId;
+
+    /**
+     * 考题id
+     */
+    @TableField(value = "question_id")
+    @Schema(description="考题id")
+    private Long questionId;
+
+    /**
+     * 选项名称 A,B,C,D
+     */
+    @TableField(value = "`name`")
+    @Schema(description="选项名称 A,B,C,D")
+    private String name;
+
+    /**
+     * 内容
+     */
+    @TableField(value = "content")
+    @Schema(description="内容")
+    private String content;
+
+    /**
+     * 是否答案 1-是 2-否
+     */
+    @TableField(value = "answer_flag")
+    @Schema(description="是否答案 1-是 2-否")
+    private Integer answerFlag;
+
+    /**
+     * 排序
+     */
+    @TableField(value = "sort")
+    @Schema(description="排序")
+    private Integer sort;
+
+    /**
+     * 创建者
+     */
+    @TableField(value = "create_by")
+    @Schema(description="创建者")
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time")
+    @Schema(description="创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    @TableField(value = "update_by")
+    @Schema(description="更新者")
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time")
+    @Schema(description="更新时间")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除标志
+     */
+    @TableField(value = "del_flag")
+    @Schema(description="删除标志")
+    private Integer delFlag;
+}

+ 113 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TTrain.java

@@ -0,0 +1,113 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 培训计划
+ */
+@Schema(description="培训计划")
+@Data
+@TableName(value = "t_train")
+public class TTrain {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 企业id
+     */
+    @TableField(value = "ent_id")
+    @Schema(description="企业id")
+    private Long entId;
+
+    /**
+     * 培训主题
+     */
+    @TableField(value = "`name`")
+    @Schema(description="培训主题")
+    private String name;
+
+    /**
+     * 开始时间
+     */
+    @TableField(value = "start_date")
+    @Schema(description="开始时间")
+    private LocalDate startDate;
+
+    /**
+     * 结束时间
+     */
+    @TableField(value = "end_date")
+    @Schema(description="结束时间")
+    private LocalDate endDate;
+
+    /**
+     * 需要考试 0-否 1-是
+     */
+    @TableField(value = "exam_flag")
+    @Schema(description="需要考试 0-否 1-是")
+    private Integer examFlag;
+
+    /**
+     * 描述
+     */
+    @TableField(value = "description")
+    @Schema(description="描述")
+    private String description;
+
+    /**
+     * 状态 1-未开始 2-进行中 3-已完成
+     */
+    @TableField(value = "`status`")
+    @Schema(description="状态 1-未开始 2-进行中 3-已完成")
+    private Integer status;
+
+    /**
+     * 创建者
+     */
+    @TableField(value = "create_by")
+    @Schema(description="创建者")
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time")
+    @Schema(description="创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    @TableField(value = "update_by")
+    @Schema(description="更新者")
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time")
+    @Schema(description="更新时间")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除标志
+     */
+    @TableField(value = "del_flag")
+    @Schema(description="删除标志")
+    private Integer delFlag;
+}

+ 41 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TTrainCourse.java

@@ -0,0 +1,41 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 培训课程
+ */
+@Schema(description="培训课程")
+@Data
+@TableName(value = "t_train_course")
+public class TTrainCourse {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 培训计划id
+     */
+    @TableField(value = "train_id")
+    @Schema(description="培训计划id")
+    private Long trainId;
+
+    /**
+     * 课程id
+     */
+    @TableField(value = "course_id")
+    @Schema(description="课程id")
+    private Long courseId;
+}

+ 41 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TTrainExam.java

@@ -0,0 +1,41 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 培训考试
+ */
+@Schema(description="培训考试")
+@Data
+@TableName(value = "t_train_exam")
+public class TTrainExam {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 培训计划id
+     */
+    @TableField(value = "train_id")
+    @Schema(description="培训计划id")
+    private Long trainId;
+
+    /**
+     * 考试id
+     */
+    @TableField(value = "exam_id")
+    @Schema(description="考试id")
+    private Long examId;
+}

+ 112 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/TTrainRecord.java

@@ -0,0 +1,112 @@
+package com.sckw.system.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+import lombok.Data;
+
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+/**
+ * 培训记录
+ */
+@Schema(description="培训记录")
+@Data
+@TableName(value = "t_train_record")
+public class TTrainRecord {
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    @Schema(description="id")
+    private Long id;
+
+    /**
+     * 企业id
+     */
+    @TableField(value = "ent_id")
+    @Schema(description="企业id")
+    private Long entId;
+
+    /**
+     * 培训计划id
+     */
+    @TableField(value = "train_id")
+    @Schema(description="培训计划id")
+    private Long trainId;
+
+    /**
+     * 司机id
+     */
+    @TableField(value = "driver_id")
+    @Schema(description="司机id")
+    private Long driverId;
+
+    /**
+     * 完成课程
+     */
+    @TableField(value = "finish_course")
+    @Schema(description="完成课程")
+    private Integer finishCourse;
+
+    /**
+     * 总课程
+     */
+    @TableField(value = "sum_course")
+    @Schema(description="总课程")
+    private Integer sumCourse;
+
+    /**
+     * 正确率
+     */
+    @TableField(value = "right_rate")
+    @Schema(description="正确率")
+    private Integer rightRate;
+
+    /**
+     * 状态 1-未开始 2-进行中 3-已完成
+     */
+    @TableField(value = "`status`")
+    @Schema(description="状态 1-未开始 2-进行中 3-已完成")
+    private Integer status;
+
+    /**
+     * 创建者
+     */
+    @TableField(value = "create_by")
+    @Schema(description="创建者")
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time")
+    @Schema(description="创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    @TableField(value = "update_by")
+    @Schema(description="更新者")
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time")
+    @Schema(description="更新时间")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除标志
+     */
+    @TableField(value = "del_flag")
+    @Schema(description="删除标志")
+    private Integer delFlag;
+}

+ 28 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/pojo/CourseStatusEnum.java

@@ -0,0 +1,28 @@
+package com.sckw.system.model.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+@AllArgsConstructor
+@Getter
+public enum CourseStatusEnum {
+
+    ENABLE(0, "启用"),
+    DISABLE(1, "停用");
+
+    private final Integer code;
+    private final String msg;
+
+    public static String getNameByCode(Integer code) {
+        CourseStatusEnum[] enums = CourseStatusEnum.values();
+        for (CourseStatusEnum instance : enums) {
+            if (Objects.equals(instance.getCode(), code)) {
+                return instance.getMsg();
+            }
+        }
+        return null;
+    }
+}
+

+ 28 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/pojo/CourseTypeEnum.java

@@ -0,0 +1,28 @@
+package com.sckw.system.model.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+@AllArgsConstructor
+@Getter
+public enum CourseTypeEnum {
+
+    IMAGE_TEXT(1, "图文课程"),
+    VIDEO(2, "视频课程");
+
+    private final Integer code;
+    private final String msg;
+
+    public static String getNameByCode(Integer code) {
+        CourseTypeEnum[] enums = CourseTypeEnum.values();
+        for (CourseTypeEnum instance : enums) {
+            if (Objects.equals(instance.getCode(), code)) {
+                return instance.getMsg();
+            }
+        }
+        return null;
+    }
+}
+

+ 28 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/pojo/ExamStatusEnum.java

@@ -0,0 +1,28 @@
+package com.sckw.system.model.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+@AllArgsConstructor
+@Getter
+public enum ExamStatusEnum {
+
+    ON_SHELF(1, "上架"),
+    OFF_SHELF(2, "下架");
+
+    private final Integer code;
+    private final String msg;
+
+    public static String getNameByCode(Integer code) {
+        ExamStatusEnum[] enums = ExamStatusEnum.values();
+        for (ExamStatusEnum instance : enums) {
+            if (Objects.equals(instance.getCode(), code)) {
+                return instance.getMsg();
+            }
+        }
+        return null;
+    }
+}
+

+ 29 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/pojo/QuestionTypeEnum.java

@@ -0,0 +1,29 @@
+package com.sckw.system.model.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+@AllArgsConstructor
+@Getter
+public enum QuestionTypeEnum {
+
+    SINGLE(1, "单选"),
+    MULTIPLE(2, "多选"),
+    TEXT(3, "问答");
+
+    private final Integer code;
+    private final String msg;
+
+    public static String getNameByCode(Integer code) {
+        QuestionTypeEnum[] enums = QuestionTypeEnum.values();
+        for (QuestionTypeEnum instance : enums) {
+            if (Objects.equals(instance.getCode(), code)) {
+                return instance.getMsg();
+            }
+        }
+        return null;
+    }
+}
+

+ 29 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/pojo/TrainStatusEnum.java

@@ -0,0 +1,29 @@
+package com.sckw.system.model.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+@AllArgsConstructor
+@Getter
+public enum TrainStatusEnum {
+
+    NOT_START(1, "未开始"),
+    IN_PROGRESS(2, "进行中"),
+    FINISHED(3, "已结束");
+
+    private final Integer code;
+    private final String msg;
+
+    public static String getNameByCode(Integer code) {
+        TrainStatusEnum[] enums = TrainStatusEnum.values();
+        for (TrainStatusEnum instance : enums) {
+            if (Objects.equals(instance.getCode(), code)) {
+                return instance.getMsg();
+            }
+        }
+        return null;
+    }
+}
+

+ 31 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/CourseManagePageReqVo.java

@@ -0,0 +1,31 @@
+package com.sckw.system.model.vo.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "PC培训课程分页查询请求")
+public class CourseManagePageReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "课程名称(支持模糊)", example = "安全生产")
+    private String courseName;
+
+    @Schema(description = "课程类型 1-图文课程 2-视频课程", example = "1")
+    private Integer courseType;
+
+    @Schema(description = "状态:0-启用 1-停用;不传/传-1表示全部", example = "0")
+    private Integer status = -1;
+
+    @Schema(description = "当前页码", example = "1")
+    private Integer pageNum = 1;
+
+    @Schema(description = "每页数量", example = "10")
+    private Integer pageSize = 10;
+}
+

+ 40 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/CourseSaveReqVo.java

@@ -0,0 +1,40 @@
+package com.sckw.system.model.vo.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "PC培训课程新增/编辑请求")
+public class CourseSaveReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "课程ID(编辑必传)", example = "1")
+    private Long id;
+
+    @Schema(description = "课程名称", example = "安全生产规章制度")
+    private String courseName;
+
+    @Schema(description = "课程类型 1-图文课程 2-视频课程", example = "1")
+    private Integer courseType;
+
+    @Schema(description = "课程时长(分钟)", example = "30")
+    private Integer courseTime;
+
+    @Schema(description = "图文课程内容", example = "## 标题\\n内容...")
+    private String content;
+
+    @Schema(description = "视频文件地址", example = "http://xx/xx.mp4")
+    private String fileUrl;
+
+    @Schema(description = "课程描述", example = "本课程旨在...")
+    private String description;
+
+    @Schema(description = "状态:0-启用 1-停用", example = "0")
+    private Integer status;
+}
+

+ 22 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/CourseStatusReqVo.java

@@ -0,0 +1,22 @@
+package com.sckw.system.model.vo.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "PC培训课程状态更新请求")
+public class CourseStatusReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "课程ID", example = "1")
+    private Long id;
+
+    @Schema(description = "状态:0-启用 1-停用", example = "0")
+    private Integer status;
+}
+

+ 28 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/ExamManagePageReqVo.java

@@ -0,0 +1,28 @@
+package com.sckw.system.model.vo.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "PC考试管理分页查询请求")
+public class ExamManagePageReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "考试名称(支持模糊)", example = "安全知识考试")
+    private String name;
+
+    @Schema(description = "状态 1-上架 2-下架;不传/传0表示全部", example = "1")
+    private Integer status = 0;
+
+    @Schema(description = "当前页码", example = "1")
+    private Integer pageNum = 1;
+
+    @Schema(description = "每页数量", example = "10")
+    private Integer pageSize = 10;
+}
+

+ 81 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/ExamSaveReqVo.java

@@ -0,0 +1,81 @@
+package com.sckw.system.model.vo.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@Schema(description = "PC考试新增/编辑请求")
+public class ExamSaveReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "考试ID(编辑必传)", example = "1")
+    private Long id;
+
+    @Schema(description = "考试名称", example = "安全知识考试")
+    private String name;
+
+    @Schema(description = "考试时长(分钟)", example = "60")
+    private Integer examDuration;
+
+    @Schema(description = "及格率(0-100)", example = "60")
+    private Integer passRate;
+
+    @Schema(description = "描述")
+    private String description;
+
+    @Schema(description = "状态 1-上架 2-下架", example = "1")
+    private Integer status;
+
+    @Schema(description = "题目列表")
+    private List<Question> questions;
+
+    @Data
+    @Schema(description = "题目")
+    public static class Question implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "题目ID(编辑回显用,可不传)", example = "1")
+        private Long id;
+
+        @Schema(description = "题目名称", example = "进入作业现场必须佩戴什么?")
+        private String name;
+
+        @Schema(description = "类型 1-单选 2-多选 3-问答", example = "1")
+        private Integer type;
+
+        @Schema(description = "选项列表(问答题可为空)")
+        private List<QuestionItem> items;
+    }
+
+    @Data
+    @Schema(description = "题目选项")
+    public static class QuestionItem implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "选项ID(编辑回显用,可不传)", example = "1")
+        private Long id;
+
+        @Schema(description = "选项名称 A/B/C/D", example = "A")
+        private String name;
+
+        @Schema(description = "选项内容", example = "安全帽")
+        private String content;
+
+        @Schema(description = "是否答案 1-是 2-否", example = "2")
+        private Integer answerFlag;
+
+        @Schema(description = "排序", example = "0")
+        private Integer sort;
+    }
+}
+

+ 22 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/ExamStatusReqVo.java

@@ -0,0 +1,22 @@
+package com.sckw.system.model.vo.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "PC考试状态更新请求")
+public class ExamStatusReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "考试ID", example = "1")
+    private Long id;
+
+    @Schema(description = "状态 1-上架 2-下架", example = "2")
+    private Integer status;
+}
+

+ 42 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/TrainAppExamSubmitReqVo.java

@@ -0,0 +1,42 @@
+package com.sckw.system.model.vo.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@Schema(description = "App提交考试请求")
+public class TrainAppExamSubmitReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @NotNull(message = "logId不能为空")
+    @Schema(description = "考试日志ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long logId;
+
+    @NotEmpty(message = "answers不能为空")
+    @Schema(description = "答题列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<AnswerItem> answers;
+
+    @Data
+    @Schema(description = "单题作答")
+    public static class AnswerItem implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @NotNull(message = "questionId不能为空")
+        @Schema(description = "题目ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Long questionId;
+
+        @Schema(description = "用户选择的答案,单选: A,多选: A,B", example = "A")
+        private String answer;
+    }
+}
+

+ 27 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/TrainAppListReqVo.java

@@ -0,0 +1,27 @@
+package com.sckw.system.model.vo.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "App培训任务列表查询请求")
+public class TrainAppListReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "任务状态筛选:0-全部 1-未开始 2-进行中 3-已结束", example = "0")
+    @NotNull(message = "status不能为空")
+    private Integer status = 0;
+
+    @Schema(description = "当前页码", example = "1")
+    private Integer pageNum = 1;
+
+    @Schema(description = "每页数量", example = "10")
+    private Integer pageSize = 10;
+}
+

+ 25 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/TrainCourseStudyReqVo.java

@@ -0,0 +1,25 @@
+package com.sckw.system.model.vo.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "App课程学习请求")
+public class TrainCourseStudyReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @NotNull(message = "trainId不能为空")
+    @Schema(description = "培训计划ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long trainId;
+
+    @NotNull(message = "courseId不能为空")
+    @Schema(description = "课程ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Long courseId;
+}
+

+ 28 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/TrainManagePageReqVo.java

@@ -0,0 +1,28 @@
+package com.sckw.system.model.vo.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "PC培训管理分页查询请求")
+public class TrainManagePageReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "培训主题(支持模糊)", example = "安全培训")
+    private String name;
+
+    @Schema(description = "状态 1-未开始 2-进行中 3-已结束;不传/传0表示全部", example = "2")
+    private Integer status = 0;
+
+    @Schema(description = "当前页码", example = "1")
+    private Integer pageNum = 1;
+
+    @Schema(description = "每页数量", example = "10")
+    private Integer pageSize = 10;
+}
+

+ 51 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/req/TrainSaveReqVo.java

@@ -0,0 +1,51 @@
+package com.sckw.system.model.vo.req;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.util.List;
+
+@Data
+@Schema(description = "PC培训计划新增/编辑请求")
+public class TrainSaveReqVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "培训ID(编辑必传)", example = "1")
+    private Long id;
+
+    @Schema(description = "培训主题", example = "2026年安全培训")
+    private String name;
+
+    @Schema(description = "开始日期", example = "2026-06-01")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate startDate;
+
+    @Schema(description = "结束日期", example = "2026-06-30")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate endDate;
+
+    @Schema(description = "需要考试 0-否 1-是", example = "1")
+    private Integer examFlag = 0;
+
+    @Schema(description = "考试ID(examFlag=1时必传)", example = "1")
+    private Long examId;
+
+    @Schema(description = "培训描述")
+    private String description;
+
+    @Schema(description = "课程ID列表", example = "[1,2]")
+    private List<Long> courseIds;
+
+    @Schema(description = "司机ID列表", example = "[1001,1002]")
+    private List<Long> driverIds;
+}
+

+ 56 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/CourseDetailResVo.java

@@ -0,0 +1,56 @@
+package com.sckw.system.model.vo.res;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@Schema(description = "PC培训课程详情")
+public class CourseDetailResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "课程ID", example = "1")
+    private Long id;
+
+    @Schema(description = "课程名称", example = "安全生产规章制度")
+    private String courseName;
+
+    @Schema(description = "课程类型 1-图文课程 2-视频课程", example = "1")
+    private Integer courseType;
+
+    @Schema(description = "课程类型名称", example = "图文课程")
+    private String courseTypeName;
+
+    @Schema(description = "课程时长(分钟)", example = "30")
+    private Integer courseTime;
+
+    @Schema(description = "图文内容")
+    private String content;
+
+    @Schema(description = "视频文件地址")
+    private String fileUrl;
+
+    @Schema(description = "课程描述")
+    private String description;
+
+    @Schema(description = "状态:0-启用 1-停用", example = "0")
+    private Integer status;
+
+    @Schema(description = "状态名称", example = "启用")
+    private String statusName;
+
+    @Schema(description = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @Schema(description = "更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+}
+

+ 46 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/CourseManageItemResVo.java

@@ -0,0 +1,46 @@
+package com.sckw.system.model.vo.res;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@Schema(description = "PC培训课程列表项")
+public class CourseManageItemResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "课程ID", example = "1")
+    private Long id;
+
+    @Schema(description = "课程名称", example = "安全生产规章制度")
+    private String courseName;
+
+    @Schema(description = "课程类型 1-图文课程 2-视频课程", example = "1")
+    private Integer courseType;
+
+    @Schema(description = "课程类型名称", example = "图文课程")
+    private String courseTypeName;
+
+    @Schema(description = "课程时长(分钟)", example = "30")
+    private Integer courseTime;
+
+    @Schema(description = "课程描述")
+    private String description;
+
+    @Schema(description = "状态:0-启用 1-停用", example = "0")
+    private Integer status;
+
+    @Schema(description = "状态名称", example = "启用")
+    private String statusName;
+
+    @Schema(description = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+}
+

+ 100 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/ExamDetailResVo.java

@@ -0,0 +1,100 @@
+package com.sckw.system.model.vo.res;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Schema(description = "PC考试详情")
+public class ExamDetailResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "考试ID", example = "1")
+    private Long id;
+
+    @Schema(description = "考试名称", example = "安全知识考试")
+    private String name;
+
+    @Schema(description = "考试时长(分钟)", example = "60")
+    private Integer examDuration;
+
+    @Schema(description = "及格率(0-100)", example = "60")
+    private Integer passRate;
+
+    @Schema(description = "题目数量", example = "20")
+    private Integer amount;
+
+    @Schema(description = "描述")
+    private String description;
+
+    @Schema(description = "状态 1-上架 2-下架", example = "1")
+    private Integer status;
+
+    @Schema(description = "状态名称", example = "上架")
+    private String statusName;
+
+    @Schema(description = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @Schema(description = "更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+    @Schema(description = "题目列表")
+    private List<Question> questions;
+
+    @Data
+    @Schema(description = "题目")
+    public static class Question implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "题目ID", example = "1")
+        private Long id;
+
+        @Schema(description = "题目名称")
+        private String name;
+
+        @Schema(description = "类型 1-单选 2-多选 3-问答", example = "1")
+        private Integer type;
+
+        @Schema(description = "类型名称", example = "单选")
+        private String typeName;
+
+        @Schema(description = "选项列表")
+        private List<QuestionItem> items;
+    }
+
+    @Data
+    @Schema(description = "题目选项")
+    public static class QuestionItem implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "选项ID", example = "1")
+        private Long id;
+
+        @Schema(description = "选项名称 A/B/C/D", example = "A")
+        private String name;
+
+        @Schema(description = "选项内容", example = "安全帽")
+        private String content;
+
+        @Schema(description = "是否答案 1-是 2-否", example = "2")
+        private Integer answerFlag;
+
+        @Schema(description = "排序", example = "0")
+        private Integer sort;
+    }
+}
+

+ 43 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/ExamManageItemResVo.java

@@ -0,0 +1,43 @@
+package com.sckw.system.model.vo.res;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@Schema(description = "PC考试管理列表项")
+public class ExamManageItemResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "考试ID", example = "1")
+    private Long id;
+
+    @Schema(description = "考试名称", example = "安全知识考试")
+    private String name;
+
+    @Schema(description = "题目数量", example = "20")
+    private Integer amount;
+
+    @Schema(description = "考试时长(分钟)", example = "60")
+    private Integer examDuration;
+
+    @Schema(description = "及格率(0-100)", example = "60")
+    private Integer passRate;
+
+    @Schema(description = "状态 1-上架 2-下架", example = "1")
+    private Integer status;
+
+    @Schema(description = "状态名称", example = "上架")
+    private String statusName;
+
+    @Schema(description = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+}
+

+ 80 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppDetailResVo.java

@@ -0,0 +1,80 @@
+package com.sckw.system.model.vo.res;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.util.List;
+
+@Data
+@Schema(description = "App培训任务详情响应")
+public class TrainAppDetailResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "培训计划ID", example = "1")
+    private Long trainId;
+
+    @Schema(description = "培训主题", example = "2026年安全培训")
+    private String trainName;
+
+    @Schema(description = "开始日期", example = "2026-06-01")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate startDate;
+
+    @Schema(description = "结束日期", example = "2026-06-30")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate endDate;
+
+    @Schema(description = "任务状态 1-未开始 2-进行中 3-已结束", example = "2")
+    private Integer status;
+
+    @Schema(description = "任务状态名称", example = "进行中")
+    private String statusName;
+
+    @Schema(description = "已学课程数", example = "1")
+    private Integer learnedCourse;
+
+    @Schema(description = "总课程数", example = "2")
+    private Integer sumCourse;
+
+    @Schema(description = "课程列表")
+    private List<TrainAppTaskCourseResVo> courses;
+
+    @Schema(description = "考试信息")
+    private Exam exam;
+
+    @Schema(description = "是否显示去考试按钮", example = "true")
+    private Boolean showExam;
+
+    @Schema(description = "考试是否已通过", example = "false")
+    private Boolean examPassed;
+
+    @Data
+    @Schema(description = "考试信息")
+    public static class Exam implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "考试ID", example = "1")
+        private Long examId;
+
+        @Schema(description = "考试名称")
+        private String name;
+
+        @Schema(description = "考试时长(分钟)", example = "60")
+        private Integer examDuration;
+
+        @Schema(description = "及格率(0-100)", example = "60")
+        private Integer passRate;
+
+        @Schema(description = "题目数量", example = "20")
+        private Integer amount;
+    }
+}
+

+ 75 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppExamPaperResVo.java

@@ -0,0 +1,75 @@
+package com.sckw.system.model.vo.res;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@Schema(description = "App考试试卷响应")
+public class TrainAppExamPaperResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "考试日志ID", example = "1")
+    private Long logId;
+
+    @Schema(description = "考试ID", example = "1")
+    private Long examId;
+
+    @Schema(description = "考试名称")
+    private String examName;
+
+    @Schema(description = "考试时长(分钟)", example = "60")
+    private Integer examDuration;
+
+    @Schema(description = "及格率(0-100)", example = "60")
+    private Integer passRate;
+
+    @Schema(description = "题目数量", example = "20")
+    private Integer amount;
+
+    @Schema(description = "题目列表")
+    private List<Question> questions;
+
+    @Data
+    @Schema(description = "题目")
+    public static class Question implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "题目ID", example = "1")
+        private Long questionId;
+
+        @Schema(description = "题目名称")
+        private String name;
+
+        @Schema(description = "类型 1-单选 2-多选 3-问答", example = "1")
+        private Integer type;
+
+        @Schema(description = "类型名称", example = "单选")
+        private String typeName;
+
+        @Schema(description = "选项列表")
+        private List<Option> options;
+    }
+
+    @Data
+    @Schema(description = "选项")
+    public static class Option implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "选项名称 A/B/C/D", example = "A")
+        private String name;
+
+        @Schema(description = "选项内容", example = "安全帽")
+        private String content;
+    }
+}
+

+ 40 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppExamStartResVo.java

@@ -0,0 +1,40 @@
+package com.sckw.system.model.vo.res;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@Schema(description = "App开始考试响应")
+public class TrainAppExamStartResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "考试日志ID", example = "1")
+    private Long logId;
+
+    @Schema(description = "考试ID", example = "1")
+    private Long examId;
+
+    @Schema(description = "考试名称")
+    private String examName;
+
+    @Schema(description = "考试时长(分钟)", example = "60")
+    private Integer examDuration;
+
+    @Schema(description = "及格率(0-100)", example = "60")
+    private Integer passRate;
+
+    @Schema(description = "题目数量", example = "20")
+    private Integer amount;
+
+    @Schema(description = "开始时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime startTime;
+}
+

+ 28 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppExamSubmitResVo.java

@@ -0,0 +1,28 @@
+package com.sckw.system.model.vo.res;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "App提交考试响应")
+public class TrainAppExamSubmitResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "正确题目数", example = "17")
+    private Integer rightAmount;
+
+    @Schema(description = "总题目数", example = "20")
+    private Integer amount;
+
+    @Schema(description = "正确率(0-100)", example = "85")
+    private Integer rightRate;
+
+    @Schema(description = "是否通过", example = "true")
+    private Boolean passed;
+}
+

+ 26 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppListResVo.java

@@ -0,0 +1,26 @@
+package com.sckw.system.model.vo.res;
+
+import com.sckw.core.web.response.result.PageDataResult;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "App培训任务列表响应")
+public class TrainAppListResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "整体学习进度(0-100)", example = "50")
+    private Integer progressRate;
+
+    @Schema(description = "任务总数", example = "2")
+    private Integer totalTask;
+
+    @Schema(description = "分页任务列表")
+    private PageDataResult<TrainAppTaskResVo> page;
+}
+

+ 31 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppTaskCourseResVo.java

@@ -0,0 +1,31 @@
+package com.sckw.system.model.vo.res;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Schema(description = "App培训任务内课程项")
+public class TrainAppTaskCourseResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "课程ID", example = "1")
+    private Long courseId;
+
+    @Schema(description = "课程名称")
+    private String courseName;
+
+    @Schema(description = "课程类型 1-图文课程 2-视频课程", example = "1")
+    private Integer courseType;
+
+    @Schema(description = "课程类型名称", example = "图文课程")
+    private String courseTypeName;
+
+    @Schema(description = "课程学习状态 0-待学习 1-已学习", example = "0")
+    private Integer learnStatus;
+}
+

+ 51 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainAppTaskResVo.java

@@ -0,0 +1,51 @@
+package com.sckw.system.model.vo.res;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.util.List;
+
+@Data
+@Schema(description = "App培训任务列表项")
+public class TrainAppTaskResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "培训计划ID", example = "1")
+    private Long trainId;
+
+    @Schema(description = "培训主题", example = "2026年安全培训")
+    private String trainName;
+
+    @Schema(description = "开始日期", example = "2026-06-01")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate startDate;
+
+    @Schema(description = "结束日期", example = "2026-06-30")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate endDate;
+
+    @Schema(description = "任务状态 1-未开始 2-进行中 3-已结束", example = "2")
+    private Integer status;
+
+    @Schema(description = "任务状态名称", example = "进行中")
+    private String statusName;
+
+    @Schema(description = "已学课程数", example = "1")
+    private Integer learnedCourse;
+
+    @Schema(description = "总课程数", example = "2")
+    private Integer sumCourse;
+
+    @Schema(description = "课程列表")
+    private List<TrainAppTaskCourseResVo> courses;
+
+    @Schema(description = "是否显示去考试按钮", example = "true")
+    private Boolean showExam;
+}
+

+ 162 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainDetailResVo.java

@@ -0,0 +1,162 @@
+package com.sckw.system.model.vo.res;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Schema(description = "PC培训详情")
+public class TrainDetailResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "培训ID", example = "1")
+    private Long id;
+
+    @Schema(description = "培训主题", example = "2026年安全培训")
+    private String name;
+
+    @Schema(description = "开始日期", example = "2026-06-01")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate startDate;
+
+    @Schema(description = "结束日期", example = "2026-06-30")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate endDate;
+
+    @Schema(description = "需要考试 0-否 1-是", example = "1")
+    private Integer examFlag;
+
+    @Schema(description = "培训描述")
+    private String description;
+
+    @Schema(description = "状态 1-未开始 2-进行中 3-已结束", example = "2")
+    private Integer status;
+
+    @Schema(description = "状态名称", example = "进行中")
+    private String statusName;
+
+    @Schema(description = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @Schema(description = "更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+    @Schema(description = "课程列表")
+    private List<Course> courses;
+
+    @Schema(description = "考试信息")
+    private Exam exam;
+
+
+    @Schema(description = "培训进度汇总")
+    private ProgressSummary progressSummary;
+
+    @Schema(description = "司机培训进度列表")
+    private List<DriverProgress> driverProgressList;
+
+    @Data
+    @Schema(description = "课程信息")
+    public static class Course implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "课程ID", example = "1")
+        private Long id;
+
+        @Schema(description = "课程名称")
+        private String courseName;
+
+        @Schema(description = "课程类型 1-图文课程 2-视频课程", example = "1")
+        private Integer courseType;
+
+        @Schema(description = "课程类型名称", example = "图文课程")
+        private String courseTypeName;
+
+        @Schema(description = "课程时长(分钟)", example = "30")
+        private Integer courseTime;
+    }
+
+    @Data
+    @Schema(description = "考试信息")
+    public static class Exam implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "考试ID", example = "1")
+        private Long id;
+
+        @Schema(description = "考试名称")
+        private String name;
+
+        @Schema(description = "考试时长(分钟)", example = "60")
+        private Integer examDuration;
+
+        @Schema(description = "及格率(0-100)", example = "60")
+        private Integer passRate;
+
+        @Schema(description = "题目数量", example = "20")
+        private Integer amount;
+    }
+
+    @Data
+    @Schema(description = "培训进度汇总")
+    public static class ProgressSummary implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "总人数", example = "10")
+        private Integer total;
+
+        @Schema(description = "已完成", example = "6")
+        private Integer finished;
+
+        @Schema(description = "进行中", example = "3")
+        private Integer inProgress;
+
+        @Schema(description = "未开始", example = "1")
+        private Integer notStart;
+
+        @Schema(description = "完成率(0-100)", example = "60")
+        private Integer progressRate;
+    }
+
+    @Data
+    @Schema(description = "司机培训进度")
+    public static class DriverProgress implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "司机ID", example = "1001")
+        private Long driverId;
+
+        @Schema(description = "完成课程数", example = "2")
+        private Integer finishCourse;
+
+        @Schema(description = "总课程数", example = "2")
+        private Integer sumCourse;
+
+        @Schema(description = "考试正确率(0-100)", example = "85")
+        private Integer rightRate;
+
+        @Schema(description = "任务状态 1-未开始 2-进行中 3-已结束", example = "3")
+        private Integer status;
+
+        @Schema(description = "任务状态名称", example = "已结束")
+        private String statusName;
+    }
+}
+

+ 46 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/model/vo/res/TrainManageItemResVo.java

@@ -0,0 +1,46 @@
+package com.sckw.system.model.vo.res;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Data
+@Schema(description = "PC培训管理列表项")
+public class TrainManageItemResVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "培训ID", example = "1")
+    private Long id;
+
+    @Schema(description = "培训主题", example = "2026年安全培训")
+    private String name;
+
+    @Schema(description = "开始日期", example = "2026-06-01")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate startDate;
+
+    @Schema(description = "结束日期", example = "2026-06-30")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate endDate;
+
+    @Schema(description = "涉及司机数量", example = "10")
+    private Integer driverCount;
+
+    @Schema(description = "状态 1-未开始 2-进行中 3-已结束", example = "2")
+    private Integer status;
+
+    @Schema(description = "状态名称", example = "进行中")
+    private String statusName;
+
+    @Schema(description = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+}
+

+ 284 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsCourseService.java

@@ -0,0 +1,284 @@
+package com.sckw.system.service;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.sckw.core.exception.SystemException;
+import com.sckw.core.model.constant.Global;
+import com.sckw.core.web.constant.HttpStatus;
+import com.sckw.core.web.context.LoginUserHolder;
+import com.sckw.core.web.response.result.PageDataResult;
+import com.sckw.system.model.TCourse;
+import com.sckw.system.model.pojo.CourseStatusEnum;
+import com.sckw.system.model.pojo.CourseTypeEnum;
+import com.sckw.system.model.vo.req.CourseManagePageReqVo;
+import com.sckw.system.model.vo.req.CourseSaveReqVo;
+import com.sckw.system.model.vo.req.CourseStatusReqVo;
+import com.sckw.system.model.vo.req.IdReqVo;
+import com.sckw.system.model.vo.res.CourseDetailResVo;
+import com.sckw.system.model.vo.res.CourseManageItemResVo;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class KwsCourseService {
+
+    private final TCourseService tCourseService;
+
+    public List<CourseManageItemResVo> listEnabledCourses() {
+        LambdaQueryWrapper<TCourse> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TCourse::getDelFlag, Global.UN_DELETED)
+                .eq(TCourse::getStatus, 0)
+                .orderByDesc(TCourse::getCreateTime);
+
+
+        List<TCourse> courses = tCourseService.list(wrapper);
+        return courses.stream().map(this::toManageItem).collect(Collectors.toList());
+    }
+
+    public PageDataResult<CourseManageItemResVo> manageCoursePage(CourseManagePageReqVo reqVo) {
+        Page<TCourse> page = new Page<>(reqVo.getPageNum(), reqVo.getPageSize());
+        LambdaQueryWrapper<TCourse> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TCourse::getDelFlag, Global.UN_DELETED)
+                .orderByDesc(TCourse::getCreateTime);
+
+
+        if (StrUtil.isNotBlank(reqVo.getCourseName())) {
+            wrapper.like(TCourse::getCourseName, reqVo.getCourseName().trim());
+        }
+        if (reqVo.getCourseType() != null) {
+            wrapper.eq(TCourse::getCourseType, reqVo.getCourseType());
+        }
+        if (reqVo.getStatus() != null && reqVo.getStatus() >= 0) {
+            wrapper.eq(TCourse::getStatus, reqVo.getStatus());
+        }
+
+        Page<TCourse> coursePage = tCourseService.page(page, wrapper);
+        return PageDataResult.of(page, coursePage.getRecords().stream().map(this::toManageItem).collect(Collectors.toList()));
+    }
+
+    public CourseDetailResVo manageCourseDetail(Long courseId) {
+        TCourse course = tCourseService.getById(courseId);
+        if (course == null || !Objects.equals(course.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "课程不存在");
+        }
+
+        return toDetail(course);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Long addCourse(CourseSaveReqVo reqVo) {
+        validateSaveReq(reqVo, false);
+
+        Long entId = LoginUserHolder.getEntId();
+        if (entId == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "企业信息缺失");
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        TCourse course = new TCourse();
+        course.setEntId(entId);
+        course.setCourseName(reqVo.getCourseName().trim());
+        course.setCourseType(reqVo.getCourseType());
+        course.setCourseTime(reqVo.getCourseTime());
+        course.setContent(normalizeNullable(reqVo.getContent()));
+        course.setFileUrl(normalizeNullable(reqVo.getFileUrl()));
+        course.setDescription(normalizeNullable(reqVo.getDescription()));
+        course.setStatus(reqVo.getStatus() == null ? 1 : reqVo.getStatus());
+        course.setCreateBy(userId);
+        course.setCreateTime(now);
+        course.setUpdateBy(userId);
+        course.setUpdateTime(now);
+        course.setDelFlag(Global.UN_DELETED);
+
+        if (!tCourseService.save(course)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "新增课程失败");
+        }
+        return course.getId();
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateCourse(CourseSaveReqVo reqVo) {
+        validateSaveReq(reqVo, true);
+
+        TCourse course = tCourseService.getById(reqVo.getId());
+        if (course == null || !Objects.equals(course.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "课程不存在");
+        }
+
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        course.setCourseName(reqVo.getCourseName().trim());
+        course.setCourseType(reqVo.getCourseType());
+        course.setCourseTime(reqVo.getCourseTime());
+        course.setContent(normalizeNullable(reqVo.getContent()));
+        course.setFileUrl(normalizeNullable(reqVo.getFileUrl()));
+        course.setDescription(normalizeNullable(reqVo.getDescription()));
+        if (reqVo.getStatus() != null) {
+            course.setStatus(reqVo.getStatus());
+        }
+        course.setUpdateBy(userId);
+        course.setUpdateTime(now);
+
+        if (!tCourseService.updateById(course)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, HttpStatus.UPDATE_FAIL);
+        }
+        return true;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateCourseStatus(CourseStatusReqVo reqVo) {
+        if (reqVo == null || reqVo.getId() == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, HttpStatus.ID_MISSING);
+        }
+        if (reqVo.getStatus() == null || (reqVo.getStatus() != 0 && reqVo.getStatus() != 1)) {
+            throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "status参数非法");
+        }
+
+        TCourse course = tCourseService.getById(reqVo.getId());
+        if (course == null || !Objects.equals(course.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "课程不存在");
+        }
+
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        boolean updated = tCourseService.lambdaUpdate()
+                .eq(TCourse::getId, reqVo.getId())
+                .set(TCourse::getStatus, reqVo.getStatus())
+                .set(TCourse::getUpdateBy, userId)
+                .set(TCourse::getUpdateTime, now)
+                .update();
+
+        if (!updated) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, HttpStatus.UPDATE_FAIL);
+        }
+        return true;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteCourse(IdReqVo reqVo) {
+        String idsStr = reqVo == null ? null : reqVo.getIds();
+        Long id = reqVo == null ? null : reqVo.getId();
+
+        List<Long> ids = parseIds(idsStr, id);
+        if (ids.isEmpty()) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "ids不能为空");
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        boolean updated = tCourseService.lambdaUpdate()
+                .in(TCourse::getId, ids)
+                .eq(TCourse::getDelFlag, Global.UN_DELETED)
+                .set(TCourse::getDelFlag, Global.DELETED)
+                .set(TCourse::getUpdateBy, userId)
+                .set(TCourse::getUpdateTime, now)
+                .update();
+
+        if (!updated) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, HttpStatus.DELETE_FAIL);
+        }
+        return true;
+    }
+
+    private CourseManageItemResVo toManageItem(TCourse course) {
+        CourseManageItemResVo vo = new CourseManageItemResVo();
+        vo.setId(course.getId());
+        vo.setCourseName(course.getCourseName());
+        vo.setCourseType(course.getCourseType());
+        vo.setCourseTypeName(CourseTypeEnum.getNameByCode(course.getCourseType()));
+        vo.setCourseTime(course.getCourseTime());
+        vo.setDescription(course.getDescription());
+        vo.setStatus(course.getStatus());
+        vo.setStatusName(CourseStatusEnum.getNameByCode(course.getStatus()));
+        vo.setCreateTime(course.getCreateTime());
+        return vo;
+    }
+
+    private CourseDetailResVo toDetail(TCourse course) {
+        CourseDetailResVo vo = new CourseDetailResVo();
+        vo.setId(course.getId());
+        vo.setCourseName(course.getCourseName());
+        vo.setCourseType(course.getCourseType());
+        vo.setCourseTypeName(CourseTypeEnum.getNameByCode(course.getCourseType()));
+        vo.setCourseTime(course.getCourseTime());
+        vo.setContent(course.getContent());
+        vo.setFileUrl(course.getFileUrl());
+        vo.setDescription(course.getDescription());
+        vo.setStatus(course.getStatus());
+        vo.setStatusName(CourseStatusEnum.getNameByCode(course.getStatus()));
+        vo.setCreateTime(course.getCreateTime());
+        vo.setUpdateTime(course.getUpdateTime());
+        return vo;
+    }
+
+    private static String normalizeNullable(String value) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+
+    private static List<Long> parseIds(String idsStr, Long singleId) {
+        if (singleId != null) {
+            return List.of(singleId);
+        }
+        if (StrUtil.isBlank(idsStr)) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(idsStr.split(","))
+                .map(String::trim)
+                .filter(StrUtil::isNotBlank)
+                .map(Long::parseLong)
+                .distinct()
+                .toList();
+    }
+
+    private static void validateSaveReq(CourseSaveReqVo reqVo, boolean requireId) {
+        if (reqVo == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "参数不能为空");
+        }
+        if (requireId && reqVo.getId() == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, HttpStatus.ID_MISSING);
+        }
+        if (StrUtil.isBlank(reqVo.getCourseName())) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "课程名称不能为空");
+        }
+        if (reqVo.getCourseType() == null || (reqVo.getCourseType() != 1 && reqVo.getCourseType() != 2)) {
+            throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "课程类型非法");
+        }
+        if (reqVo.getCourseTime() == null || reqVo.getCourseTime() <= 0) {
+            throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "课程时长非法");
+        }
+        if (reqVo.getCourseType() == 1) {
+            if (StrUtil.isBlank(reqVo.getContent())) {
+                throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "图文课程内容不能为空");
+            }
+        } else if (Objects.equals(reqVo.getCourseType(), 2)) {
+            if (StrUtil.isBlank(reqVo.getFileUrl())) {
+                throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "视频课程文件不能为空");
+            }
+        }
+        if (reqVo.getStatus() != null && reqVo.getStatus() != 0 && reqVo.getStatus() != 1) {
+            throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "status参数非法");
+        }
+    }
+
+}

+ 516 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsExamService.java

@@ -0,0 +1,516 @@
+package com.sckw.system.service;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.sckw.core.exception.SystemException;
+import com.sckw.core.model.constant.Global;
+import com.sckw.core.web.constant.HttpStatus;
+import com.sckw.core.web.context.LoginUserHolder;
+import com.sckw.core.web.response.result.PageDataResult;
+import com.sckw.system.model.TExam;
+import com.sckw.system.model.TQuestion;
+import com.sckw.system.model.TQuestionItem;
+import com.sckw.system.model.pojo.ExamStatusEnum;
+import com.sckw.system.model.pojo.QuestionTypeEnum;
+import com.sckw.system.model.vo.req.ExamManagePageReqVo;
+import com.sckw.system.model.vo.req.ExamSaveReqVo;
+import com.sckw.system.model.vo.req.ExamStatusReqVo;
+import com.sckw.system.model.vo.req.IdReqVo;
+import com.sckw.system.model.vo.res.ExamDetailResVo;
+import com.sckw.system.model.vo.res.ExamManageItemResVo;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+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 KwsExamService {
+
+    private final TExamService tExamService;
+    private final TQuestionService tQuestionService;
+    private final TQuestionItemService tQuestionItemService;
+
+    public List<ExamManageItemResVo> listOnShelfExams() {
+        LambdaQueryWrapper<TExam> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TExam::getDelFlag, Global.UN_DELETED)
+                .eq(TExam::getStatus, 1)
+                .orderByDesc(TExam::getCreateTime);
+
+        Set<Long> entIds = resolveAuthorizedEntIds();
+        if (!entIds.isEmpty()) {
+            wrapper.in(TExam::getEntId, entIds);
+        }
+
+        List<TExam> exams = tExamService.list(wrapper);
+        return exams.stream().map(this::toManageItem).collect(Collectors.toList());
+    }
+
+    public PageDataResult<ExamManageItemResVo> manageExamPage(ExamManagePageReqVo reqVo) {
+        Page<TExam> page = new Page<>(reqVo.getPageNum(), reqVo.getPageSize());
+        LambdaQueryWrapper<TExam> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TExam::getDelFlag, Global.UN_DELETED)
+                .orderByDesc(TExam::getCreateTime);
+
+        Set<Long> entIds = resolveAuthorizedEntIds();
+        if (!entIds.isEmpty()) {
+            wrapper.in(TExam::getEntId, entIds);
+        }
+
+        if (StrUtil.isNotBlank(reqVo.getName())) {
+            wrapper.like(TExam::getName, reqVo.getName().trim());
+        }
+        if (reqVo.getStatus() != null && reqVo.getStatus() > 0) {
+            wrapper.eq(TExam::getStatus, reqVo.getStatus());
+        }
+
+        Page<TExam> examPage = tExamService.page(page, wrapper);
+        return PageDataResult.of(page, examPage.getRecords().stream().map(this::toManageItem).collect(Collectors.toList()));
+    }
+
+    public ExamDetailResVo manageExamDetail(Long examId) {
+        TExam exam = tExamService.getById(examId);
+        if (exam == null || !Objects.equals(exam.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "考试不存在");
+        }
+        if (!isAuthorizedEnt(exam.getEntId())) {
+            throw new SystemException(HttpStatus.AUTHORITY_NO_CODE, HttpStatus.ACCESS_FIAL);
+        }
+
+        List<TQuestion> questions = tQuestionService.lambdaQuery()
+                .eq(TQuestion::getDelFlag, Global.UN_DELETED)
+                .eq(TQuestion::getExamId, examId)
+                .orderByAsc(TQuestion::getId)
+                .list();
+
+        Map<Long, List<TQuestionItem>> itemMap;
+        if (CollUtil.isNotEmpty(questions)) {
+            List<Long> questionIds = questions.stream().map(TQuestion::getId).filter(Objects::nonNull).toList();
+            List<TQuestionItem> items = tQuestionItemService.lambdaQuery()
+                    .eq(TQuestionItem::getDelFlag, Global.UN_DELETED)
+                    .in(TQuestionItem::getQuestionId, questionIds)
+                    .orderByAsc(TQuestionItem::getSort)
+                    .orderByAsc(TQuestionItem::getId)
+                    .list();
+            itemMap = items.stream().collect(Collectors.groupingBy(TQuestionItem::getQuestionId));
+        } else {
+            itemMap = Collections.emptyMap();
+        }
+
+        ExamDetailResVo detail = toDetail(exam);
+        List<ExamDetailResVo.Question> questionVos = questions.stream().map(q -> {
+            ExamDetailResVo.Question vo = new ExamDetailResVo.Question();
+            vo.setId(q.getId());
+            vo.setName(q.getName());
+            vo.setType(q.getType());
+            vo.setTypeName(QuestionTypeEnum.getNameByCode(q.getType()));
+            List<TQuestionItem> items = itemMap.getOrDefault(q.getId(), Collections.emptyList());
+            List<ExamDetailResVo.QuestionItem> itemVos = items.stream().map(this::toQuestionItem).toList();
+            vo.setItems(itemVos);
+            return vo;
+        }).toList();
+
+        detail.setQuestions(questionVos);
+        detail.setAmount(questionVos.size());
+        return detail;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Long addExam(ExamSaveReqVo reqVo) {
+        validateSaveReq(reqVo, false);
+
+        Long entId = LoginUserHolder.getEntId();
+        if (entId == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "企业信息缺失");
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        List<ExamSaveReqVo.Question> questions = reqVo.getQuestions() == null ? Collections.emptyList() : reqVo.getQuestions();
+        int amount = questions.size();
+
+        TExam exam = new TExam();
+        exam.setEntId(entId);
+        exam.setName(reqVo.getName().trim());
+        exam.setExamDuration(reqVo.getExamDuration());
+        exam.setPassRate(reqVo.getPassRate());
+        exam.setAmount(amount);
+        exam.setDescription(normalizeNullable(reqVo.getDescription()));
+        exam.setStatus(reqVo.getStatus() == null ? 1 : reqVo.getStatus());
+        exam.setCreateBy(userId);
+        exam.setCreateTime(now);
+        exam.setUpdateBy(userId);
+        exam.setUpdateTime(now);
+        exam.setDelFlag(Global.UN_DELETED);
+
+        if (!tExamService.save(exam)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "新增考试失败");
+        }
+
+        saveQuestionsAndItems(exam.getId(), entId, userId, now, questions);
+        return exam.getId();
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateExam(ExamSaveReqVo reqVo) {
+        validateSaveReq(reqVo, true);
+
+        TExam exam = tExamService.getById(reqVo.getId());
+        if (exam == null || !Objects.equals(exam.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "考试不存在");
+        }
+        if (!isAuthorizedEnt(exam.getEntId())) {
+            throw new SystemException(HttpStatus.AUTHORITY_NO_CODE, HttpStatus.ACCESS_FIAL);
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        List<ExamSaveReqVo.Question> questions = reqVo.getQuestions() == null ? Collections.emptyList() : reqVo.getQuestions();
+        int amount = questions.size();
+
+        exam.setName(reqVo.getName().trim());
+        exam.setExamDuration(reqVo.getExamDuration());
+        exam.setPassRate(reqVo.getPassRate());
+        exam.setAmount(amount);
+        exam.setDescription(normalizeNullable(reqVo.getDescription()));
+        if (reqVo.getStatus() != null) {
+            exam.setStatus(reqVo.getStatus());
+        }
+        exam.setUpdateBy(userId);
+        exam.setUpdateTime(now);
+
+        if (!tExamService.updateById(exam)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, HttpStatus.UPDATE_FAIL);
+        }
+
+        logicDeleteQuestionsAndItemsByExamId(exam.getId(), userId, now);
+        saveQuestionsAndItems(exam.getId(), exam.getEntId(), userId, now, questions);
+        return true;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateExamStatus(ExamStatusReqVo reqVo) {
+        if (reqVo == null || reqVo.getId() == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, HttpStatus.ID_MISSING);
+        }
+        if (reqVo.getStatus() == null || (reqVo.getStatus() != 1 && reqVo.getStatus() != 2)) {
+            throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "status参数非法");
+        }
+
+        TExam exam = tExamService.getById(reqVo.getId());
+        if (exam == null || !Objects.equals(exam.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "考试不存在");
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        boolean updated = tExamService.lambdaUpdate()
+                .eq(TExam::getId, reqVo.getId())
+                .set(TExam::getStatus, reqVo.getStatus())
+                .set(TExam::getUpdateBy, userId)
+                .set(TExam::getUpdateTime, now)
+                .update();
+
+        if (!updated) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, HttpStatus.UPDATE_FAIL);
+        }
+        return true;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteExam(IdReqVo reqVo) {
+        List<Long> examIds = parseIds(reqVo == null ? null : reqVo.getIds(), reqVo == null ? null : reqVo.getId());
+        if (examIds.isEmpty()) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "ids不能为空");
+        }
+
+        Set<Long> entIds = resolveAuthorizedEntIds();
+        if (!LoginUserHolder.isManager() && entIds.isEmpty()) {
+            throw new SystemException(HttpStatus.AUTHORITY_NO_CODE, HttpStatus.ACCESS_FIAL);
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        boolean updated = tExamService.lambdaUpdate()
+                .in(TExam::getId, examIds)
+                .eq(TExam::getDelFlag, Global.UN_DELETED)
+                .in(!entIds.isEmpty(), TExam::getEntId, entIds)
+                .set(TExam::getDelFlag, Global.DELETED)
+                .set(TExam::getUpdateBy, userId)
+                .set(TExam::getUpdateTime, now)
+                .update();
+
+        if (!updated) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, HttpStatus.DELETE_FAIL);
+        }
+
+        List<Long> questionIds = tQuestionService.lambdaQuery()
+                .select(TQuestion::getId)
+                .eq(TQuestion::getDelFlag, Global.UN_DELETED)
+                .in(TQuestion::getExamId, examIds)
+                .list()
+                .stream()
+                .map(TQuestion::getId)
+                .filter(Objects::nonNull)
+                .toList();
+
+        if (CollUtil.isNotEmpty(questionIds)) {
+            tQuestionItemService.lambdaUpdate()
+                    .in(TQuestionItem::getQuestionId, questionIds)
+                    .eq(TQuestionItem::getDelFlag, Global.UN_DELETED)
+                    .set(TQuestionItem::getDelFlag, Global.DELETED)
+                    .set(TQuestionItem::getUpdateBy, userId)
+                    .set(TQuestionItem::getUpdateTime, now)
+                    .update();
+
+            tQuestionService.lambdaUpdate()
+                    .in(TQuestion::getId, questionIds)
+                    .eq(TQuestion::getDelFlag, Global.UN_DELETED)
+                    .set(TQuestion::getDelFlag, Global.DELETED)
+                    .set(TQuestion::getUpdateBy, userId)
+                    .set(TQuestion::getUpdateTime, now)
+                    .update();
+        }
+
+        return true;
+    }
+
+    private void saveQuestionsAndItems(Long examId, Long entId, Long userId, LocalDateTime now, List<ExamSaveReqVo.Question> questions) {
+        if (CollUtil.isEmpty(questions)) {
+            return;
+        }
+
+        for (ExamSaveReqVo.Question q : questions) {
+            TQuestion question = new TQuestion();
+            question.setEntId(entId);
+            question.setExamId(examId);
+            question.setName(q.getName().trim());
+            question.setType(q.getType());
+            question.setCreateBy(userId);
+            question.setCreateTime(now);
+            question.setUpdateBy(userId);
+            question.setUpdateTime(now);
+            question.setDelFlag(Global.UN_DELETED);
+
+            if (!tQuestionService.save(question)) {
+                throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "保存题目失败");
+            }
+
+            List<ExamSaveReqVo.QuestionItem> items = q.getItems() == null ? Collections.emptyList() : q.getItems();
+            if (CollUtil.isEmpty(items)) {
+                continue;
+            }
+
+            List<TQuestionItem> itemEntities = items.stream().map(item -> {
+                TQuestionItem entity = new TQuestionItem();
+                entity.setEntId(entId);
+                entity.setQuestionId(question.getId());
+                entity.setName(normalizeNullable(item.getName()));
+                entity.setContent(item.getContent().trim());
+                entity.setAnswerFlag(item.getAnswerFlag());
+                entity.setSort(item.getSort() == null ? 0 : item.getSort());
+                entity.setCreateBy(userId);
+                entity.setCreateTime(now);
+                entity.setUpdateBy(userId);
+                entity.setUpdateTime(now);
+                entity.setDelFlag(Global.UN_DELETED);
+                return entity;
+            }).toList();
+
+            if (!tQuestionItemService.saveBatch(itemEntities)) {
+                throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "保存题目选项失败");
+            }
+        }
+    }
+
+    private void logicDeleteQuestionsAndItemsByExamId(Long examId, Long userId, LocalDateTime now) {
+        List<Long> questionIds = tQuestionService.lambdaQuery()
+                .select(TQuestion::getId)
+                .eq(TQuestion::getDelFlag, Global.UN_DELETED)
+                .eq(TQuestion::getExamId, examId)
+                .list()
+                .stream()
+                .map(TQuestion::getId)
+                .filter(Objects::nonNull)
+                .toList();
+
+        if (CollUtil.isEmpty(questionIds)) {
+            return;
+        }
+
+        tQuestionItemService.lambdaUpdate()
+                .in(TQuestionItem::getQuestionId, questionIds)
+                .eq(TQuestionItem::getDelFlag, Global.UN_DELETED)
+                .set(TQuestionItem::getDelFlag, Global.DELETED)
+                .set(TQuestionItem::getUpdateBy, userId)
+                .set(TQuestionItem::getUpdateTime, now)
+                .update();
+
+        tQuestionService.lambdaUpdate()
+                .in(TQuestion::getId, questionIds)
+                .eq(TQuestion::getDelFlag, Global.UN_DELETED)
+                .set(TQuestion::getDelFlag, Global.DELETED)
+                .set(TQuestion::getUpdateBy, userId)
+                .set(TQuestion::getUpdateTime, now)
+                .update();
+    }
+
+    private ExamManageItemResVo toManageItem(TExam exam) {
+        ExamManageItemResVo vo = new ExamManageItemResVo();
+        vo.setId(exam.getId());
+        vo.setName(exam.getName());
+        vo.setAmount(exam.getAmount());
+        vo.setExamDuration(exam.getExamDuration());
+        vo.setPassRate(exam.getPassRate());
+        vo.setStatus(exam.getStatus());
+        vo.setStatusName(ExamStatusEnum.getNameByCode(exam.getStatus()));
+        vo.setCreateTime(exam.getCreateTime());
+        return vo;
+    }
+
+    private ExamDetailResVo toDetail(TExam exam) {
+        ExamDetailResVo vo = new ExamDetailResVo();
+        vo.setId(exam.getId());
+        vo.setName(exam.getName());
+        vo.setExamDuration(exam.getExamDuration());
+        vo.setPassRate(exam.getPassRate());
+        vo.setAmount(exam.getAmount());
+        vo.setDescription(exam.getDescription());
+        vo.setStatus(exam.getStatus());
+        vo.setStatusName(ExamStatusEnum.getNameByCode(exam.getStatus()));
+        vo.setCreateTime(exam.getCreateTime());
+        vo.setUpdateTime(exam.getUpdateTime());
+        return vo;
+    }
+
+    private ExamDetailResVo.QuestionItem toQuestionItem(TQuestionItem item) {
+        ExamDetailResVo.QuestionItem vo = new ExamDetailResVo.QuestionItem();
+        vo.setId(item.getId());
+        vo.setName(item.getName());
+        vo.setContent(item.getContent());
+        vo.setAnswerFlag(item.getAnswerFlag());
+        vo.setSort(item.getSort());
+        return vo;
+    }
+
+    private static String normalizeNullable(String value) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+
+    private static List<Long> parseIds(String idsStr, Long singleId) {
+        if (singleId != null) {
+            return List.of(singleId);
+        }
+        if (StrUtil.isBlank(idsStr)) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(idsStr.split(","))
+                .map(String::trim)
+                .filter(StrUtil::isNotBlank)
+                .map(Long::parseLong)
+                .distinct()
+                .toList();
+    }
+
+    private static void validateSaveReq(ExamSaveReqVo reqVo, boolean requireId) {
+        if (reqVo == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "参数不能为空");
+        }
+        if (requireId && reqVo.getId() == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, HttpStatus.ID_MISSING);
+        }
+        if (StrUtil.isBlank(reqVo.getName())) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "考试名称不能为空");
+        }
+        if (reqVo.getExamDuration() == null || reqVo.getExamDuration() <= 0) {
+            throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "考试时长非法");
+        }
+        if (reqVo.getPassRate() == null || reqVo.getPassRate() < 0 || reqVo.getPassRate() > 100) {
+            throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "及格率非法");
+        }
+        if (reqVo.getStatus() != null && reqVo.getStatus() != 1 && reqVo.getStatus() != 2) {
+            throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "status参数非法");
+        }
+
+        List<ExamSaveReqVo.Question> questions = reqVo.getQuestions() == null ? Collections.emptyList() : reqVo.getQuestions();
+        if (questions.isEmpty()) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "题目不能为空");
+        }
+
+        for (ExamSaveReqVo.Question q : questions) {
+            if (q == null || StrUtil.isBlank(q.getName())) {
+                throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "题目名称不能为空");
+            }
+            if (q.getType() == null || (q.getType() != 1 && q.getType() != 2 && q.getType() != 3)) {
+                throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "题目类型非法");
+            }
+
+            List<ExamSaveReqVo.QuestionItem> items = q.getItems() == null ? Collections.emptyList() : q.getItems();
+            if (q.getType() == 3) {
+                continue;
+            }
+            if (items.isEmpty()) {
+                throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "题目选项不能为空");
+            }
+
+            long answerCount = items.stream().filter(item -> item != null && Objects.equals(item.getAnswerFlag(), 1)).count();
+            if (q.getType() == 1 && answerCount != 1) {
+                throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "单选题必须且只能有1个正确答案");
+            }
+            if (q.getType() == 2 && answerCount < 1) {
+                throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "多选题至少要有1个正确答案");
+            }
+
+            for (ExamSaveReqVo.QuestionItem item : items) {
+                if (item == null || StrUtil.isBlank(item.getContent())) {
+                    throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "选项内容不能为空");
+                }
+                if (item.getAnswerFlag() == null || (item.getAnswerFlag() != 1 && item.getAnswerFlag() != 2)) {
+                    throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "answerFlag参数非法");
+                }
+            }
+        }
+    }
+
+    private static Set<Long> resolveAuthorizedEntIds() {
+        if (LoginUserHolder.isManager()) {
+            return Collections.emptySet();
+        }
+        Set<Long> result = new LinkedHashSet<>();
+        if (LoginUserHolder.getEntId() != null) {
+            result.add(LoginUserHolder.getEntId());
+        }
+        result.addAll(LoginUserHolder.getChildEntList());
+        return result;
+    }
+
+    private static boolean isAuthorizedEnt(Long entId) {
+        if (LoginUserHolder.isManager()) {
+            return true;
+        }
+        if (entId == null) {
+            return false;
+        }
+        Set<Long> entIds = resolveAuthorizedEntIds();
+        return entIds.contains(entId);
+    }
+}

+ 839 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsTrainAppService.java

@@ -0,0 +1,839 @@
+package com.sckw.system.service;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.sckw.core.exception.SystemException;
+import com.sckw.core.model.constant.Global;
+import com.sckw.core.web.constant.HttpStatus;
+import com.sckw.core.web.context.LoginUserHolder;
+import com.sckw.core.web.response.result.PageDataResult;
+import com.sckw.system.dao.KwsUserDao;
+import com.sckw.system.model.*;
+import com.sckw.system.model.pojo.CourseTypeEnum;
+import com.sckw.system.model.pojo.QuestionTypeEnum;
+import com.sckw.system.model.pojo.TrainStatusEnum;
+import com.sckw.system.model.vo.req.TrainAppExamSubmitReqVo;
+import com.sckw.system.model.vo.req.TrainAppListReqVo;
+import com.sckw.system.model.vo.req.TrainCourseStudyReqVo;
+import com.sckw.system.model.vo.res.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class KwsTrainAppService {
+
+    private final TTrainService tTrainService;
+    private final TTrainRecordService tTrainRecordService;
+    private final TTrainCourseService tTrainCourseService;
+    private final TTrainExamService tTrainExamService;
+    private final TCourseService tCourseService;
+    private final TCourseRecordService tCourseRecordService;
+    private final TExamService tExamService;
+    private final TExamLogService tExamLogService;
+    private final TExamRecordService tExamRecordService;
+    private final TQuestionService tQuestionService;
+    private final TQuestionItemService tQuestionItemService;
+    private final KwsCourseService kwsCourseService;
+    private final KwsUserDao kwsUserDao;
+
+    private Long getDriverId() {
+        KwsUser kwsUser = kwsUserDao.selectById(LoginUserHolder.getUserId());
+        if (Objects.nonNull(kwsUser)) {
+            return Long.parseLong(kwsUser.getDriverId());
+        }
+        return null;
+    }
+
+    public TrainAppListResVo listTrainTasks(TrainAppListReqVo reqVo) {
+        Long driverId = getDriverId();
+        if (Objects.isNull(driverId)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "未找到司机id");
+        }
+        LambdaQueryWrapper<TTrainRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TTrainRecord::getDelFlag, Global.UN_DELETED)
+                .eq(TTrainRecord::getDriverId, driverId)
+                .orderByDesc(TTrainRecord::getCreateTime);
+        if (reqVo.getStatus() != null && reqVo.getStatus() > 0) {
+            wrapper.eq(TTrainRecord::getStatus, reqVo.getStatus());
+        }
+
+        Page<TTrainRecord> page = new Page<>(reqVo.getPageNum(), reqVo.getPageSize());
+        Page<TTrainRecord> recordPage = tTrainRecordService.page(page, wrapper);
+        List<TTrainRecord> records = recordPage.getRecords();
+
+        Map<Long, TTrain> trainMap = loadTrainMap(records.stream().map(TTrainRecord::getTrainId).toList());
+        Map<Long, List<Long>> trainCourseIdsMap = loadTrainCourseIdsMap(trainMap.keySet());
+        Map<Long, TCourse> courseMap = loadCourseMap(trainCourseIdsMap.values().stream().flatMap(List::stream).distinct().toList());
+        Map<String, Integer> learnStatusMap = loadLearnStatusMap(driverId, trainMap.keySet());
+
+        List<TrainAppTaskResVo> taskVos = records.stream().map(r -> toTask(r, trainMap.get(r.getTrainId()), trainCourseIdsMap.getOrDefault(r.getTrainId(), Collections.emptyList()), courseMap, learnStatusMap)).toList();
+
+        TrainAppListResVo resVo = new TrainAppListResVo();
+        resVo.setProgressRate(calcOverallProgressRate(driverId));
+        resVo.setTotalTask((int) recordPage.getTotal());
+        resVo.setPage(PageDataResult.of(page, taskVos));
+        return resVo;
+    }
+
+    public TrainAppDetailResVo trainDetail(Long trainId) {
+        Long driverId = getDriverId();
+
+        TTrainRecord trainRecord = getDriverTrainRecord(trainId, driverId);
+        TTrain train = tTrainService.getById(trainId);
+        if (train == null || !Objects.equals(train.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "培训计划不存在");
+        }
+
+        List<Long> courseIds = tTrainCourseService.lambdaQuery()
+                .select(TTrainCourse::getCourseId)
+                .eq(TTrainCourse::getTrainId, trainId)
+                .list()
+                .stream()
+                .map(TTrainCourse::getCourseId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .toList();
+
+        Map<Long, TCourse> courseMap = loadCourseMap(courseIds);
+        Map<String, Integer> learnStatusMap = loadLearnStatusMap(driverId, Set.of(trainId));
+        List<TrainAppTaskCourseResVo> courses = buildCourseList(courseIds, courseMap, learnStatusMap, trainId);
+
+        TrainAppDetailResVo res = new TrainAppDetailResVo();
+        res.setTrainId(trainId);
+        res.setTrainName(train.getName());
+        res.setStartDate(train.getStartDate());
+        res.setEndDate(train.getEndDate());
+        res.setStatus(trainRecord.getStatus());
+        res.setStatusName(TrainStatusEnum.getNameByCode(trainRecord.getStatus()));
+        res.setLearnedCourse(defaultInt(trainRecord.getFinishCourse()));
+        res.setSumCourse(defaultInt(trainRecord.getSumCourse()));
+        res.setCourses(courses);
+
+        TrainAppDetailResVo.Exam exam = resolveTrainExam(trainId);
+        res.setExam(exam);
+
+        boolean allCourseDone = Objects.equals(res.getLearnedCourse(), res.getSumCourse()) && res.getSumCourse() > 0;
+        res.setShowExam(Objects.equals(train.getExamFlag(),1) && allCourseDone && exam != null);
+
+        if (Objects.equals(train.getExamFlag(),1) && exam != null) {
+            boolean passed = checkExamPassed(trainId, exam.getExamId(), driverId, exam.getPassRate());
+            res.setExamPassed(passed);
+        } else {
+            res.setExamPassed(false);
+        }
+
+        return res;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean startCourse(TrainCourseStudyReqVo reqVo) {
+        Long driverId = getDriverId();
+        ensureCourseInTrain(reqVo.getTrainId(), reqVo.getCourseId());
+
+        TTrainRecord trainRecord = getDriverTrainRecord(reqVo.getTrainId(), driverId);
+        if (Objects.equals(trainRecord.getStatus(), 3)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "培训任务已结束");
+        }
+
+        TCourseRecord record = tCourseRecordService.lambdaQuery()
+                .eq(TCourseRecord::getDelFlag, Global.UN_DELETED)
+                .eq(TCourseRecord::getTrainId, reqVo.getTrainId())
+                .eq(TCourseRecord::getCourseId, reqVo.getCourseId())
+                .eq(TCourseRecord::getDriverId, driverId)
+                .last("limit 1")
+                .one();
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        if (record == null) {
+            record = new TCourseRecord();
+            record.setTrainId(reqVo.getTrainId());
+            record.setCourseId(reqVo.getCourseId());
+            record.setDriverId(driverId);
+            record.setStartTime(now);
+            record.setEndTime(null);
+            record.setStatus(0);
+            record.setCreateBy(userId);
+            record.setCreateTime(now);
+            record.setUpdateBy(userId);
+            record.setUpdateTime(now);
+            record.setDelFlag(Global.UN_DELETED);
+            if (!tCourseRecordService.save(record)) {
+                throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "开始学习失败");
+            }
+        } else if (Objects.equals(record.getStatus(), 1)) {
+            return true;
+        } else {
+            boolean updated = tCourseRecordService.lambdaUpdate()
+                    .eq(TCourseRecord::getId, record.getId())
+                    .set(TCourseRecord::getStartTime, record.getStartTime() == null ? now : record.getStartTime())
+                    .set(TCourseRecord::getUpdateBy, userId)
+                    .set(TCourseRecord::getUpdateTime, now)
+                    .update();
+            if (!updated) {
+                throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "开始学习失败");
+            }
+        }
+
+        updateTrainRecordStatusOnLearning(reqVo.getTrainId(), driverId);
+        return true;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean finishCourse(TrainCourseStudyReqVo reqVo) {
+        Long driverId = getDriverId();
+//        ensureDriverInTrain(reqVo.getTrainId(), driverId);
+        ensureCourseInTrain(reqVo.getTrainId(), reqVo.getCourseId());
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        TCourseRecord record = tCourseRecordService.lambdaQuery()
+                .eq(TCourseRecord::getDelFlag, Global.UN_DELETED)
+                .eq(TCourseRecord::getTrainId, reqVo.getTrainId())
+                .eq(TCourseRecord::getCourseId, reqVo.getCourseId())
+                .eq(TCourseRecord::getDriverId, driverId)
+                .last("limit 1")
+                .one();
+
+        if (record == null) {
+            record = new TCourseRecord();
+            record.setTrainId(reqVo.getTrainId());
+            record.setCourseId(reqVo.getCourseId());
+            record.setDriverId(driverId);
+            record.setStartTime(now);
+            record.setEndTime(now);
+            record.setStatus(1);
+            record.setCreateBy(userId);
+            record.setCreateTime(now);
+            record.setUpdateBy(userId);
+            record.setUpdateTime(now);
+            record.setDelFlag(Global.UN_DELETED);
+            if (!tCourseRecordService.save(record)) {
+                throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "完成课程失败");
+            }
+        } else if (!Objects.equals(record.getStatus(), 1)) {
+            boolean updated = tCourseRecordService.lambdaUpdate()
+                    .eq(TCourseRecord::getId, record.getId())
+                    .set(TCourseRecord::getStatus, 1)
+                    .set(TCourseRecord::getEndTime, now)
+                    .set(TCourseRecord::getUpdateBy, userId)
+                    .set(TCourseRecord::getUpdateTime, now)
+                    .update();
+            if (!updated) {
+                throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "完成课程失败");
+            }
+        }
+
+        syncTrainProgress(reqVo.getTrainId(), driverId, now, userId);
+        return true;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public TrainAppExamStartResVo startExam(Long trainId) {
+        Long driverId = getDriverId();
+//        ensureDriverInTrain(trainId, driverId);
+
+        TTrain train = tTrainService.getById(trainId);
+        if (train == null || !Objects.equals(train.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "培训计划不存在");
+        }
+        if (!Objects.equals(train.getExamFlag(), 1)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "当前培训无需考试");
+        }
+
+        TTrainRecord trainRecord = getDriverTrainRecord(trainId, driverId);
+        if (!Objects.equals(trainRecord.getFinishCourse(), trainRecord.getSumCourse())) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "请先完成全部课程");
+        }
+
+        Long examId = resolveTrainExamId(trainId);
+        if (examId == null) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "培训考试未配置");
+        }
+
+        TExam exam = tExamService.getById(examId);
+        if (exam == null || !Objects.equals(exam.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "考试不存在");
+        }
+
+        TExamLog existing = tExamLogService.lambdaQuery()
+                .eq(TExamLog::getDelFlag, Global.UN_DELETED)
+                .eq(TExamLog::getTrainId, trainId)
+                .eq(TExamLog::getExamId, examId)
+                .eq(TExamLog::getDriverId, driverId)
+                .eq(TExamLog::getExamFlag, 1)
+                .orderByDesc(TExamLog::getId)
+                .last("limit 1")
+                .one();
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        TExamLog log = existing;
+        if (log == null) {
+            log = new TExamLog();
+            log.setTrainId(trainId);
+            log.setExamId(examId);
+            log.setDriverId(driverId);
+            log.setExamFlag(1);
+            log.setStartTime(now);
+            log.setEndTime(null);
+            log.setRightAmount(0);
+            log.setAmount(exam.getAmount());
+            log.setRightRate(0);
+            log.setCreateBy(userId);
+            log.setCreateTime(now);
+            log.setUpdateBy(userId);
+            log.setUpdateTime(now);
+            log.setDelFlag(Global.UN_DELETED);
+            if (!tExamLogService.save(log)) {
+                throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "开始考试失败");
+            }
+        }
+
+        TrainAppExamStartResVo res = new TrainAppExamStartResVo();
+        res.setLogId(log.getId());
+        res.setExamId(examId);
+        res.setExamName(exam.getName());
+        res.setExamDuration(exam.getExamDuration());
+        res.setPassRate(exam.getPassRate());
+        res.setAmount(exam.getAmount());
+        res.setStartTime(log.getStartTime());
+        return res;
+    }
+
+    public TrainAppExamPaperResVo examPaper(Long trainId, Long logId) {
+        Long driverId = getDriverId();
+//        ensureDriverInTrain(trainId, driverId);
+
+        Long examId = resolveTrainExamId(trainId);
+        if (examId == null) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "培训考试未配置");
+        }
+        TExam exam = tExamService.getById(examId);
+        if (exam == null || !Objects.equals(exam.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "考试不存在");
+        }
+
+        Long resolvedLogId = logId;
+        if (resolvedLogId == null) {
+            TExamLog newest = tExamLogService.lambdaQuery()
+                    .eq(TExamLog::getDelFlag, Global.UN_DELETED)
+                    .eq(TExamLog::getTrainId, trainId)
+                    .eq(TExamLog::getExamId, examId)
+                    .eq(TExamLog::getDriverId, driverId)
+                    .orderByDesc(TExamLog::getId)
+                    .last("limit 1")
+                    .one();
+            resolvedLogId = newest == null ? null : newest.getId();
+        }
+
+        List<TQuestion> questions = tQuestionService.lambdaQuery()
+                .eq(TQuestion::getDelFlag, Global.UN_DELETED)
+                .eq(TQuestion::getExamId, examId)
+                .orderByAsc(TQuestion::getId)
+                .list();
+
+        Map<Long, List<TQuestionItem>> optionMap;
+        if (CollUtil.isNotEmpty(questions)) {
+            List<Long> qIds = questions.stream().map(TQuestion::getId).filter(Objects::nonNull).toList();
+            List<TQuestionItem> items = tQuestionItemService.lambdaQuery()
+                    .eq(TQuestionItem::getDelFlag, Global.UN_DELETED)
+                    .in(TQuestionItem::getQuestionId, qIds)
+                    .orderByAsc(TQuestionItem::getSort)
+                    .orderByAsc(TQuestionItem::getId)
+                    .list();
+            optionMap = items.stream().collect(Collectors.groupingBy(TQuestionItem::getQuestionId));
+        } else {
+            optionMap = Collections.emptyMap();
+        }
+
+        List<TrainAppExamPaperResVo.Question> qVos = questions.stream().map(q -> {
+            TrainAppExamPaperResVo.Question qVo = new TrainAppExamPaperResVo.Question();
+            qVo.setQuestionId(q.getId());
+            qVo.setName(q.getName());
+            qVo.setType(q.getType());
+            qVo.setTypeName(QuestionTypeEnum.getNameByCode(q.getType()));
+            List<TQuestionItem> items = optionMap.getOrDefault(q.getId(), Collections.emptyList());
+            List<TrainAppExamPaperResVo.Option> opts = items.stream().map(item -> {
+                TrainAppExamPaperResVo.Option opt = new TrainAppExamPaperResVo.Option();
+                opt.setName(item.getName());
+                opt.setContent(item.getContent());
+                return opt;
+            }).toList();
+            qVo.setOptions(opts);
+            return qVo;
+        }).toList();
+
+        TrainAppExamPaperResVo res = new TrainAppExamPaperResVo();
+        res.setLogId(resolvedLogId);
+        res.setExamId(examId);
+        res.setExamName(exam.getName());
+        res.setExamDuration(exam.getExamDuration());
+        res.setPassRate(exam.getPassRate());
+        res.setAmount(exam.getAmount());
+        res.setQuestions(qVos);
+        return res;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public TrainAppExamSubmitResVo submitExam(TrainAppExamSubmitReqVo reqVo) {
+        Long driverId = getDriverId();
+
+        TExamLog log = tExamLogService.getById(reqVo.getLogId());
+        if (log == null || !Objects.equals(log.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "考试日志不存在");
+        }
+        if (!Objects.equals(log.getDriverId(), driverId)) {
+            throw new SystemException(HttpStatus.AUTHORITY_NO_CODE, HttpStatus.ACCESS_FIAL);
+        }
+        if (!Objects.equals(log.getExamFlag(), 1)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "考试已结束");
+        }
+
+        Long trainId = log.getTrainId();
+        Long examId = log.getExamId();
+//        ensureDriverInTrain(trainId, driverId);
+
+        TExam exam = tExamService.getById(examId);
+        if (exam == null || !Objects.equals(exam.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "考试不存在");
+        }
+
+        List<TQuestion> questions = tQuestionService.lambdaQuery()
+                .eq(TQuestion::getDelFlag, Global.UN_DELETED)
+                .eq(TQuestion::getExamId, examId)
+                .list();
+        Map<Long, Integer> questionTypeMap = questions.stream().filter(q -> q.getId() != null).collect(Collectors.toMap(TQuestion::getId, TQuestion::getType, (a, b) -> a));
+
+        List<Long> qIds = questions.stream().map(TQuestion::getId).filter(Objects::nonNull).toList();
+        Map<Long, Set<String>> correctAnswerMap = buildCorrectAnswerMap(qIds);
+
+        Map<Long, String> userAnswerMap = reqVo.getAnswers().stream()
+                .filter(a -> a != null && a.getQuestionId() != null)
+                .collect(Collectors.toMap(TrainAppExamSubmitReqVo.AnswerItem::getQuestionId, a -> normalizeAnswer(a.getAnswer()), (a, b) -> a));
+
+        tExamRecordService.lambdaUpdate()
+                .eq(TExamRecord::getLogId, log.getId())
+                .eq(TExamRecord::getDelFlag, Global.UN_DELETED)
+                .set(TExamRecord::getDelFlag, Global.DELETED)
+                .update();
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        List<TExamRecord> recordEntities = new ArrayList<>();
+        int rightAmount = 0;
+        for (Long questionId : qIds) {
+            Integer type = questionTypeMap.get(questionId);
+            Set<String> correct = correctAnswerMap.getOrDefault(questionId, Collections.emptySet());
+            String userAns = userAnswerMap.getOrDefault(questionId, "");
+            int correctFlag = judgeCorrectFlag(type, userAns, correct);
+            if (correctFlag == 1) {
+                rightAmount++;
+            }
+
+            TExamRecord record = new TExamRecord();
+            record.setLogId(log.getId());
+            record.setQuestionId(questionId);
+            record.setAnswer(userAns);
+            record.setCorrectFlag(correctFlag);
+            record.setCreateBy(userId);
+            record.setCreateTime(now);
+            record.setUpdateBy(userId);
+            record.setUpdateTime(now);
+            record.setDelFlag(Global.UN_DELETED);
+            recordEntities.add(record);
+        }
+
+        if (!recordEntities.isEmpty() && !tExamRecordService.saveBatch(recordEntities)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "保存考试记录失败");
+        }
+
+        int amount = exam.getAmount() == null ? qIds.size() : exam.getAmount();
+        int rightRate = amount == 0 ? 0 : (int) Math.round(rightAmount * 100.0 / amount);
+        boolean passed = rightRate >= (exam.getPassRate() == null ? 0 : exam.getPassRate());
+
+        boolean updated = tExamLogService.lambdaUpdate()
+                .eq(TExamLog::getId, log.getId())
+                .set(TExamLog::getExamFlag, 2)
+                .set(TExamLog::getEndTime, now)
+                .set(TExamLog::getRightAmount, rightAmount)
+                .set(TExamLog::getAmount, amount)
+                .set(TExamLog::getRightRate, rightRate)
+                .set(TExamLog::getUpdateBy, userId)
+                .set(TExamLog::getUpdateTime, now)
+                .update();
+        if (!updated) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "更新考试日志失败");
+        }
+
+        syncTrainExamResult(trainId, driverId, rightRate, passed, now, userId);
+
+        TrainAppExamSubmitResVo resVo = new TrainAppExamSubmitResVo();
+        resVo.setRightAmount(rightAmount);
+        resVo.setAmount(amount);
+        resVo.setRightRate(rightRate);
+        resVo.setPassed(passed);
+        return resVo;
+    }
+
+    private Map<Long, TTrain> loadTrainMap(List<Long> trainIds) {
+        if (CollUtil.isEmpty(trainIds)) {
+            return Collections.emptyMap();
+        }
+        List<TTrain> trains = tTrainService.listByIds(trainIds.stream().filter(Objects::nonNull).distinct().toList());
+        return trains.stream()
+                .filter(t -> t != null && Objects.equals(t.getDelFlag(), Global.UN_DELETED))
+                .collect(Collectors.toMap(TTrain::getId, t -> t, (a, b) -> a));
+    }
+
+    private Map<Long, List<Long>> loadTrainCourseIdsMap(Set<Long> trainIds) {
+        if (CollUtil.isEmpty(trainIds)) {
+            return Collections.emptyMap();
+        }
+        List<TTrainCourse> relations = tTrainCourseService.lambdaQuery()
+                .in(TTrainCourse::getTrainId, trainIds)
+                .list();
+        Map<Long, List<Long>> result = new LinkedHashMap<>();
+        for (TTrainCourse r : relations) {
+            if (r.getTrainId() == null || r.getCourseId() == null) {
+                continue;
+            }
+            result.computeIfAbsent(r.getTrainId(), k -> new ArrayList<>()).add(r.getCourseId());
+        }
+        result.values().forEach(list -> {
+            List<Long> distinct = list.stream().distinct().toList();
+            list.clear();
+            list.addAll(distinct);
+        });
+        return result;
+    }
+
+    private Map<Long, TCourse> loadCourseMap(List<Long> courseIds) {
+        if (CollUtil.isEmpty(courseIds)) {
+            return Collections.emptyMap();
+        }
+        List<TCourse> courses = tCourseService.listByIds(courseIds.stream().filter(Objects::nonNull).distinct().toList());
+        return courses.stream()
+                .filter(c -> c != null && Objects.equals(c.getDelFlag(), Global.UN_DELETED))
+                .collect(Collectors.toMap(TCourse::getId, c -> c, (a, b) -> a));
+    }
+
+    private Map<String, Integer> loadLearnStatusMap(Long driverId, Set<Long> trainIds) {
+        if (CollUtil.isEmpty(trainIds)) {
+            return Collections.emptyMap();
+        }
+        List<TCourseRecord> records = tCourseRecordService.lambdaQuery()
+                .select(TCourseRecord::getTrainId, TCourseRecord::getCourseId, TCourseRecord::getStatus)
+                .eq(TCourseRecord::getDelFlag, Global.UN_DELETED)
+                .eq(TCourseRecord::getDriverId, driverId)
+                .in(TCourseRecord::getTrainId, trainIds)
+                .list();
+        Map<String, Integer> map = new HashMap<>();
+        for (TCourseRecord r : records) {
+            if (r.getTrainId() == null || r.getCourseId() == null) {
+                continue;
+            }
+            String key = r.getTrainId() + "_" + r.getCourseId();
+            map.put(key, r.getStatus());
+        }
+        return map;
+    }
+
+    private List<TrainAppTaskCourseResVo> buildCourseList(List<Long> courseIds, Map<Long, TCourse> courseMap, Map<String, Integer> learnStatusMap, Long trainId) {
+        if (CollUtil.isEmpty(courseIds)) {
+            return Collections.emptyList();
+        }
+        List<TrainAppTaskCourseResVo> list = new ArrayList<>();
+        for (Long courseId : courseIds) {
+            TCourse course = courseMap.get(courseId);
+            if (course == null) {
+                continue;
+            }
+            TrainAppTaskCourseResVo vo = new TrainAppTaskCourseResVo();
+            vo.setCourseId(courseId);
+            vo.setCourseName(course.getCourseName());
+            vo.setCourseType(course.getCourseType());
+            vo.setCourseTypeName(CourseTypeEnum.getNameByCode(course.getCourseType()));
+            Integer status = learnStatusMap.get(trainId + "_" + courseId);
+            vo.setLearnStatus(Objects.equals(status, 1) ? 1 : 0);
+            list.add(vo);
+        }
+        return list;
+    }
+
+    private TrainAppTaskResVo toTask(TTrainRecord record, TTrain train, List<Long> courseIds, Map<Long, TCourse> courseMap, Map<String, Integer> learnStatusMap) {
+        TrainAppTaskResVo vo = new TrainAppTaskResVo();
+        vo.setTrainId(record.getTrainId());
+        vo.setStatus(record.getStatus());
+        vo.setStatusName(TrainStatusEnum.getNameByCode(record.getStatus()));
+        vo.setLearnedCourse(defaultInt(record.getFinishCourse()));
+        vo.setSumCourse(defaultInt(record.getSumCourse()));
+
+        if (train != null) {
+            vo.setTrainName(train.getName());
+            vo.setStartDate(train.getStartDate());
+            vo.setEndDate(train.getEndDate());
+        }
+
+        vo.setCourses(buildCourseList(courseIds, courseMap, learnStatusMap, record.getTrainId()));
+        boolean allCourseDone = Objects.equals(vo.getLearnedCourse(), vo.getSumCourse()) && vo.getSumCourse() > 0;
+        vo.setShowExam(Boolean.TRUE.equals(train != null ? train.getExamFlag() : null) && allCourseDone);
+        return vo;
+    }
+
+    private Integer calcOverallProgressRate(Long driverId) {
+        List<TTrainRecord> records = tTrainRecordService.lambdaQuery()
+                .select(TTrainRecord::getFinishCourse, TTrainRecord::getSumCourse)
+                .eq(TTrainRecord::getDelFlag, Global.UN_DELETED)
+                .eq(TTrainRecord::getDriverId, driverId)
+                .list();
+        int learned = records.stream().mapToInt(r -> defaultInt(r.getFinishCourse())).sum();
+        int sum = records.stream().mapToInt(r -> defaultInt(r.getSumCourse())).sum();
+        if (sum == 0) {
+            return 0;
+        }
+        return (int) Math.round(learned * 100.0 / sum);
+    }
+
+    private static int defaultInt(Integer value) {
+        return value == null ? 0 : value;
+    }
+
+
+    private void ensureCourseInTrain(Long trainId, Long courseId) {
+        boolean exists = tTrainCourseService.lambdaQuery()
+                .eq(TTrainCourse::getTrainId, trainId)
+                .eq(TTrainCourse::getCourseId, courseId)
+                .exists();
+        if (!exists) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "课程不属于该培训");
+        }
+    }
+
+    private TTrainRecord getDriverTrainRecord(Long trainId, Long driverId) {
+        TTrainRecord record = tTrainRecordService.lambdaQuery()
+                .eq(TTrainRecord::getDelFlag, Global.UN_DELETED)
+                .eq(TTrainRecord::getTrainId, trainId)
+                .eq(TTrainRecord::getDriverId, driverId)
+                .last("limit 1")
+                .one();
+        if (record == null) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "培训任务不存在");
+        }
+        return record;
+    }
+
+    private void updateTrainRecordStatusOnLearning(Long trainId, Long driverId) {
+        TTrainRecord record = getDriverTrainRecord(trainId, driverId);
+        if (Objects.equals(record.getStatus(), 1)) {
+            LocalDateTime now = LocalDateTime.now();
+            Long userId = LoginUserHolder.getUserId();
+            tTrainRecordService.lambdaUpdate()
+                    .eq(TTrainRecord::getId, record.getId())
+                    .set(TTrainRecord::getStatus, 2)
+                    .set(TTrainRecord::getUpdateBy, userId)
+                    .set(TTrainRecord::getUpdateTime, now)
+                    .update();
+        }
+    }
+
+    private void syncTrainProgress(Long trainId, Long driverId, LocalDateTime now, Long userId) {
+        TTrainRecord trainRecord = getDriverTrainRecord(trainId, driverId);
+        int sumCourse = defaultInt(trainRecord.getSumCourse());
+        int learned = countLearnedCourse(trainId, driverId);
+        Integer newStatus = trainRecord.getStatus();
+        if (learned > 0 && Objects.equals(newStatus, 1)) {
+            newStatus = 2;
+        }
+
+        TTrain train = tTrainService.getById(trainId);
+        boolean hasExam = train != null && Objects.equals(train.getExamFlag(), 1);
+        if (sumCourse > 0 && learned >= sumCourse) {
+            if (!hasExam) {
+                newStatus = 3;
+            } else {
+                newStatus = 2;
+            }
+        }
+
+        boolean updated = tTrainRecordService.lambdaUpdate()
+                .eq(TTrainRecord::getId, trainRecord.getId())
+                .set(TTrainRecord::getFinishCourse, learned)
+                .set(TTrainRecord::getStatus, newStatus)
+                .set(TTrainRecord::getUpdateBy, userId)
+                .set(TTrainRecord::getUpdateTime, now)
+                .update();
+        if (!updated) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "更新培训记录失败");
+        }
+    }
+
+    private int countLearnedCourse(Long trainId, Long driverId) {
+        Long count = tCourseRecordService.lambdaQuery()
+                .eq(TCourseRecord::getDelFlag, Global.UN_DELETED)
+                .eq(TCourseRecord::getTrainId, trainId)
+                .eq(TCourseRecord::getDriverId, driverId)
+                .eq(TCourseRecord::getStatus, 1)
+                .count();
+        return count == null ? 0 : count.intValue();
+    }
+
+    private Long resolveTrainExamId(Long trainId) {
+        return tTrainExamService.lambdaQuery()
+                .select(TTrainExam::getExamId)
+                .eq(TTrainExam::getTrainId, trainId)
+                .orderByDesc(TTrainExam::getId)
+                .last("limit 1")
+                .oneOpt()
+                .map(TTrainExam::getExamId)
+                .orElse(null);
+    }
+
+    private TrainAppDetailResVo.Exam resolveTrainExam(Long trainId) {
+        Long examId = resolveTrainExamId(trainId);
+        if (examId == null) {
+            return null;
+        }
+        TExam exam = tExamService.getById(examId);
+        if (exam == null || !Objects.equals(exam.getDelFlag(), Global.UN_DELETED)) {
+            return null;
+        }
+        TrainAppDetailResVo.Exam vo = new TrainAppDetailResVo.Exam();
+        vo.setExamId(examId);
+        vo.setName(exam.getName());
+        vo.setExamDuration(exam.getExamDuration());
+        vo.setPassRate(exam.getPassRate());
+        vo.setAmount(exam.getAmount());
+        return vo;
+    }
+
+    private boolean checkExamPassed(Long trainId, Long examId, Long driverId, Integer passRate) {
+        TExamLog newest = tExamLogService.lambdaQuery()
+                .eq(TExamLog::getDelFlag, Global.UN_DELETED)
+                .eq(TExamLog::getTrainId, trainId)
+                .eq(TExamLog::getExamId, examId)
+                .eq(TExamLog::getDriverId, driverId)
+                .eq(TExamLog::getExamFlag, 2)
+                .orderByDesc(TExamLog::getId)
+                .last("limit 1")
+                .one();
+        if (newest == null) {
+            return false;
+        }
+        Integer rate = newest.getRightRate();
+        if (rate == null) {
+            return false;
+        }
+        int pass = passRate == null ? 0 : passRate;
+        return rate >= pass;
+    }
+
+    private Map<Long, Set<String>> buildCorrectAnswerMap(List<Long> questionIds) {
+        if (CollUtil.isEmpty(questionIds)) {
+            return Collections.emptyMap();
+        }
+        List<TQuestionItem> items = tQuestionItemService.lambdaQuery()
+                .select(TQuestionItem::getQuestionId, TQuestionItem::getName, TQuestionItem::getAnswerFlag)
+                .eq(TQuestionItem::getDelFlag, Global.UN_DELETED)
+                .in(TQuestionItem::getQuestionId, questionIds)
+                .list();
+        Map<Long, Set<String>> map = new HashMap<>();
+        for (TQuestionItem item : items) {
+            if (item.getQuestionId() == null) {
+                continue;
+            }
+            if (!Objects.equals(item.getAnswerFlag(), 1)) {
+                continue;
+            }
+            if (StrUtil.isBlank(item.getName())) {
+                continue;
+            }
+            String opt = item.getName().trim().toUpperCase();
+            map.computeIfAbsent(item.getQuestionId(), k -> new LinkedHashSet<>()).add(opt);
+        }
+        return map;
+    }
+
+    private static String normalizeAnswer(String answer) {
+        if (answer == null) {
+            return "";
+        }
+        return Arrays.stream(answer.split(","))
+                .map(String::trim)
+                .filter(StrUtil::isNotBlank)
+                .map(s -> s.toUpperCase())
+                .distinct()
+                .collect(Collectors.joining(","));
+    }
+
+    private static int judgeCorrectFlag(Integer questionType, String userAnswer, Set<String> correct) {
+        if (questionType == null) {
+            return 2;
+        }
+        if (questionType == 3) {
+            return 2;
+        }
+
+        Set<String> userSet = Arrays.stream(userAnswer.split(","))
+                .map(String::trim)
+                .filter(StrUtil::isNotBlank)
+                .map(String::toUpperCase)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+
+        if (questionType == 1) {
+            if (userSet.size() != 1 || correct.size() != 1) {
+                return 2;
+            }
+            return Objects.equals(userSet.iterator().next(), correct.iterator().next()) ? 1 : 2;
+        }
+
+        if (userSet.isEmpty()) {
+            return 2;
+        }
+        if (userSet.equals(correct)) {
+            return 1;
+        }
+        boolean subset = correct.containsAll(userSet);
+        return subset ? 3 : 2;
+    }
+
+    private void syncTrainExamResult(Long trainId, Long driverId, Integer rightRate, boolean passed, LocalDateTime now, Long userId) {
+        TTrainRecord record = getDriverTrainRecord(trainId, driverId);
+        Integer newStatus = record.getStatus();
+        if (passed && Objects.equals(record.getFinishCourse(), record.getSumCourse())) {
+            newStatus = 3;
+        } else if (Objects.equals(newStatus, 1)) {
+            newStatus = 2;
+        }
+
+        boolean updated = tTrainRecordService.lambdaUpdate()
+                .eq(TTrainRecord::getId, record.getId())
+                .set(TTrainRecord::getRightRate, rightRate)
+                .set(TTrainRecord::getStatus, newStatus)
+                .set(TTrainRecord::getUpdateBy, userId)
+                .set(TTrainRecord::getUpdateTime, now)
+                .update();
+        if (!updated) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "更新培训记录失败");
+        }
+    }
+
+    public CourseDetailResVo courseDetail(Long courseId) {
+        return kwsCourseService.manageCourseDetail(courseId);
+    }
+}
+

+ 436 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsTrainService.java

@@ -0,0 +1,436 @@
+package com.sckw.system.service;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.sckw.core.exception.SystemException;
+import com.sckw.core.model.constant.Global;
+import com.sckw.core.web.constant.HttpStatus;
+import com.sckw.core.web.context.LoginUserHolder;
+import com.sckw.core.web.response.result.PageDataResult;
+import com.sckw.system.model.*;
+import com.sckw.system.model.pojo.CourseTypeEnum;
+import com.sckw.system.model.pojo.TrainStatusEnum;
+import com.sckw.system.model.vo.req.IdReqVo;
+import com.sckw.system.model.vo.req.TrainManagePageReqVo;
+import com.sckw.system.model.vo.req.TrainSaveReqVo;
+import com.sckw.system.model.vo.res.TrainDetailResVo;
+import com.sckw.system.model.vo.res.TrainManageItemResVo;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class KwsTrainService {
+
+    private final TTrainService tTrainService;
+    private final TTrainCourseService tTrainCourseService;
+    private final TTrainExamService tTrainExamService;
+    private final TTrainRecordService tTrainRecordService;
+    private final TCourseService tCourseService;
+    private final TExamService tExamService;
+
+    public PageDataResult<TrainManageItemResVo> manageTrainPage(TrainManagePageReqVo reqVo) {
+        Page<TTrain> page = new Page<>(reqVo.getPageNum(), reqVo.getPageSize());
+        LambdaQueryWrapper<TTrain> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TTrain::getDelFlag, Global.UN_DELETED)
+                .orderByDesc(TTrain::getCreateTime);
+
+        if (StrUtil.isNotBlank(reqVo.getName())) {
+            wrapper.like(TTrain::getName, reqVo.getName().trim());
+        }
+        if (reqVo.getStatus() != null && reqVo.getStatus() > 0) {
+            wrapper.eq(TTrain::getStatus, reqVo.getStatus());
+        }
+
+        Page<TTrain> trainPage = tTrainService.page(page, wrapper);
+        List<TTrain> trains = trainPage.getRecords();
+        Map<Long, Long> trainDriverCountMap = buildTrainDriverCountMap(trains);
+        return PageDataResult.of(page, trains.stream().map(t -> toManageItem(t, trainDriverCountMap.getOrDefault(t.getId(), 0L))).collect(Collectors.toList()));
+    }
+
+    public TrainDetailResVo manageTrainDetail(Long trainId) {
+        TTrain train = tTrainService.getById(trainId);
+        if (train == null || !Objects.equals(train.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "培训计划不存在");
+        }
+
+        TrainDetailResVo detail = toDetail(train);
+
+        List<Long> courseIds = tTrainCourseService.lambdaQuery()
+                .select(TTrainCourse::getCourseId)
+                .eq(TTrainCourse::getTrainId, trainId)
+                .list()
+                .stream()
+                .map(TTrainCourse::getCourseId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .toList();
+
+        List<TCourse> courses = CollUtil.isEmpty(courseIds) ? Collections.emptyList() : tCourseService.listByIds(courseIds);
+        List<TrainDetailResVo.Course> courseVos = courses.stream()
+                .filter(c -> c != null && Objects.equals(c.getDelFlag(), Global.UN_DELETED))
+                .map(this::toCourseVo)
+                .toList();
+        detail.setCourses(courseVos);
+
+        Long examId = tTrainExamService.lambdaQuery()
+                .select(TTrainExam::getExamId)
+                .eq(TTrainExam::getTrainId, trainId)
+                .last("limit 1")
+                .oneOpt()
+                .map(TTrainExam::getExamId)
+                .orElse(null);
+
+        if (examId != null) {
+            TExam exam = tExamService.getById(examId);
+            if (exam != null && Objects.equals(exam.getDelFlag(), Global.UN_DELETED)) {
+                detail.setExam(toExamVo(exam));
+            }
+        }
+
+
+        List<TTrainRecord> records = tTrainRecordService.lambdaQuery()
+                .eq(TTrainRecord::getDelFlag, Global.UN_DELETED)
+                .eq(TTrainRecord::getTrainId, trainId)
+                .orderByAsc(TTrainRecord::getId)
+                .list();
+
+        List<TrainDetailResVo.DriverProgress> progressList = records.stream().map(this::toDriverProgress).toList();
+        detail.setDriverProgressList(progressList);
+        detail.setProgressSummary(buildProgressSummary(progressList));
+
+        return detail;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Long addTrain(TrainSaveReqVo reqVo) {
+        validateSaveReq(reqVo, false);
+
+        Long entId = LoginUserHolder.getEntId();
+        if (entId == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "企业信息缺失");
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        TTrain train = new TTrain();
+        train.setEntId(entId);
+        train.setName(reqVo.getName().trim());
+        train.setStartDate(reqVo.getStartDate());
+        train.setEndDate(reqVo.getEndDate());
+        train.setExamFlag(reqVo.getExamFlag());
+        train.setDescription(normalizeNullable(reqVo.getDescription()));
+        train.setStatus(calcTrainStatus(reqVo.getStartDate(), reqVo.getEndDate()));
+        train.setCreateBy(userId);
+        train.setCreateTime(now);
+        train.setUpdateBy(userId);
+        train.setUpdateTime(now);
+        train.setDelFlag(Global.UN_DELETED);
+
+        if (!tTrainService.save(train)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "新增培训计划失败");
+        }
+
+        saveRelations(train.getId(), reqVo.getCourseIds(), reqVo.getDriverIds(), reqVo.getExamFlag(), reqVo.getExamId());
+        saveTrainRecords(entId, train.getId(), reqVo.getDriverIds(), reqVo.getCourseIds().size(), userId, now);
+        return train.getId();
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateTrain(TrainSaveReqVo reqVo) {
+        validateSaveReq(reqVo, true);
+
+        TTrain train = tTrainService.getById(reqVo.getId());
+        if (train == null || !Objects.equals(train.getDelFlag(), Global.UN_DELETED)) {
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "培训计划不存在");
+        }
+        if(!Objects.equals(train.getStatus(),TrainStatusEnum.NOT_START.getCode())){
+            throw new SystemException(HttpStatus.QUERY_FAIL_CODE, "培训计划当前状态不允许修改");
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        train.setName(reqVo.getName().trim());
+        train.setStartDate(reqVo.getStartDate());
+        train.setEndDate(reqVo.getEndDate());
+        train.setExamFlag(reqVo.getExamFlag());
+        train.setDescription(normalizeNullable(reqVo.getDescription()));
+        train.setStatus(calcTrainStatus(reqVo.getStartDate(), reqVo.getEndDate()));
+        train.setUpdateBy(userId);
+        train.setUpdateTime(now);
+
+        if (!tTrainService.updateById(train)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, HttpStatus.UPDATE_FAIL);
+        }
+
+        deleteRelations(train.getId());
+        logicDeleteTrainRecords(train.getId(), userId, now);
+        saveRelations(train.getId(), reqVo.getCourseIds(), reqVo.getDriverIds(), reqVo.getExamFlag(), reqVo.getExamId());
+        saveTrainRecords(train.getEntId(), train.getId(), reqVo.getDriverIds(), reqVo.getCourseIds().size(), userId, now);
+        return true;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteTrain(IdReqVo reqVo) {
+        List<Long> trainIds = parseIds(reqVo == null ? null : reqVo.getIds(), reqVo == null ? null : reqVo.getId());
+        if (trainIds.isEmpty()) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "ids不能为空");
+        }
+
+
+        LocalDateTime now = LocalDateTime.now();
+        Long userId = LoginUserHolder.getUserId();
+
+        boolean updated = tTrainService.lambdaUpdate()
+                .in(TTrain::getId, trainIds)
+                .eq(TTrain::getDelFlag, Global.UN_DELETED)
+                .set(TTrain::getDelFlag, Global.DELETED)
+                .set(TTrain::getUpdateBy, userId)
+                .set(TTrain::getUpdateTime, now)
+                .update();
+
+        if (!updated) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, HttpStatus.DELETE_FAIL);
+        }
+
+        tTrainCourseService.lambdaUpdate().in(TTrainCourse::getTrainId, trainIds).remove();
+        tTrainExamService.lambdaUpdate().in(TTrainExam::getTrainId, trainIds).remove();
+
+        tTrainRecordService.lambdaUpdate()
+                .in(TTrainRecord::getTrainId, trainIds)
+                .eq(TTrainRecord::getDelFlag, Global.UN_DELETED)
+                .set(TTrainRecord::getDelFlag, Global.DELETED)
+                .set(TTrainRecord::getUpdateBy, userId)
+                .set(TTrainRecord::getUpdateTime, now)
+                .update();
+
+        return true;
+    }
+
+    private Map<Long, Long> buildTrainDriverCountMap(List<TTrain> trains) {
+        if (CollUtil.isEmpty(trains)) {
+            return Collections.emptyMap();
+        }
+        List<Long> trainIds = trains.stream().map(TTrain::getId).filter(Objects::nonNull).toList();
+        List<TTrainRecord> trainUsers = tTrainRecordService.lambdaQuery()
+                .in(TTrainRecord::getTrainId, trainIds)
+                .list();
+        return trainUsers.stream()
+                .filter(u -> u.getTrainId() != null)
+                .collect(Collectors.groupingBy(TTrainRecord::getTrainId, Collectors.counting()));
+    }
+
+    private TrainManageItemResVo toManageItem(TTrain train, long driverCount) {
+        TrainManageItemResVo vo = new TrainManageItemResVo();
+        vo.setId(train.getId());
+        vo.setName(train.getName());
+        vo.setStartDate(train.getStartDate());
+        vo.setEndDate(train.getEndDate());
+        vo.setDriverCount((int) driverCount);
+        vo.setStatus(train.getStatus());
+        vo.setStatusName(TrainStatusEnum.getNameByCode(train.getStatus()));
+        vo.setCreateTime(train.getCreateTime());
+        return vo;
+    }
+
+    private TrainDetailResVo toDetail(TTrain train) {
+        TrainDetailResVo vo = new TrainDetailResVo();
+        vo.setId(train.getId());
+        vo.setName(train.getName());
+        vo.setStartDate(train.getStartDate());
+        vo.setEndDate(train.getEndDate());
+        vo.setExamFlag(train.getExamFlag());
+        vo.setDescription(train.getDescription());
+        vo.setStatus(train.getStatus());
+        vo.setStatusName(TrainStatusEnum.getNameByCode(train.getStatus()));
+        vo.setCreateTime(train.getCreateTime());
+        vo.setUpdateTime(train.getUpdateTime());
+        return vo;
+    }
+
+    private TrainDetailResVo.Course toCourseVo(TCourse course) {
+        TrainDetailResVo.Course vo = new TrainDetailResVo.Course();
+        vo.setId(course.getId());
+        vo.setCourseName(course.getCourseName());
+        vo.setCourseType(course.getCourseType());
+        vo.setCourseTypeName(CourseTypeEnum.getNameByCode(course.getCourseType()));
+        vo.setCourseTime(course.getCourseTime());
+        return vo;
+    }
+
+    private TrainDetailResVo.Exam toExamVo(TExam exam) {
+        TrainDetailResVo.Exam vo = new TrainDetailResVo.Exam();
+        vo.setId(exam.getId());
+        vo.setName(exam.getName());
+        vo.setExamDuration(exam.getExamDuration());
+        vo.setPassRate(exam.getPassRate());
+        vo.setAmount(exam.getAmount());
+        return vo;
+    }
+
+    private TrainDetailResVo.DriverProgress toDriverProgress(TTrainRecord record) {
+        TrainDetailResVo.DriverProgress vo = new TrainDetailResVo.DriverProgress();
+        vo.setDriverId(record.getDriverId());
+        vo.setFinishCourse(record.getFinishCourse());
+        vo.setSumCourse(record.getSumCourse());
+        vo.setRightRate(record.getRightRate());
+        vo.setStatus(record.getStatus());
+        vo.setStatusName(TrainStatusEnum.getNameByCode(record.getStatus()));
+        return vo;
+    }
+
+    private static TrainDetailResVo.ProgressSummary buildProgressSummary(List<TrainDetailResVo.DriverProgress> progressList) {
+        int total = progressList == null ? 0 : progressList.size();
+        int finished = (int) progressList.stream().filter(p -> Objects.equals(p.getStatus(), 3)).count();
+        int inProgress = (int) progressList.stream().filter(p -> Objects.equals(p.getStatus(), 2)).count();
+        int notStart = total - finished - inProgress;
+        int progressRate = total == 0 ? 0 : (int) Math.round(finished * 100.0 / total);
+
+        TrainDetailResVo.ProgressSummary summary = new TrainDetailResVo.ProgressSummary();
+        summary.setTotal(total);
+        summary.setFinished(finished);
+        summary.setInProgress(inProgress);
+        summary.setNotStart(notStart);
+        summary.setProgressRate(progressRate);
+        return summary;
+    }
+
+    private void saveRelations(Long trainId, List<Long> courseIds, List<Long> driverIds, Integer examFlag, Long examId) {
+        List<TTrainCourse> trainCourses = courseIds.stream().distinct().map(courseId -> {
+            TTrainCourse tc = new TTrainCourse();
+            tc.setTrainId(trainId);
+            tc.setCourseId(courseId);
+            return tc;
+        }).toList();
+        if (!trainCourses.isEmpty() && !tTrainCourseService.saveBatch(trainCourses)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "保存培训课程失败");
+        }
+
+
+        if (Objects.equals(examFlag, 1)) {
+            TTrainExam te = new TTrainExam();
+            te.setTrainId(trainId);
+            te.setExamId(examId);
+            if (!tTrainExamService.save(te)) {
+                throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "保存培训考试失败");
+            }
+        }
+    }
+
+    private void deleteRelations(Long trainId) {
+        tTrainCourseService.lambdaUpdate().eq(TTrainCourse::getTrainId, trainId).remove();
+        tTrainExamService.lambdaUpdate().eq(TTrainExam::getTrainId, trainId).remove();
+    }
+
+    private void saveTrainRecords(Long entId, Long trainId, List<Long> driverIds, int sumCourse, Long userId, LocalDateTime now) {
+        List<TTrainRecord> records = driverIds.stream().distinct().map(driverId -> {
+            TTrainRecord record = new TTrainRecord();
+            record.setEntId(entId);
+            record.setTrainId(trainId);
+            record.setDriverId(driverId);
+            record.setFinishCourse(0);
+            record.setSumCourse(sumCourse);
+            record.setRightRate(null);
+            record.setStatus(1);
+            record.setCreateBy(userId);
+            record.setCreateTime(now);
+            record.setUpdateBy(userId);
+            record.setUpdateTime(now);
+            record.setDelFlag(Global.UN_DELETED);
+            return record;
+        }).toList();
+        if (!records.isEmpty() && !tTrainRecordService.saveBatch(records)) {
+            throw new SystemException(HttpStatus.CRUD_FAIL_CODE, "保存培训记录失败");
+        }
+    }
+
+    private void logicDeleteTrainRecords(Long trainId, Long userId, LocalDateTime now) {
+        tTrainRecordService.lambdaUpdate()
+                .eq(TTrainRecord::getTrainId, trainId)
+                .eq(TTrainRecord::getDelFlag, Global.UN_DELETED)
+                .set(TTrainRecord::getDelFlag, Global.DELETED)
+                .set(TTrainRecord::getUpdateBy, userId)
+                .set(TTrainRecord::getUpdateTime, now)
+                .update();
+    }
+
+    private static int calcTrainStatus(LocalDate startDate, LocalDate endDate) {
+        if (startDate == null || endDate == null) {
+            return 1;
+        }
+        LocalDate today = LocalDate.now();
+        if (today.isBefore(startDate)) {
+            return 1;
+        }
+        if (!today.isAfter(endDate)) {
+            return 2;
+        }
+        return 3;
+    }
+
+    private static String normalizeNullable(String value) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+
+    private static List<Long> parseIds(String idsStr, Long singleId) {
+        if (singleId != null) {
+            return List.of(singleId);
+        }
+        if (StrUtil.isBlank(idsStr)) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(idsStr.split(","))
+                .map(String::trim)
+                .filter(StrUtil::isNotBlank)
+                .map(Long::parseLong)
+                .distinct()
+                .toList();
+    }
+
+    private static void validateSaveReq(TrainSaveReqVo reqVo, boolean requireId) {
+        if (reqVo == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "参数不能为空");
+        }
+        if (requireId && reqVo.getId() == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, HttpStatus.ID_MISSING);
+        }
+        if (StrUtil.isBlank(reqVo.getName())) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "培训主题不能为空");
+        }
+        if (reqVo.getStartDate() == null || reqVo.getEndDate() == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "培训时间不能为空");
+        }
+        if (reqVo.getEndDate().isBefore(reqVo.getStartDate())) {
+            throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "结束时间不能早于开始时间");
+        }
+        if (reqVo.getExamFlag() == null || (reqVo.getExamFlag() != 0 && reqVo.getExamFlag() != 1)) {
+            throw new SystemException(HttpStatus.PARAMETERS_PATTERN_ERROR_CODE, "examFlag参数非法");
+        }
+        if (CollUtil.isEmpty(reqVo.getCourseIds())) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "课程不能为空");
+        }
+        if (CollUtil.isEmpty(reqVo.getDriverIds())) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "司机不能为空");
+        }
+        if (Objects.equals(reqVo.getExamFlag(), 1) && reqVo.getExamId() == null) {
+            throw new SystemException(HttpStatus.PARAMETERS_MISSING_CODE, "请选择考试");
+        }
+    }
+
+
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TCourseRecordService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.model.TCourseRecord;
+import com.sckw.system.dao.TCourseRecordMapper;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TCourseRecordService extends ServiceImpl<TCourseRecordMapper, TCourseRecord> {
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TCourseService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.dao.TCourseMapper;
+import com.sckw.system.model.TCourse;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TCourseService extends ServiceImpl<TCourseMapper, TCourse> {
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TExamLogService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.model.TExamLog;
+import com.sckw.system.dao.TExamLogMapper;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TExamLogService extends ServiceImpl<TExamLogMapper, TExamLog> {
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TExamRecordService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.dao.TExamRecordMapper;
+import com.sckw.system.model.TExamRecord;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TExamRecordService extends ServiceImpl<TExamRecordMapper, TExamRecord> {
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TExamService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.dao.TExamMapper;
+import com.sckw.system.model.TExam;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TExamService extends ServiceImpl<TExamMapper, TExam> {
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TQuestionItemService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.dao.TQuestionItemMapper;
+import com.sckw.system.model.TQuestionItem;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TQuestionItemService extends ServiceImpl<TQuestionItemMapper, TQuestionItem> {
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TQuestionService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.model.TQuestion;
+import com.sckw.system.dao.TQuestionMapper;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TQuestionService extends ServiceImpl<TQuestionMapper, TQuestion> {
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TTrainCourseService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.dao.TTrainCourseMapper;
+import com.sckw.system.model.TTrainCourse;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TTrainCourseService extends ServiceImpl<TTrainCourseMapper, TTrainCourse> {
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TTrainExamService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.dao.TTrainExamMapper;
+import com.sckw.system.model.TTrainExam;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TTrainExamService extends ServiceImpl<TTrainExamMapper, TTrainExam> {
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TTrainRecordService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.model.TTrainRecord;
+import com.sckw.system.dao.TTrainRecordMapper;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TTrainRecordService extends ServiceImpl<TTrainRecordMapper, TTrainRecord> {
+
+}

+ 16 - 0
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/TTrainService.java

@@ -0,0 +1,16 @@
+package com.sckw.system.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sckw.system.dao.TTrainMapper;
+import com.sckw.system.model.TTrain;
+/**
+* @date 2026-06-09 10:50:04
+* @author xucaiqin
+*/
+@Service
+public class TTrainService extends ServiceImpl<TTrainMapper, TTrain> {
+
+}

+ 2 - 2
sckw-modules/sckw-system/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true

+ 27 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TCourseMapper.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TCourseMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TCourse">
+    <!--@mbg.generated-->
+    <!--@Table t_course-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="ent_id" jdbcType="BIGINT" property="entId" />
+    <result column="course_name" jdbcType="VARCHAR" property="courseName" />
+    <result column="course_type" jdbcType="TINYINT" property="courseType" />
+    <result column="course_time" jdbcType="INTEGER" property="courseTime" />
+    <result column="content" jdbcType="LONGVARCHAR" property="content" />
+    <result column="file_url" jdbcType="VARCHAR" property="fileUrl" />
+    <result column="description" jdbcType="LONGVARCHAR" property="description" />
+    <result column="status" jdbcType="TINYINT" property="status" />
+    <result column="create_by" jdbcType="BIGINT" property="createBy" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+    <result column="update_by" jdbcType="BIGINT" property="updateBy" />
+    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
+    <result column="del_flag" jdbcType="INTEGER" property="delFlag" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, ent_id, course_name, course_type, course_time, content, file_url, description, 
+    `status`, create_by, create_time, update_by, update_time, del_flag
+  </sql>
+</mapper>

+ 25 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TCourseRecordMapper.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TCourseRecordMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TCourseRecord">
+    <!--@mbg.generated-->
+    <!--@Table t_course_record-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="train_id" jdbcType="BIGINT" property="trainId" />
+    <result column="course_id" jdbcType="BIGINT" property="courseId" />
+    <result column="driver_id" jdbcType="BIGINT" property="driverId" />
+    <result column="start_time" jdbcType="TIMESTAMP" property="startTime" />
+    <result column="end_time" jdbcType="TIMESTAMP" property="endTime" />
+    <result column="status" jdbcType="TINYINT" property="status" />
+    <result column="create_by" jdbcType="BIGINT" property="createBy" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+    <result column="update_by" jdbcType="BIGINT" property="updateBy" />
+    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
+    <result column="del_flag" jdbcType="INTEGER" property="delFlag" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, train_id, course_id, driver_id, start_time, end_time, `status`, create_by, create_time, 
+    update_by, update_time, del_flag
+  </sql>
+</mapper>

+ 28 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TExamLogMapper.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TExamLogMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TExamLog">
+    <!--@mbg.generated-->
+    <!--@Table t_exam_log-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="train_id" jdbcType="BIGINT" property="trainId" />
+    <result column="exam_id" jdbcType="BIGINT" property="examId" />
+    <result column="driver_id" jdbcType="BIGINT" property="driverId" />
+    <result column="exam_flag" jdbcType="INTEGER" property="examFlag" />
+    <result column="start_time" jdbcType="TIMESTAMP" property="startTime" />
+    <result column="end_time" jdbcType="TIMESTAMP" property="endTime" />
+    <result column="right_amount" jdbcType="INTEGER" property="rightAmount" />
+    <result column="amount" jdbcType="INTEGER" property="amount" />
+    <result column="right_rate" jdbcType="INTEGER" property="rightRate" />
+    <result column="create_by" jdbcType="BIGINT" property="createBy" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+    <result column="update_by" jdbcType="BIGINT" property="updateBy" />
+    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
+    <result column="del_flag" jdbcType="INTEGER" property="delFlag" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, train_id, exam_id, driver_id, exam_flag, start_time, end_time, right_amount, 
+    amount, right_rate, create_by, create_time, update_by, update_time, del_flag
+  </sql>
+</mapper>

+ 26 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TExamMapper.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TExamMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TExam">
+    <!--@mbg.generated-->
+    <!--@Table t_exam-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="ent_id" jdbcType="BIGINT" property="entId" />
+    <result column="name" jdbcType="VARCHAR" property="name" />
+    <result column="exam_duration" jdbcType="INTEGER" property="examDuration" />
+    <result column="pass_rate" jdbcType="INTEGER" property="passRate" />
+    <result column="amount" jdbcType="INTEGER" property="amount" />
+    <result column="description" jdbcType="VARCHAR" property="description" />
+    <result column="status" jdbcType="INTEGER" property="status" />
+    <result column="create_by" jdbcType="BIGINT" property="createBy" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+    <result column="update_by" jdbcType="BIGINT" property="updateBy" />
+    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
+    <result column="del_flag" jdbcType="INTEGER" property="delFlag" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, ent_id, `name`, exam_duration, pass_rate, amount, description, `status`, create_by, 
+    create_time, update_by, update_time, del_flag
+  </sql>
+</mapper>

+ 23 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TExamRecordMapper.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TExamRecordMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TExamRecord">
+    <!--@mbg.generated-->
+    <!--@Table t_exam_record-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="log_id" jdbcType="BIGINT" property="logId" />
+    <result column="question_id" jdbcType="BIGINT" property="questionId" />
+    <result column="answer" jdbcType="VARCHAR" property="answer" />
+    <result column="correct_flag" jdbcType="INTEGER" property="correctFlag" />
+    <result column="create_by" jdbcType="BIGINT" property="createBy" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+    <result column="update_by" jdbcType="BIGINT" property="updateBy" />
+    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
+    <result column="del_flag" jdbcType="INTEGER" property="delFlag" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, log_id, question_id, answer, correct_flag, create_by, create_time, update_by, 
+    update_time, del_flag
+  </sql>
+</mapper>

+ 25 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TQuestionItemMapper.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TQuestionItemMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TQuestionItem">
+    <!--@mbg.generated-->
+    <!--@Table t_question_item-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="ent_id" jdbcType="BIGINT" property="entId" />
+    <result column="question_id" jdbcType="BIGINT" property="questionId" />
+    <result column="name" jdbcType="VARCHAR" property="name" />
+    <result column="content" jdbcType="VARCHAR" property="content" />
+    <result column="answer_flag" jdbcType="INTEGER" property="answerFlag" />
+    <result column="sort" jdbcType="INTEGER" property="sort" />
+    <result column="create_by" jdbcType="BIGINT" property="createBy" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+    <result column="update_by" jdbcType="BIGINT" property="updateBy" />
+    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
+    <result column="del_flag" jdbcType="INTEGER" property="delFlag" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, ent_id, question_id, `name`, content, answer_flag, sort, create_by, create_time, 
+    update_by, update_time, del_flag
+  </sql>
+</mapper>

+ 23 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TQuestionMapper.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TQuestionMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TQuestion">
+    <!--@mbg.generated-->
+    <!--@Table t_question-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="ent_id" jdbcType="BIGINT" property="entId" />
+    <result column="exam_id" jdbcType="BIGINT" property="examId" />
+    <result column="name" jdbcType="VARCHAR" property="name" />
+    <result column="type" jdbcType="TINYINT" property="type" />
+    <result column="create_by" jdbcType="BIGINT" property="createBy" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+    <result column="update_by" jdbcType="BIGINT" property="updateBy" />
+    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
+    <result column="del_flag" jdbcType="INTEGER" property="delFlag" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, ent_id, exam_id, `name`, `type`, create_by, create_time, update_by, update_time, 
+    del_flag
+  </sql>
+</mapper>

+ 15 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TTrainCourseMapper.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TTrainCourseMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TTrainCourse">
+    <!--@mbg.generated-->
+    <!--@Table t_train_course-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="train_id" jdbcType="BIGINT" property="trainId" />
+    <result column="course_id" jdbcType="BIGINT" property="courseId" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, train_id, course_id
+  </sql>
+</mapper>

+ 15 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TTrainExamMapper.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TTrainExamMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TTrainExam">
+    <!--@mbg.generated-->
+    <!--@Table t_train_exam-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="train_id" jdbcType="BIGINT" property="trainId" />
+    <result column="exam_id" jdbcType="BIGINT" property="examId" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, train_id, exam_id
+  </sql>
+</mapper>

+ 26 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TTrainMapper.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TTrainMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TTrain">
+    <!--@mbg.generated-->
+    <!--@Table t_train-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="ent_id" jdbcType="BIGINT" property="entId" />
+    <result column="name" jdbcType="VARCHAR" property="name" />
+    <result column="start_date" jdbcType="DATE" property="startDate" />
+    <result column="end_date" jdbcType="DATE" property="endDate" />
+    <result column="exam_flag" jdbcType="TINYINT" property="examFlag" />
+    <result column="description" jdbcType="VARCHAR" property="description" />
+    <result column="status" jdbcType="INTEGER" property="status" />
+    <result column="create_by" jdbcType="BIGINT" property="createBy" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+    <result column="update_by" jdbcType="BIGINT" property="updateBy" />
+    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
+    <result column="del_flag" jdbcType="INTEGER" property="delFlag" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, ent_id, `name`, start_date, end_date, exam_flag, description, `status`, create_by, 
+    create_time, update_by, update_time, del_flag
+  </sql>
+</mapper>

+ 26 - 0
sckw-modules/sckw-system/src/main/resources/mapper/TTrainRecordMapper.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.sckw.system.dao.TTrainRecordMapper">
+  <resultMap id="BaseResultMap" type="com.sckw.system.model.TTrainRecord">
+    <!--@mbg.generated-->
+    <!--@Table t_train_record-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="ent_id" jdbcType="BIGINT" property="entId" />
+    <result column="train_id" jdbcType="BIGINT" property="trainId" />
+    <result column="driver_id" jdbcType="BIGINT" property="driverId" />
+    <result column="finish_course" jdbcType="INTEGER" property="finishCourse" />
+    <result column="sum_course" jdbcType="INTEGER" property="sumCourse" />
+    <result column="right_rate" jdbcType="INTEGER" property="rightRate" />
+    <result column="status" jdbcType="INTEGER" property="status" />
+    <result column="create_by" jdbcType="BIGINT" property="createBy" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+    <result column="update_by" jdbcType="BIGINT" property="updateBy" />
+    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
+    <result column="del_flag" jdbcType="INTEGER" property="delFlag" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, ent_id, train_id, driver_id, finish_course, sum_course, right_rate, `status`, 
+    create_by, create_time, update_by, update_time, del_flag
+  </sql>
+</mapper>

+ 2 - 2
sckw-modules/sckw-transport/src/main/resources/bootstrap-prod.yml

@@ -19,10 +19,10 @@ spring:
         file-extension: yaml
         shared-configs:
           - data-id: sckw-common.yml
-            group: sckw-common
+            group: sckw-ng-common
             refresh: true
         #可以读多个配置文件 需要在同一个命名空间下面可以是不同的组
         extension-configs:
           - dataId: sckw-common.yml
             group: sckw-ng-service-platform
-            refresh: true
+            refresh: true