From f0caa491330fbbf1a09afd75cf40381d708aeb48 Mon Sep 17 00:00:00 2001 From: tangweijie <877588133@qq.com> Date: Sat, 24 Jan 2026 10:55:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=97=AE=E5=8D=B7?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增问卷任务CRUD功能(controller、service、dal) - 新增问卷任务统计相关VO(进度、区域对比、汇总统计) - 新增问卷记录状态枚举 - 更新问卷记录DO结构 - 更新dashboard服务实现 - 新增lombok依赖 --- .../PrisonQuestionnaireTaskController.java | 187 +++++++ .../vo/AreaComparisonRespVO.java | 33 ++ .../vo/QuestionnaireTaskCreateReqVO.java | 49 ++ .../vo/QuestionnaireTaskPageReqVO.java | 29 + .../vo/QuestionnaireTaskRespVO.java | 67 +++ .../vo/QuestionnaireTaskUpdateReqVO.java | 33 ++ .../vo/TaskAreaStatisticsRespVO.java | 60 ++ .../vo/TaskProgressRespVO.java | 75 +++ .../vo/TaskStatisticsSummaryRespVO.java | 33 ++ .../QuestionnaireTaskConvert.java | 24 + .../QuestionnaireTaskDO.java | 117 ++++ .../QuestionnaireRecordDO.java | 15 + .../QuestionnaireTaskMapper.java | 59 ++ .../prison/enums/ErrorCodeConstants.java | 9 + .../QuestionnaireRecordStatusEnum.java | 33 ++ .../impl/PrisonDashboardServiceImpl.java | 4 +- .../QuestionnaireTaskService.java | 149 +++++ .../QuestionnaireTaskServiceImpl.java | 517 ++++++++++++++++++ yudao-module-system/pom.xml | 7 +- 19 files changed, 1495 insertions(+), 5 deletions(-) create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/PrisonQuestionnaireTaskController.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/AreaComparisonRespVO.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskCreateReqVO.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskPageReqVO.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskRespVO.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskUpdateReqVO.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskAreaStatisticsRespVO.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskProgressRespVO.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskStatisticsSummaryRespVO.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/convert/questionnaire_task/QuestionnaireTaskConvert.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/dataobject/questionnaire_task/QuestionnaireTaskDO.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/questionnaire_task/QuestionnaireTaskMapper.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/enums/questionnaire/QuestionnaireRecordStatusEnum.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/questionnaire_task/QuestionnaireTaskService.java create mode 100644 yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/questionnaire_task/QuestionnaireTaskServiceImpl.java diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/PrisonQuestionnaireTaskController.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/PrisonQuestionnaireTaskController.java new file mode 100644 index 0000000000..1bb7e02186 --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/PrisonQuestionnaireTaskController.java @@ -0,0 +1,187 @@ +package cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task; + +import cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo.*; +import cn.iocoder.yudao.module.prison.convert.questionnaire_task.QuestionnaireTaskConvert; +import cn.iocoder.yudao.module.prison.dal.dataobject.questionnaire_task.QuestionnaireTaskDO; +import cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO; +import cn.iocoder.yudao.module.prison.service.questionnaire_task.QuestionnaireTaskService; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 问卷任务") +@RestController +@RequestMapping("/prison/questionnaire-task") +@Validated +public class PrisonQuestionnaireTaskController { + + @Resource + private QuestionnaireTaskService questionnaireTaskService; + + // ==================== 基础 CRUD ==================== + + @PostMapping("/create") + @Operation(summary = "创建问卷任务") + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:create')") + public CommonResult createQuestionnaireTask(@Valid @RequestBody QuestionnaireTaskCreateReqVO createReqVO) { + return success(questionnaireTaskService.createQuestionnaireTask(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新问卷任务") + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:update')") + public CommonResult updateQuestionnaireTask(@Valid @RequestBody QuestionnaireTaskUpdateReqVO updateReqVO) { + questionnaireTaskService.updateQuestionnaireTask(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除问卷任务") + @Parameter(name = "id", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:delete')") + public CommonResult deleteQuestionnaireTask(@NotNull(message = "任务ID不能为空") @RequestParam("id") Long id) { + questionnaireTaskService.deleteQuestionnaireTask(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除问卷任务") + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:delete')") + public CommonResult deleteQuestionnaireTaskList(@NotEmpty(message = "任务ID列表不能为空") @RequestParam("ids") List ids) { + questionnaireTaskService.deleteQuestionnaireTaskListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得问卷任务") + @Parameter(name = "id", description = "任务ID", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:query')") + public CommonResult getQuestionnaireTask(@NotNull(message = "任务ID不能为空") @RequestParam("id") Long id) { + QuestionnaireTaskDO task = questionnaireTaskService.getQuestionnaireTask(id); + return success(QuestionnaireTaskConvert.INSTANCE.convert(task)); + } + + @GetMapping("/page") + @Operation(summary = "获得问卷任务分页") + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:query')") + public CommonResult> getQuestionnaireTaskPage(@Valid QuestionnaireTaskPageReqVO pageReqVO) { + PageResult pageResult = questionnaireTaskService.getQuestionnaireTaskPage(pageReqVO); + return success(QuestionnaireTaskConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出问卷任务 Excel") + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportQuestionnaireTaskExcel(@Valid QuestionnaireTaskPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = questionnaireTaskService.getQuestionnaireTaskPage(pageReqVO).getList(); + ExcelUtils.write(response, "问卷任务.xls", "数据", QuestionnaireTaskRespVO.class, + QuestionnaireTaskConvert.INSTANCE.convertList(list)); + } + + // ==================== 任务执行相关 ==================== + + @PostMapping("/cancel") + @Operation(summary = "取消任务") + @Parameter(name = "id", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:cancel')") + public CommonResult cancelTask(@NotNull(message = "任务ID不能为空") @RequestParam("id") Long id) { + questionnaireTaskService.cancelTask(id); + return success(true); + } + + @PostMapping("/finish") + @Operation(summary = "结束任务") + @Parameter(name = "id", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:finish')") + public CommonResult finishTask(@NotNull(message = "任务ID不能为空") @RequestParam("id") Long id) { + questionnaireTaskService.finishTask(id); + return success(true); + } + + @PostMapping("/restart") + @Operation(summary = "重新开始任务") + @Parameter(name = "id", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:restart')") + public CommonResult restartTask(@NotNull(message = "任务ID不能为空") @RequestParam("id") Long id) { + questionnaireTaskService.restartTask(id); + return success(true); + } + + // ==================== 进度跟踪相关 ==================== + + @GetMapping("/progress") + @Operation(summary = "获取任务进度") + @Parameter(name = "id", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:query')") + public CommonResult getTaskProgress(@NotNull(message = "任务ID不能为空") @RequestParam("id") Long id) { + return success(questionnaireTaskService.getTaskProgress(id)); + } + + @GetMapping("/pending-prisoners") + @Operation(summary = "获取任务未完成人员") + @Parameter(name = "id", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:query')") + public CommonResult> getPendingPrisoners( + @NotNull(message = "任务ID不能为空") @RequestParam("id") Long id, + @Valid PageParam pageReqVO) { + return success(questionnaireTaskService.getPendingPrisoners(id, pageReqVO)); + } + + @PostMapping("/remind") + @Operation(summary = "提醒未完成人员") + @Parameter(name = "id", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:remind')") + public CommonResult remindPendingPrisoners(@NotNull(message = "任务ID不能为空") @RequestParam("id") Long id) { + return success(questionnaireTaskService.remindPendingPrisoners(id)); + } + + // ==================== 统计相关 ==================== + + @GetMapping("/area-statistics") + @Operation(summary = "按监区统计任务完成情况") + @Parameter(name = "id", description = "任务ID", required = true) + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:query')") + public CommonResult> getTaskAreaStatistics( + @NotNull(message = "任务ID不能为空") @RequestParam("id") Long id) { + return success(questionnaireTaskService.getTaskAreaStatistics(id)); + } + + @GetMapping("/statistics-summary") + @Operation(summary = "获取全局任务统计汇总") + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:query')") + public CommonResult getStatisticsSummary() { + return success(questionnaireTaskService.getStatisticsSummary()); + } + + @GetMapping("/area-comparison") + @Operation(summary = "按监区对比分析") + @PreAuthorize("@ss.hasPermission('prison:questionnaire-task:query')") + public CommonResult> compareAreasByQuestionnaire( + @RequestParam(value = "questionnaireId", required = false) Long questionnaireId, + @RequestParam(value = "areaIds", required = false) List areaIds) { + return success(questionnaireTaskService.compareAreasByQuestionnaire(questionnaireId, areaIds)); + } + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/AreaComparisonRespVO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/AreaComparisonRespVO.java new file mode 100644 index 0000000000..1c68f592ab --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/AreaComparisonRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.math.BigDecimal; + +/** + * 管理后台 - 按监区对比分析 Response VO + * + * @author xlcp + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AreaComparisonRespVO { + + @Schema(description = "任务ID", example = "18966") + private Long taskId; + + @Schema(description = "任务名称", example = "一月心理测评") + private String taskName; + + @Schema(description = "目标总人数") + private Integer totalCount; + + @Schema(description = "已完成人数") + private Integer completedCount; + + @Schema(description = "完成率(%)") + private BigDecimal completionRate; + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskCreateReqVO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskCreateReqVO.java new file mode 100644 index 0000000000..f3ec941fd1 --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskCreateReqVO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import jakarta.validation.constraints.*; +import java.util.*; +import java.time.LocalDateTime; + +/** + * 管理后台 - 问卷任务创建 Request VO + * + * @author xlcp + */ +@Data +@EqualsAndHashCode +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionnaireTaskCreateReqVO { + + @Schema(description = "任务名称", required = true, example = "一月心理测评") + @NotBlank(message = "任务名称不能为空") + private String taskName; + + @Schema(description = "问卷ID", required = true, example = "18966") + @NotNull(message = "问卷ID不能为空") + private Long questionnaireId; + + @Schema(description = "目标类型:1-指定犯人 2-指定监区 3-全部犯人", required = true, example = "2") + @NotNull(message = "目标类型不能为空") + private Integer targetType; + + @Schema(description = "犯人ID列表(当targetType=1时)") + private List prisonerIds; + + @Schema(description = "监区ID(当targetType=2时)") + private Long areaId; + + @Schema(description = "任务开始时间") + private LocalDateTime startTime; + + @Schema(description = "截止时间", required = true) + @NotNull(message = "截止时间不能为空") + private LocalDateTime deadline; + + @Schema(description = "备注", example = "一月度常规心理测评") + private String remark; + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskPageReqVO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskPageReqVO.java new file mode 100644 index 0000000000..553487219e --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskPageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 问卷任务分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class QuestionnaireTaskPageReqVO extends PageParam { + + @Schema(description = "任务名称", example = "测试任务") + private String taskName; + + @Schema(description = "问卷ID", example = "1") + private Long questionnaireId; + + @Schema(description = "状态:1-未开始 2-进行中 3-已完成 4-已取消", example = "2") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskRespVO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskRespVO.java new file mode 100644 index 0000000000..c3a0afbed8 --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskRespVO.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 管理后台 - 问卷任务详情 Response VO + * + * @author xlcp + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionnaireTaskRespVO { + + @Schema(description = "任务ID", example = "18966") + private Long id; + + @Schema(description = "任务名称", example = "一月心理测评") + private String taskName; + + @Schema(description = "问卷ID", example = "18966") + private Long questionnaireId; + + @Schema(description = "问卷名称", example = "心理评估问卷") + private String questionnaireName; + + @Schema(description = "目标类型:1-指定犯人 2-指定监区 3-全部犯人", example = "2") + private Integer targetType; + + @Schema(description = "监区ID", example = "456") + private Long areaId; + + @Schema(description = "监区名称", example = "一监区") + private String areaName; + + @Schema(description = "目标总人数") + private Integer totalCount; + + @Schema(description = "已完成人数") + private Integer completedCount; + + @Schema(description = "待完成人数") + private Integer pendingCount; + + @Schema(description = "完成率(%)") + private BigDecimal completionRate; + + @Schema(description = "状态:1-草稿 2-进行中 3-已结束 4-已取消", example = "2") + private Integer status; + + @Schema(description = "任务开始时间") + private LocalDateTime startTime; + + @Schema(description = "截止时间") + private LocalDateTime deadline; + + @Schema(description = "备注", example = "一月度常规心理测评") + private String remark; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskUpdateReqVO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskUpdateReqVO.java new file mode 100644 index 0000000000..235fc9b556 --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/QuestionnaireTaskUpdateReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import jakarta.validation.constraints.*; +import java.time.LocalDateTime; + +/** + * 管理后台 - 问卷任务更新 Request VO + * + * @author xlcp + */ +@Data +@EqualsAndHashCode +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionnaireTaskUpdateReqVO { + + @Schema(description = "任务ID", required = true, example = "18966") + @NotNull(message = "任务ID不能为空") + private Long id; + + @Schema(description = "任务名称", example = "一月心理测评") + private String taskName; + + @Schema(description = "截止时间") + private LocalDateTime deadline; + + @Schema(description = "备注", example = "一月度常规心理测评") + private String remark; + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskAreaStatisticsRespVO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskAreaStatisticsRespVO.java new file mode 100644 index 0000000000..1476e2758b --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskAreaStatisticsRespVO.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.math.BigDecimal; +import java.util.List; + +/** + * 管理后台 - 任务按监区统计 Response VO + * + * @author xlcp + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskAreaStatisticsRespVO { + + @Schema(description = "监区ID", example = "456") + private Long areaId; + + @Schema(description = "监区名称", example = "一监区") + private String areaName; + + @Schema(description = "目标总人数") + private Integer totalCount; + + @Schema(description = "已完成人数") + private Integer completedCount; + + @Schema(description = "完成率(%)") + private BigDecimal completionRate; + + @Schema(description = "平均分") + private BigDecimal avgScore; + + @Schema(description = "及格率(%)") + private BigDecimal passRate; + + @Schema(description = "风险分布") + private RiskDistribution riskDistribution; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RiskDistribution { + + @Schema(description = "高风险人数") + private Integer highRisk; + + @Schema(description = "中风险人数") + private Integer mediumRisk; + + @Schema(description = "低风险人数") + private Integer lowRisk; + + } + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskProgressRespVO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskProgressRespVO.java new file mode 100644 index 0000000000..55de07e3fa --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskProgressRespVO.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 管理后台 - 任务进度详情 Response VO + * + * @author xlcp + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskProgressRespVO { + + @Schema(description = "任务ID", example = "18966") + private Long taskId; + + @Schema(description = "任务名称", example = "一月心理测评") + private String taskName; + + @Schema(description = "问卷名称", example = "心理评估问卷") + private String questionnaireName; + + @Schema(description = "状态:1-草稿 2-进行中 3-已结束 4-已取消", example = "2") + private Integer status; + + @Schema(description = "任务开始时间") + private LocalDateTime startTime; + + @Schema(description = "截止时间") + private LocalDateTime deadline; + + @Schema(description = "目标总人数") + private Integer totalCount; + + @Schema(description = "已完成人数") + private Integer completedCount; + + @Schema(description = "待完成人数") + private Integer pendingCount; + + @Schema(description = "完成率(%)") + private BigDecimal completionRate; + + @Schema(description = "状态分布") + private StatusBreakdown statusBreakdown; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class StatusBreakdown { + + @Schema(description = "待测评人数") + private Integer pending; + + @Schema(description = "测评中人数") + private Integer inProgress; + + @Schema(description = "已完成人数") + private Integer completed; + + @Schema(description = "已过期人数") + private Integer expired; + + @Schema(description = "已取消人数") + private Integer cancelled; + + } + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskStatisticsSummaryRespVO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskStatisticsSummaryRespVO.java new file mode 100644 index 0000000000..a51f9ddd2b --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/questionnaire_task/vo/TaskStatisticsSummaryRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.math.BigDecimal; + +/** + * 管理后台 - 任务统计汇总 Response VO + * + * @author xlcp + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskStatisticsSummaryRespVO { + + @Schema(description = "任务总数") + private Integer taskCount; + + @Schema(description = "目标总人数") + private Integer totalPrisoners; + + @Schema(description = "已完成人数") + private Integer totalCompleted; + + @Schema(description = "待完成人数") + private Integer totalPending; + + @Schema(description = "整体完成率(%)") + private BigDecimal overallCompletionRate; + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/convert/questionnaire_task/QuestionnaireTaskConvert.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/convert/questionnaire_task/QuestionnaireTaskConvert.java new file mode 100644 index 0000000000..48de7d57a3 --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/convert/questionnaire_task/QuestionnaireTaskConvert.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.prison.convert.questionnaire_task; + +import cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo.*; +import cn.iocoder.yudao.module.prison.dal.dataobject.questionnaire_task.QuestionnaireTaskDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface QuestionnaireTaskConvert { + + QuestionnaireTaskConvert INSTANCE = Mappers.getMapper(QuestionnaireTaskConvert.class); + + QuestionnaireTaskDO convert(QuestionnaireTaskUpdateReqVO bean); + + QuestionnaireTaskRespVO convert(QuestionnaireTaskDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/dataobject/questionnaire_task/QuestionnaireTaskDO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/dataobject/questionnaire_task/QuestionnaireTaskDO.java new file mode 100644 index 0000000000..6acba90c3a --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/dataobject/questionnaire_task/QuestionnaireTaskDO.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.module.prison.dal.dataobject.questionnaire_task; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; + +/** + * 问卷任务 DO + * + * @author xlcp + */ +@TableName("prison_questionnaire_task") +@KeySequence("prison_questionnaire_task_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionnaireTaskDO extends TenantBaseDO { + + /** + * 任务ID + */ + @TableId + private Long id; + + /** + * 任务名称 + */ + private String taskName; + + // ==================== 问卷信息 ==================== + + /** + * 问卷ID + */ + private Long questionnaireId; + + /** + * 问卷名称 + */ + private String questionnaireName; + + // ==================== 目标范围 ==================== + + /** + * 目标类型:1-指定犯人 2-指定监区 3-全部犯人 + */ + private Integer targetType; + + /** + * 监区ID(当targetType=2时) + */ + private Long areaId; + + /** + * 监区名称 + */ + private String areaName; + + /** + * 犯人ID列表(当targetType=1时,JSON格式) + */ + private String prisonerIds; + + // ==================== 时间设置 ==================== + + /** + * 任务开始时间 + */ + private LocalDateTime startTime; + + /** + * 截止时间 + */ + private LocalDateTime deadline; + + // ==================== 任务状态 ==================== + + /** + * 状态:1-草稿 2-进行中 3-已结束 4-已取消 + */ + private Integer status; + + // ==================== 统计信息 ==================== + + /** + * 目标总人数 + */ + private Integer totalCount; + + /** + * 已完成人数 + */ + private Integer completedCount; + + /** + * 待完成人数 + */ + private Integer pendingCount; + + /** + * 完成率(%) + */ + private java.math.BigDecimal completionRate; + + // ==================== 备注 ==================== + + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/dataobject/questionnairerecord/QuestionnaireRecordDO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/dataobject/questionnairerecord/QuestionnaireRecordDO.java index 6023c428c4..410da48e1f 100644 --- a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/dataobject/questionnairerecord/QuestionnaireRecordDO.java +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/dataobject/questionnairerecord/QuestionnaireRecordDO.java @@ -54,6 +54,21 @@ public class QuestionnaireRecordDO extends TenantBaseDO { */ private String prisonerName; + // ==================== 任务关联 ==================== + + /** + * 所属任务ID + */ + private Long taskId; + /** + * 犯人所属监区ID + */ + private Long prisonerAreaId; + /** + * 犯人所属监区名称 + */ + private String prisonerAreaName; + // ==================== 测评状态 ==================== /** diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/questionnaire_task/QuestionnaireTaskMapper.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/questionnaire_task/QuestionnaireTaskMapper.java new file mode 100644 index 0000000000..c1f043fb1b --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/questionnaire_task/QuestionnaireTaskMapper.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.prison.dal.mysql.questionnaire_task; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.prison.dal.dataobject.questionnaire_task.QuestionnaireTaskDO; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +/** + * 问卷任务 Mapper + * + * @author xlcp + */ +@Mapper +public interface QuestionnaireTaskMapper extends BaseMapperX { + + default PageResult selectPage(QuestionnaireTaskPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(QuestionnaireTaskDO::getTaskName, pageReqVO.getTaskName()) + .eqIfPresent(QuestionnaireTaskDO::getStatus, pageReqVO.getStatus()) + .eqIfPresent(QuestionnaireTaskDO::getQuestionnaireId, pageReqVO.getQuestionnaireId()) + .betweenIfPresent(QuestionnaireTaskDO::getCreateTime, pageReqVO.getCreateTime()) + .orderByDesc(QuestionnaireTaskDO::getCreateTime)); + } + + @Select("SELECT qt.*, q.title as questionnaire_title FROM prison_questionnaire_task qt " + + "LEFT JOIN prison_questionnaire q ON qt.questionnaire_id = q.id " + + "WHERE qt.id = #{id}") + Map selectTaskDetailById(@Param("id") Long id); + + @Select("UPDATE prison_questionnaire_task qt " + + "SET total_count = (SELECT COUNT(*) FROM prison_questionnaire_record r WHERE r.task_id = #{taskId}), " + + "completed_count = (SELECT COUNT(*) FROM prison_questionnaire_record r WHERE r.task_id = #{taskId} AND r.status = 3), " + + "pending_count = (SELECT COUNT(*) FROM prison_questionnaire_record r WHERE r.task_id = #{taskId} AND r.status IN (1, 2)), " + + "completion_rate = CASE " + + "WHEN (SELECT COUNT(*) FROM prison_questionnaire_record r WHERE r.task_id = #{taskId}) > 0 " + + "THEN (SELECT COUNT(*) FROM prison_questionnaire_record r WHERE r.task_id = #{taskId} AND r.status = 3) * 100.0 / " + + "(SELECT COUNT(*) FROM prison_questionnaire_record r WHERE r.task_id = #{taskId}) " + + "ELSE 0 END, " + + "update_time = NOW() " + + "WHERE qt.id = #{taskId}") + void updateTaskStatistics(@Param("taskId") Long taskId); + + @Select("SELECT COUNT(*) as task_count, " + + "SUM(total_count) as total_prisoners, " + + "SUM(completed_count) as total_completed, " + + "AVG(completion_rate) as avg_completion_rate " + + "FROM prison_questionnaire_task " + + "WHERE deleted = 0 AND status IN (2, 3)") + Map selectTaskStatisticsSummary(); + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/enums/ErrorCodeConstants.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/enums/ErrorCodeConstants.java index 204591de15..d2fad0ce19 100644 --- a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/enums/ErrorCodeConstants.java +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/enums/ErrorCodeConstants.java @@ -89,4 +89,13 @@ public class ErrorCodeConstants { public static final ErrorCode REPORT_COMMENT_NOT_EXISTS = new ErrorCode(13_000_006, "快捷评语不存在"); public static final ErrorCode PRISON_REPORT_TEMPLATE_NOT_EXISTS = new ErrorCode(13_000_007, "评估报告模板不存在"); + // ========== 问卷任务 14xxxx ========== + public static final ErrorCode QUESTIONNAIRE_TASK_NOT_EXISTS = new ErrorCode(14_000_001, "问卷任务不存在"); + public static final ErrorCode QUESTIONNAIRE_TASK_CANNOT_UPDATE = new ErrorCode(14_000_002, "只有草稿状态的任务可以修改"); + public static final ErrorCode QUESTIONNAIRE_TASK_CANNOT_CANCEL = new ErrorCode(14_000_003, "已结束或已取消的任务不能取消"); + public static final ErrorCode QUESTIONNAIRE_TASK_ALREADY_CANCELLED = new ErrorCode(14_000_004, "任务已被取消"); + public static final ErrorCode QUESTIONNAIRE_TASK_CANNOT_RESTART = new ErrorCode(14_000_005, "只有已结束的任务可以重新开始"); + public static final ErrorCode ERROR_TASK_PRISONER_EMPTY = new ErrorCode(14_000_006, "请选择要参与的犯人"); + public static final ErrorCode ERROR_TASK_AREA_EMPTY = new ErrorCode(14_000_007, "请选择监区"); + } diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/enums/questionnaire/QuestionnaireRecordStatusEnum.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/enums/questionnaire/QuestionnaireRecordStatusEnum.java new file mode 100644 index 0000000000..53190d3336 --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/enums/questionnaire/QuestionnaireRecordStatusEnum.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.prison.enums.questionnaire; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 问卷记录状态枚举 + * + * @author xlcp + */ +@Getter +@AllArgsConstructor +public enum QuestionnaireRecordStatusEnum { + + PENDING(1, "待完成"), + IN_PROGRESS(2, "进行中"), + COMPLETED(3, "已完成"), + EXPIRED(4, "已过期"), + CANCELLED(5, "已取消"); + + private final Integer code; + private final String name; + + public static QuestionnaireRecordStatusEnum getByCode(Integer code) { + for (QuestionnaireRecordStatusEnum statusEnum : values()) { + if (statusEnum.getCode().equals(code)) { + return statusEnum; + } + } + return null; + } + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/dashboard/impl/PrisonDashboardServiceImpl.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/dashboard/impl/PrisonDashboardServiceImpl.java index e75c7bad9e..4a5bc349a1 100644 --- a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/dashboard/impl/PrisonDashboardServiceImpl.java +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/dashboard/impl/PrisonDashboardServiceImpl.java @@ -369,9 +369,11 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService { Collectors.summingDouble(c -> c.getTotalAmount() != null ? c.getTotalAmount().doubleValue() : 0) )); for (Map.Entry entry : monthlyConsumptionMap.entrySet()) { + Double consumption = entry.getValue(); monthlyDataList.add(PrisonerDashboardStatsRespVO.MonthlyConsumptionData.builder() .category(entry.getKey()) - .perCapita(entry.getValue().intValue()) + .monthlyStandard(consumption.intValue()) // 支出 = 该月实际消费总额 + .perCapita(consumption.intValue()) // 人均消费 = 该月实际消费总额 .build()); } vo.setConsumptionMonthlyData(monthlyDataList); diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/questionnaire_task/QuestionnaireTaskService.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/questionnaire_task/QuestionnaireTaskService.java new file mode 100644 index 0000000000..2f24cbe345 --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/questionnaire_task/QuestionnaireTaskService.java @@ -0,0 +1,149 @@ +package cn.iocoder.yudao.module.prison.service.questionnaire_task; + +import java.util.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import jakarta.validation.Valid; + +import cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo.*; +import cn.iocoder.yudao.module.prison.dal.dataobject.questionnaire_task.QuestionnaireTaskDO; +import cn.iocoder.yudao.module.prison.dal.dataobject.questionnaire.QuestionnaireDO; +import cn.iocoder.yudao.module.prison.dal.dataobject.PrisonerDO; +import cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO; +import cn.iocoder.yudao.module.prison.dal.dataobject.area.AreaDO; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +/** + * 问卷任务 Service 接口 + * + * @author xlcp + */ +public interface QuestionnaireTaskService { + + // ==================== 基础 CRUD ==================== + + /** + * 创建问卷任务 + * + * @param createReqVO 创建信息 + * @return 任务ID + */ + Long createQuestionnaireTask(@Valid QuestionnaireTaskCreateReqVO createReqVO); + + /** + * 更新问卷任务 + * + * @param updateReqVO 更新信息 + */ + void updateQuestionnaireTask(@Valid QuestionnaireTaskUpdateReqVO updateReqVO); + + /** + * 删除问卷任务 + * + * @param id 任务ID + */ + void deleteQuestionnaireTask(Long id); + + /** + * 批量删除问卷任务 + * + * @param ids 任务ID列表 + */ + void deleteQuestionnaireTaskListByIds(List ids); + + /** + * 获得问卷任务 + * + * @param id 任务ID + * @return 问卷任务 + */ + QuestionnaireTaskDO getQuestionnaireTask(Long id); + + /** + * 获得问卷任务分页 + * + * @param pageReqVO 分页查询 + * @return 问卷任务分页 + */ + PageResult getQuestionnaireTaskPage(QuestionnaireTaskPageReqVO pageReqVO); + + // ==================== 任务执行相关 ==================== + + /** + * 取消任务 + * + * @param id 任务ID + */ + void cancelTask(Long id); + + /** + * 结束任务 + * + * @param id 任务ID + */ + void finishTask(Long id); + + /** + * 重新开始已结束的任务 + * + * @param id 任务ID + */ + void restartTask(Long id); + + // ==================== 进度跟踪相关 ==================== + + /** + * 获取任务进度详情 + * + * @param id 任务ID + * @return 进度详情 + */ + TaskProgressRespVO getTaskProgress(Long id); + + /** + * 获取任务未完成人员列表 + * + * @param id 任务ID + * @param pageReqVO 分页参数 + * @return 未完成人员分页 + */ + PageResult getPendingPrisoners(Long id, PageParam pageReqVO); + + /** + * 提醒未完成人员 + * + * @param id 任务ID + * @return 提醒人数 + */ + Integer remindPendingPrisoners(Long id); + + // ==================== 统计相关 ==================== + + /** + * 按监区统计任务完成情况 + * + * @param id 任务ID + * @return 监区统计列表 + */ + List getTaskAreaStatistics(Long id); + + /** + * 获取全局任务统计汇总 + * + * @return 统计汇总 + */ + TaskStatisticsSummaryRespVO getStatisticsSummary(); + + /** + * 按监区对比分析 + * + * @param questionnaireId 问卷ID(可选,为空则统计所有问卷) + * @param areaIds 监区ID列表 + * @return 对比数据 + */ + List compareAreasByQuestionnaire(Long questionnaireId, List areaIds); + +} diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/questionnaire_task/QuestionnaireTaskServiceImpl.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/questionnaire_task/QuestionnaireTaskServiceImpl.java new file mode 100644 index 0000000000..cd5510eb57 --- /dev/null +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/questionnaire_task/QuestionnaireTaskServiceImpl.java @@ -0,0 +1,517 @@ +package cn.iocoder.yudao.module.prison.service.questionnaire_task; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo.*; +import cn.iocoder.yudao.module.prison.dal.dataobject.questionnaire_task.QuestionnaireTaskDO; +import cn.iocoder.yudao.module.prison.dal.dataobject.questionnaire.QuestionnaireDO; +import cn.iocoder.yudao.module.prison.dal.dataobject.PrisonerDO; +import cn.iocoder.yudao.module.prison.dal.dataobject.area.AreaDO; +import cn.iocoder.yudao.module.prison.dal.mysql.questionnaire_task.QuestionnaireTaskMapper; +import cn.iocoder.yudao.module.prison.dal.mysql.questionnaire.QuestionnaireMapper; +import cn.iocoder.yudao.module.prison.dal.mysql.questionnairerecord.QuestionnaireRecordMapper; +import cn.iocoder.yudao.module.prison.dal.mysql.PrisonerMapper; +import cn.iocoder.yudao.module.prison.dal.mysql.area.AreaMapper; +import cn.iocoder.yudao.module.prison.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.prison.enums.questionnaire.QuestionnaireRecordStatusEnum; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 问卷任务 Service 实现类 + * + * @author xlcp + */ +@Service +@Slf4j +public class QuestionnaireTaskServiceImpl implements QuestionnaireTaskService { + + @Resource + private QuestionnaireTaskMapper questionnaireTaskMapper; + + @Resource + private QuestionnaireMapper questionnaireMapper; + + @Resource + private QuestionnaireRecordMapper questionnaireRecordMapper; + + @Resource + private PrisonerMapper prisonerMapper; + + @Resource + private AreaMapper areaMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createQuestionnaireTask(QuestionnaireTaskCreateReqVO createReqVO) { + QuestionnaireDO questionnaire = questionnaireMapper.selectById(createReqVO.getQuestionnaireId()); + if (questionnaire == null) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_NOT_EXISTS); + } + + validateTarget(createReqVO); + + QuestionnaireTaskDO task = QuestionnaireTaskDO.builder() + .taskName(createReqVO.getTaskName()) + .questionnaireId(createReqVO.getQuestionnaireId()) + .questionnaireName(questionnaire.getTitle()) + .targetType(createReqVO.getTargetType()) + .startTime(createReqVO.getStartTime()) + .deadline(createReqVO.getDeadline()) + .status(2) + .totalCount(0) + .completedCount(0) + .pendingCount(0) + .completionRate(BigDecimal.ZERO) + .remark(createReqVO.getRemark()) + .build(); + + if (createReqVO.getTargetType() == 1) { + task.setPrisonerIds(CollUtil.join(createReqVO.getPrisonerIds(), ",")); + } else if (createReqVO.getTargetType() == 2) { + task.setAreaId(createReqVO.getAreaId()); + AreaDO area = areaMapper.selectById(createReqVO.getAreaId()); + if (area != null) { + task.setAreaName(area.getName()); + } + } + + questionnaireTaskMapper.insert(task); + + List prisoners = getTargetPrisoners(createReqVO); + if (CollUtil.isNotEmpty(prisoners)) { + createRecordsForPrisoners(task, prisoners); + } + + updateTaskStatistics(task.getId()); + + return task.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateQuestionnaireTask(QuestionnaireTaskUpdateReqVO updateReqVO) { + QuestionnaireTaskDO task = questionnaireTaskMapper.selectById(updateReqVO.getId()); + if (task == null) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_TASK_NOT_EXISTS); + } + + if (!task.getStatus().equals(1)) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_TASK_CANNOT_UPDATE); + } + + if (updateReqVO.getTaskName() != null) { + task.setTaskName(updateReqVO.getTaskName()); + } + if (updateReqVO.getDeadline() != null) { + task.setDeadline(updateReqVO.getDeadline()); + } + if (updateReqVO.getRemark() != null) { + task.setRemark(updateReqVO.getRemark()); + } + + questionnaireTaskMapper.updateById(task); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteQuestionnaireTask(Long id) { + QuestionnaireTaskDO task = questionnaireTaskMapper.selectById(id); + if (task == null) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_TASK_NOT_EXISTS); + } + + questionnaireRecordMapper.delete(new LambdaQueryWrapper() + .eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getTaskId, id)); + + questionnaireTaskMapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteQuestionnaireTaskListByIds(List ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + + List records = questionnaireRecordMapper.selectList( + new LambdaQueryWrapper() + .in(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getTaskId, ids)); + + if (CollUtil.isNotEmpty(records)) { + questionnaireRecordMapper.deleteBatchIds(records.stream() + .map(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getId) + .collect(Collectors.toList())); + } + + questionnaireTaskMapper.deleteBatchIds(ids); + } + + @Override + public QuestionnaireTaskDO getQuestionnaireTask(Long id) { + return questionnaireTaskMapper.selectById(id); + } + + @Override + public PageResult getQuestionnaireTaskPage(QuestionnaireTaskPageReqVO pageReqVO) { + Page page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .likeIfPresent(QuestionnaireTaskDO::getTaskName, pageReqVO.getTaskName()) + .eqIfPresent(QuestionnaireTaskDO::getStatus, pageReqVO.getStatus()) + .eqIfPresent(QuestionnaireTaskDO::getQuestionnaireId, pageReqVO.getQuestionnaireId()) + .betweenIfPresent(QuestionnaireTaskDO::getCreateTime, pageReqVO.getCreateTime()) + .orderByDesc(QuestionnaireTaskDO::getCreateTime); + + Page result = questionnaireTaskMapper.selectPage(page, wrapper); + return new PageResult<>(result.getRecords(), result.getTotal()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelTask(Long id) { + QuestionnaireTaskDO task = questionnaireTaskMapper.selectById(id); + if (task == null) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_TASK_NOT_EXISTS); + } + + if (task.getStatus().equals(3) || task.getStatus().equals(4)) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_TASK_CANNOT_CANCEL); + } + + task.setStatus(4); + questionnaireTaskMapper.updateById(task); + + questionnaireRecordMapper.update(null, + new LambdaUpdateWrapper() + .eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getTaskId, id) + .eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getStatus, QuestionnaireRecordStatusEnum.PENDING.getCode()) + .set(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getStatus, QuestionnaireRecordStatusEnum.CANCELLED.getCode())); + + updateTaskStatistics(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void finishTask(Long id) { + QuestionnaireTaskDO task = questionnaireTaskMapper.selectById(id); + if (task == null) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_TASK_NOT_EXISTS); + } + + if (task.getStatus().equals(4)) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_TASK_ALREADY_CANCELLED); + } + + task.setStatus(3); + questionnaireTaskMapper.updateById(task); + + questionnaireRecordMapper.update(null, + new LambdaUpdateWrapper() + .eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getTaskId, id) + .in(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getStatus, + QuestionnaireRecordStatusEnum.PENDING.getCode(), + QuestionnaireRecordStatusEnum.IN_PROGRESS.getCode()) + .set(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getStatus, QuestionnaireRecordStatusEnum.EXPIRED.getCode())); + + updateTaskStatistics(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void restartTask(Long id) { + QuestionnaireTaskDO task = questionnaireTaskMapper.selectById(id); + if (task == null) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_TASK_NOT_EXISTS); + } + + if (!task.getStatus().equals(3)) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_TASK_CANNOT_RESTART); + } + + task.setStatus(2); + task.setDeadline(LocalDateTime.now().plusDays(7)); + questionnaireTaskMapper.updateById(task); + + questionnaireRecordMapper.update(null, + new LambdaUpdateWrapper() + .eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getTaskId, id) + .eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getStatus, QuestionnaireRecordStatusEnum.EXPIRED.getCode()) + .set(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getStatus, QuestionnaireRecordStatusEnum.PENDING.getCode()) + .set(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getDeadline, task.getDeadline())); + } + + @Override + public TaskProgressRespVO getTaskProgress(Long id) { + QuestionnaireTaskDO task = questionnaireTaskMapper.selectById(id); + if (task == null) { + throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_TASK_NOT_EXISTS); + } + + Map stats = questionnaireRecordMapper.selectMaps( + new QueryWrapper() + .select("COUNT(*) as total", + "SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as pending", + "SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as in_progress", + "SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) as completed", + "SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) as expired", + "SUM(CASE WHEN status = 5 THEN 1 ELSE 0 END) as cancelled") + .eq("task_id", id) + ).stream().findFirst().orElse(Collections.emptyMap()); + + int total = ((Number) stats.getOrDefault("total", 0)).intValue(); + int pending = ((Number) stats.getOrDefault("pending", 0)).intValue(); + int inProgress = ((Number) stats.getOrDefault("in_progress", 0)).intValue(); + int completed = ((Number) stats.getOrDefault("completed", 0)).intValue(); + + return TaskProgressRespVO.builder() + .taskId(id) + .taskName(task.getTaskName()) + .questionnaireName(task.getQuestionnaireName()) + .status(task.getStatus()) + .startTime(task.getStartTime()) + .deadline(task.getDeadline()) + .totalCount(total) + .completedCount(completed) + .pendingCount(pending + inProgress) + .completionRate(total > 0 ? BigDecimal.valueOf(completed * 100.0 / total).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO) + .statusBreakdown(TaskProgressRespVO.StatusBreakdown.builder() + .pending(pending) + .inProgress(inProgress) + .completed(completed) + .expired(((Number) stats.getOrDefault("expired", 0)).intValue()) + .cancelled(((Number) stats.getOrDefault("cancelled", 0)).intValue()) + .build()) + .build(); + } + + @Override + public PageResult getPendingPrisoners(Long id, PageParam pageReqVO) { + Page page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getTaskId, id) + .in(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getStatus, + QuestionnaireRecordStatusEnum.PENDING.getCode(), + QuestionnaireRecordStatusEnum.IN_PROGRESS.getCode()) + .orderByAsc(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getCreateTime); + + Page result = questionnaireRecordMapper.selectPage(page, wrapper); + return new PageResult<>(result.getRecords(), result.getTotal()); + } + + @Override + public Integer remindPendingPrisoners(Long id) { + List pendingRecords = questionnaireRecordMapper.selectList( + new LambdaQueryWrapper() + .eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getTaskId, id) + .eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getStatus, QuestionnaireRecordStatusEnum.PENDING.getCode()) + ); + + if (CollUtil.isEmpty(pendingRecords)) { + return 0; + } + + log.info("提醒任务 {} 中 {} 位未完成人员", id, pendingRecords.size()); + return pendingRecords.size(); + } + + @Override + public List getTaskAreaStatistics(Long id) { + List> stats = questionnaireRecordMapper.selectMaps( + new QueryWrapper() + .select("area_id", "area_name", + "COUNT(*) as total_count", + "SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) as completed_count", + "AVG(CASE WHEN status = 3 THEN total_score ELSE NULL END) as avg_score", + "SUM(CASE WHEN status = 3 AND pass_status = 1 THEN 1 ELSE 0 END) * 100.0 / NULLIF(SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END), 0) as pass_rate", + "SUM(CASE WHEN risk_level = 1 THEN 1 ELSE 0 END) as high_risk_count", + "SUM(CASE WHEN risk_level = 2 THEN 1 ELSE 0 END) as medium_risk_count", + "SUM(CASE WHEN risk_level = 3 THEN 1 ELSE 0 END) as low_risk_count") + .eq("task_id", id) + .groupBy("area_id", "area_name") + .orderByDesc("completed_count") + ); + + return stats.stream().map(stat -> { + int total = ((Number) stat.getOrDefault("total_count", 0)).intValue(); + int completed = ((Number) stat.getOrDefault("completed_count", 0)).intValue(); + BigDecimal avgScore = stat.get("avg_score") != null ? + new BigDecimal(stat.get("avg_score").toString()) : BigDecimal.ZERO; + BigDecimal passRate = stat.get("pass_rate") != null ? + new BigDecimal(stat.get("pass_rate").toString()) : BigDecimal.ZERO; + + return TaskAreaStatisticsRespVO.builder() + .areaId(stat.get("area_id") != null ? ((Number) stat.get("area_id")).longValue() : null) + .areaName((String) stat.get("area_name")) + .totalCount(total) + .completedCount(completed) + .completionRate(total > 0 ? BigDecimal.valueOf(completed * 100.0 / total).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO) + .avgScore(avgScore.setScale(2, RoundingMode.HALF_UP)) + .passRate(passRate.setScale(2, RoundingMode.HALF_UP)) + .riskDistribution(TaskAreaStatisticsRespVO.RiskDistribution.builder() + .highRisk(((Number) stat.getOrDefault("high_risk_count", 0)).intValue()) + .mediumRisk(((Number) stat.getOrDefault("medium_risk_count", 0)).intValue()) + .lowRisk(((Number) stat.getOrDefault("low_risk_count", 0)).intValue()) + .build()) + .build(); + }).collect(Collectors.toList()); + } + + @Override + public TaskStatisticsSummaryRespVO getStatisticsSummary() { + Map stats = questionnaireTaskMapper.selectMaps( + new QueryWrapper() + .select("COUNT(*) as task_count", + "SUM(total_count) as total_prisoners", + "SUM(completed_count) as total_completed", + "AVG(completion_rate) as avg_completion_rate") + .eq("deleted", 0) + .in("status", Arrays.asList(2, 3)) + ).stream().findFirst().orElse(Collections.emptyMap()); + + int taskCount = ((Number) stats.getOrDefault("task_count", 0)).intValue(); + int totalPrisoners = ((Number) stats.getOrDefault("total_prisoners", 0)).intValue(); + int totalCompleted = ((Number) stats.getOrDefault("total_completed", 0)).intValue(); + + return TaskStatisticsSummaryRespVO.builder() + .taskCount(taskCount) + .totalPrisoners(totalPrisoners) + .totalCompleted(totalCompleted) + .totalPending(totalPrisoners - totalCompleted) + .overallCompletionRate(totalPrisoners > 0 ? + BigDecimal.valueOf(totalCompleted * 100.0 / totalPrisoners).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO) + .build(); + } + + @Override + public List compareAreasByQuestionnaire(Long questionnaireId, List areaIds) { + if (CollUtil.isEmpty(areaIds)) { + return Collections.emptyList(); + } + + List> stats; + if (questionnaireId != null) { + stats = questionnaireTaskMapper.selectMaps( + new QueryWrapper() + .select("qt.id as task_id", "qt.task_name", + "SUM(qr.total_count) as total_count", + "SUM(qr.completed_count) as completed_count") + .from("prison_questionnaire_task qt") + .innerJoin("prison_questionnaire_record qr ON qt.id = qr.task_id") + .eq("qt.deleted", 0) + .eq("qt.questionnaire_id", questionnaireId) + .in("qr.area_id", areaIds) + .groupBy("qt.id", "qt.task_name") + ); + } else { + stats = questionnaireTaskMapper.selectMaps( + new QueryWrapper() + .select("qt.id as task_id", "qt.task_name", + "SUM(qr.total_count) as total_count", + "SUM(qr.completed_count) as completed_count") + .from("prison_questionnaire_task qt") + .innerJoin("prison_questionnaire_record qr ON qt.id = qr.task_id") + .eq("qt.deleted", 0) + .in("qr.area_id", areaIds) + .groupBy("qt.id", "qt.task_name") + ); + } + + return stats.stream().map(stat -> AreaComparisonRespVO.builder() + .taskId(((Number) stat.get("task_id")).longValue()) + .taskName((String) stat.get("task_name")) + .totalCount(((Number) stat.getOrDefault("total_count", 0)).intValue()) + .completedCount(((Number) stat.getOrDefault("completed_count", 0)).intValue()) + .build() + ).collect(Collectors.toList()); + } + + private void validateTarget(QuestionnaireTaskCreateReqVO createReqVO) { + if (createReqVO.getTargetType() == 1) { + if (CollUtil.isEmpty(createReqVO.getPrisonerIds())) { + throw new ServiceException(ErrorCodeConstants.ERROR_TASK_PRISONER_EMPTY); + } + } else if (createReqVO.getTargetType() == 2) { + if (createReqVO.getAreaId() == null) { + throw new ServiceException(ErrorCodeConstants.ERROR_TASK_AREA_EMPTY); + } + AreaDO area = areaMapper.selectById(createReqVO.getAreaId()); + if (area == null) { + throw new ServiceException(ErrorCodeConstants.AREA_NOT_EXISTS); + } + } + } + + private List getTargetPrisoners(QuestionnaireTaskCreateReqVO createReqVO) { + switch (createReqVO.getTargetType()) { + case 1: + return prisonerMapper.selectBatchIds(createReqVO.getPrisonerIds()); + case 2: + return prisonerMapper.selectList( + new LambdaQueryWrapper() + .eq(PrisonerDO::getPrisonAreaId, createReqVO.getAreaId()) + .eq(PrisonerDO::getStatus, 1) + ); + case 3: + return prisonerMapper.selectList( + new LambdaQueryWrapper() + .eq(PrisonerDO::getStatus, 1) + ); + default: + return Collections.emptyList(); + } + } + + private void createRecordsForPrisoners(QuestionnaireTaskDO task, List prisoners) { + QuestionnaireDO questionnaire = questionnaireMapper.selectById(task.getQuestionnaireId()); + + List records = prisoners.stream() + .filter(p -> p.getStatus() != null && p.getStatus().getValue() == 1) + .map(prisoner -> cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO.builder() + .questionnaireId(task.getQuestionnaireId()) + .questionnaireName(task.getQuestionnaireName()) + .prisonerId(prisoner.getId()) + .prisonerNo(prisoner.getPrisonerNo()) + .prisonerName(prisoner.getName()) + .prisonerAreaId(prisoner.getPrisonAreaId()) + .prisonerAreaName(getAreaName(prisoner.getPrisonAreaId())) + .taskId(task.getId()) + .status(QuestionnaireRecordStatusEnum.PENDING.getCode()) + .startTime(task.getStartTime()) + .deadline(task.getDeadline()) + .passScore(questionnaire != null ? questionnaire.getPassScore() : null) + .build()) + .collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(records)) { + questionnaireRecordMapper.insertBatch(records); + } + } + + private String getAreaName(Long areaId) { + if (areaId == null) { + return null; + } + AreaDO area = areaMapper.selectById(areaId); + return area != null ? area.getName() : null; + } + + private void updateTaskStatistics(Long taskId) { + questionnaireTaskMapper.updateTaskStatistics(taskId); + } + +} diff --git a/yudao-module-system/pom.xml b/yudao-module-system/pom.xml index 57e24b1979..8ac462462c 100644 --- a/yudao-module-system/pom.xml +++ b/yudao-module-system/pom.xml @@ -95,16 +95,15 @@ justauth-spring-boot-starter - com.github.binarywang - wx-java-mp-spring-boot-starter + wx-java-mp-spring-boot-starter com.github.binarywang - wx-java-miniapp-spring-boot-starter + wx-java-miniapp-spring-boot-starter - --> com.anji-plus