Compare commits

...

7 Commits

Author SHA1 Message Date
25aa6144c4 feat: 扩展问卷问题管理权限,支持问卷管理员权限
- 后端:PrisonQuestionController 接口增加 prison:questionnaire:update 和 prison:questionnaire:query 权限判断
- 前端:QuestionList.vue 按钮增加 prison:questionnaire:update 权限判断
- 允许拥有问卷模板管理权限的用户也能管理问卷问题
2026-01-28 11:50:34 +08:00
984b95b6ed fix: 修复罪犯工作台汇款记录显示问题
- 修复调用错误方法导致的汇款记录为空问题
- 修复金额解析异常(BigDecimal处理小数)
- 修改selectRecentRemittanceDetails替代selectRecentRemittances
- 优化selectRecentRiskAssessments方法注释
2026-01-27 14:55:56 +08:00
182b7a7b43 fix(prison): 修复刑期总天数计算错误
- 使用 ChronoUnit.DAYS.between 计算入监日期到释放日期的总天数
- 修复后:2024-01-15 到 2027-07-14 = 1276天(原错误计算为743天)
- 确保与其他日期计算逻辑一致
2026-01-27 12:11:42 +08:00
653e55a075 feat(prison): 新增累计表扬次数统计字段
- 后端新增 praiseCount 字段(Integer类型)
- 从 prison_situation 表统计 category=2(表扬类型)的记录数
- 遵循 violationCount 的实现模式

参考计划: .sisyphus/plans/dashboard-center-update.md Task 1
2026-01-27 10:27:52 +08:00
cea9ed7335 feat(dashboard): 优化大帐统计展示
- 大帐统计改为显示账户余额,移除出入库卡片
- 柱状图展示收入和支出数据,按月份正序排列
- 奖惩记录从数据库真实查询,区分奖励和惩罚
- 修复惩罚记录显示问题(类型匹配)
- 新增查询方法:selectRecentRewardsPunishments、selectLatestBalance
- PrisonerDashboardStatsRespVO新增balance字段
- ConsumptionMapper新增selectLatestBalance方法
2026-01-26 18:16:43 +08:00
ff09efa216 feat: 问卷任务管理模块增强
- 新增犯人完成进度查询接口
- 新增批量分配 Agent 填写功能
- 优化仪表盘统计逻辑
- 新增 Agent 填写权限 SQL
2026-01-26 16:02:45 +08:00
f0caa49133 feat: 新增问卷任务管理模块
- 新增问卷任务CRUD功能(controller、service、dal)
- 新增问卷任务统计相关VO(进度、区域对比、汇总统计)
- 新增问卷记录状态枚举
- 更新问卷记录DO结构
- 更新dashboard服务实现
- 新增lombok依赖
2026-01-24 10:55:36 +08:00
32 changed files with 2165 additions and 54 deletions

View File

@ -69,6 +69,27 @@ public class PrisonerDashboardStatsRespVO {
@Schema(description = "风险等级1-低风险 2-中风险 3-高风险 4-极高风险", example = "1")
private Integer riskLevel;
@Schema(description = "剩余刑期天数", example = "1000")
private Integer remainingDays;
@Schema(description = "刑期总天数", example = "1276")
private Integer sentenceDays;
@Schema(description = "累计违规次数", example = "5")
private Integer violationCount;
@Schema(description = "累计表扬天数", example = "-")
private String praiseDays;
@Schema(description = "累计表扬次数", example = "8")
private Integer praiseCount;
@Schema(description = "累计扣分次数", example = "3")
private Integer penaltyCount;
@Schema(description = "累计加分次数", example = "8")
private Integer rewardCount;
@Schema(description = "中心左侧数据")
private CenterLeftData centerLeftData;
@ -78,6 +99,9 @@ public class PrisonerDashboardStatsRespVO {
@Schema(description = "月度消费数据")
private List<MonthlyConsumptionData> consumptionMonthlyData;
@Schema(description = "账户余额")
private Integer balance;
@Schema(description = "消费汇总")
private ConsumptionSummary consumptionSummary;

View File

@ -40,7 +40,7 @@ public class PrisonQuestionController {
@PostMapping("/create")
@Operation(summary = "创建问卷问题")
@PreAuthorize("@ss.hasPermission('prison:question:create')")
@PreAuthorize("@ss.hasPermission('prison:question:create') or @ss.hasPermission('prison:questionnaire:update')")
@ApiAccessLog(operateType = CREATE)
public CommonResult<Long> createQuestion(@Valid @RequestBody QuestionSaveReqVO createReqVO) {
return success(questionService.createQuestion(createReqVO));
@ -48,7 +48,7 @@ public class PrisonQuestionController {
@PutMapping("/update")
@Operation(summary = "更新问卷问题")
@PreAuthorize("@ss.hasPermission('prison:question:update')")
@PreAuthorize("@ss.hasPermission('prison:question:update') or @ss.hasPermission('prison:questionnaire:update')")
@ApiAccessLog(operateType = UPDATE)
public CommonResult<Boolean> updateQuestion(@Valid @RequestBody QuestionSaveReqVO updateReqVO) {
questionService.updateQuestion(updateReqVO);
@ -58,7 +58,7 @@ public class PrisonQuestionController {
@DeleteMapping("/delete")
@Operation(summary = "删除问卷问题")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('prison:question:delete')")
@PreAuthorize("@ss.hasPermission('prison:question:delete') or @ss.hasPermission('prison:questionnaire:update')")
@ApiAccessLog(operateType = DELETE)
public CommonResult<Boolean> deleteQuestion(@NotNull(message = "编号不能为空") @RequestParam("id") Long id) {
questionService.deleteQuestion(id);
@ -68,7 +68,7 @@ public class PrisonQuestionController {
@DeleteMapping("/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除问卷问题")
@PreAuthorize("@ss.hasPermission('prison:question:delete')")
@PreAuthorize("@ss.hasPermission('prison:question:delete') or @ss.hasPermission('prison:questionnaire:update')")
@ApiAccessLog(operateType = DELETE)
public CommonResult<Boolean> deleteQuestionList(@NotEmpty(message = "编号列表不能为空") @RequestParam("ids") List<Long> ids) {
questionService.deleteQuestionListByIds(ids);
@ -78,7 +78,7 @@ public class PrisonQuestionController {
@GetMapping("/get")
@Operation(summary = "获得问卷问题")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('prison:question:query')")
@PreAuthorize("@ss.hasPermission('prison:question:query') or @ss.hasPermission('prison:questionnaire:query') or @ss.hasPermission('prison:questionnaire:update')")
public CommonResult<QuestionRespVO> getQuestion(@RequestParam("id") Long id) {
QuestionDO question = questionService.getQuestion(id);
return success(QuestionConvert.INSTANCE.convert(question));
@ -86,7 +86,7 @@ public class PrisonQuestionController {
@GetMapping("/page")
@Operation(summary = "获得问卷问题分页")
@PreAuthorize("@ss.hasPermission('prison:question:query')")
@PreAuthorize("@ss.hasPermission('prison:question:query') or @ss.hasPermission('prison:questionnaire:query') or @ss.hasPermission('prison:questionnaire:update')")
public CommonResult<PageResult<QuestionRespVO>> getQuestionPage(@Valid QuestionPageReqVO pageReqVO) {
PageResult<QuestionDO> pageResult = questionService.getQuestionPage(pageReqVO);
return success(QuestionConvert.INSTANCE.convertPage(pageResult));
@ -94,7 +94,7 @@ public class PrisonQuestionController {
@PostMapping("/batch-update")
@Operation(summary = "批量更新问卷问题")
@PreAuthorize("@ss.hasPermission('prison:question:update')")
@PreAuthorize("@ss.hasPermission('prison:question:update') or @ss.hasPermission('prison:questionnaire:update')")
@ApiAccessLog(operateType = UPDATE)
public CommonResult<Boolean> batchUpdateQuestion(@Valid @RequestBody QuestionBatchUpdateReqVO reqVO) {
// 转换为 Service 需要的格式
@ -113,7 +113,7 @@ public class PrisonQuestionController {
@GetMapping("/export-excel")
@Operation(summary = "导出问卷问题 Excel")
@PreAuthorize("@ss.hasPermission('prison:question:export')")
@PreAuthorize("@ss.hasPermission('prison:question:export') or @ss.hasPermission('prison:questionnaire:export') or @ss.hasPermission('prison:questionnaire:update')")
@ApiAccessLog(operateType = EXPORT)
public void exportQuestionExcel(@Valid QuestionPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {

View File

@ -0,0 +1,214 @@
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<Long> createQuestionnaireTask(@Valid @RequestBody QuestionnaireTaskCreateReqVO createReqVO) {
return success(questionnaireTaskService.createQuestionnaireTask(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新问卷任务")
@PreAuthorize("@ss.hasPermission('prison:questionnaire-task:update')")
public CommonResult<Boolean> 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<Boolean> 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<Boolean> deleteQuestionnaireTaskList(@NotEmpty(message = "任务ID列表不能为空") @RequestParam("ids") List<Long> 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<QuestionnaireTaskRespVO> 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<PageResult<QuestionnaireTaskRespVO>> getQuestionnaireTaskPage(@Valid QuestionnaireTaskPageReqVO pageReqVO) {
PageResult<QuestionnaireTaskDO> 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<QuestionnaireTaskDO> 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<Boolean> 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<Boolean> 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<Boolean> 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<TaskProgressRespVO> 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<PageResult<QuestionnaireRecordDO>> getPendingPrisoners(
@NotNull(message = "任务ID不能为空") @RequestParam("id") Long id,
@Valid PageParam pageReqVO) {
return success(questionnaireTaskService.getPendingPrisoners(id, pageReqVO));
}
@GetMapping("/prisoner-progress")
@Operation(summary = "获取任务的人员填写进度列表")
@Parameter(name = "id", description = "任务ID", required = true)
@PreAuthorize("@ss.hasPermission('prison:questionnaire-task:query')")
public CommonResult<List<PrisonerProgressRespVO>> getPrisonerProgress(
@NotNull(message = "任务ID不能为空") @RequestParam("id") Long id) {
return success(questionnaireTaskService.getPrisonerProgress(id));
}
@PostMapping("/remind")
@Operation(summary = "提醒未完成人员")
@Parameter(name = "id", description = "任务ID", required = true)
@PreAuthorize("@ss.hasPermission('prison:questionnaire-task:remind')")
public CommonResult<Integer> remindPendingPrisoners(@NotNull(message = "任务ID不能为空") @RequestParam("id") Long id) {
return success(questionnaireTaskService.remindPendingPrisoners(id));
}
@PostMapping("/notify-prisoner")
@Operation(summary = "通知单个人员")
@Parameter(name = "recordId", description = "记录ID", required = true)
@PreAuthorize("@ss.hasPermission('prison:questionnaire-task:notify')")
public CommonResult<Boolean> notifyPrisoner(@NotNull(message = "记录ID不能为空") @RequestParam("recordId") Long recordId) {
questionnaireTaskService.notifyPrisoner(recordId);
return success(true);
}
@PostMapping("/reset-record")
@Operation(summary = "重置人员答题记录")
@Parameter(name = "recordId", description = "记录ID", required = true)
@PreAuthorize("@ss.hasPermission('prison:questionnaire-record:reset')")
public CommonResult<Boolean> resetPrisonerRecord(@NotNull(message = "记录ID不能为空") @RequestParam("recordId") Long recordId) {
questionnaireTaskService.resetPrisonerRecord(recordId);
return success(true);
}
// ==================== 统计相关 ====================
@GetMapping("/area-statistics")
@Operation(summary = "按监区统计任务完成情况")
@Parameter(name = "id", description = "任务ID", required = true)
@PreAuthorize("@ss.hasPermission('prison:questionnaire-task:query')")
public CommonResult<List<TaskAreaStatisticsRespVO>> 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<TaskStatisticsSummaryRespVO> getStatisticsSummary() {
return success(questionnaireTaskService.getStatisticsSummary());
}
@GetMapping("/area-comparison")
@Operation(summary = "按监区对比分析")
@PreAuthorize("@ss.hasPermission('prison:questionnaire-task:query')")
public CommonResult<List<AreaComparisonRespVO>> compareAreasByQuestionnaire(
@RequestParam(value = "questionnaireId", required = false) Long questionnaireId,
@RequestParam(value = "areaIds", required = false) List<Long> areaIds) {
return success(questionnaireTaskService.compareAreasByQuestionnaire(questionnaireId, areaIds));
}
}

View File

@ -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;
}

View File

@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.prison.controller.admin.questionnaire_task.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 问卷任务人员填写进度 Response VO")
@Data
public class PrisonerProgressRespVO {
@Schema(description = "记录ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "罪犯ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private Long prisonerId;
@Schema(description = "罪犯编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "P001")
private String prisonerNo;
@Schema(description = "罪犯姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
private String prisonerName;
@Schema(description = "监区ID", example = "100")
private Long areaId;
@Schema(description = "监区名称", example = "一监区")
private String areaName;
@Schema(description = "填写状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
@Schema(description = "客观分", example = "80")
private Integer objectiveScore;
@Schema(description = "主观分", example = "85")
private Integer subjectiveScore;
@Schema(description = "总分", example = "165")
private Integer totalScore;
@Schema(description = "风险等级", example = "2")
private Integer riskLevel;
@Schema(description = "答题用时(秒)", example = "300")
private Integer duration;
@Schema(description = "开始时间")
private LocalDateTime startTime;
@Schema(description = "完成时间")
private LocalDateTime finishTime;
}

View File

@ -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<Long> 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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -129,6 +129,14 @@ public class PrisonQuestionnaireRecordController {
return success(true);
}
@PostMapping("/submit-by-agent")
@Operation(summary = "代为提交答卷(民警代填)")
@PreAuthorize("@ss.hasPermission('prison:questionnaire-record:agent-fill')")
public CommonResult<Boolean> submitAnswerByAgent(@Valid @RequestBody AssessmentAnswerSubmitReqVO reqVO) {
questionnaireRecordService.submitAnswerByAgent(reqVO);
return success(true);
}
@PostMapping("/finish")
@Operation(summary = "结束测评")
@PreAuthorize("@ss.hasPermission('prison:questionnaire-record:finish')")

View File

@ -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<QuestionnaireTaskRespVO> convertList(List<QuestionnaireTaskDO> list);
PageResult<QuestionnaireTaskRespVO> convertPage(PageResult<QuestionnaireTaskDO> page);
}

View File

@ -67,5 +67,21 @@ public class AnswerDO extends TenantBaseDO {
* 答题时间
*/
private Integer duration;
/**
* 是否代填true-民警代填 false-本人填写 null-未知
*/
private Boolean isAgentFill;
/**
* 代填操作人ID民警ID
*/
private Long agentOperatorId;
/**
* 代填操作人姓名
*/
private String agentOperatorName;
/**
* 代填时间
*/
private LocalDateTime agentFillTime;
}

View File

@ -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;
}

View File

@ -54,6 +54,21 @@ public class QuestionnaireRecordDO extends TenantBaseDO {
*/
private String prisonerName;
// ==================== 任务关联 ====================
/**
* 所属任务ID
*/
private Long taskId;
/**
* 犯人所属监区ID
*/
private Long prisonerAreaId;
/**
* 犯人所属监区名称
*/
private String prisonerAreaName;
// ==================== 测评状态 ====================
/**

View File

@ -123,4 +123,18 @@ public interface ConsumptionMapper extends BaseMapperX<ConsumptionDO> {
@ResultMap("ConsumptionDetailResultMap")
List<ConsumptionRespVO> selectConsumptionDetailPage(@Param("reqVO") ConsumptionPageReqVO reqVO);
/**
* 查询罪犯最新账户余额
*/
@Select("""
SELECT balance
FROM prison_consumption
WHERE deleted = 0
AND status = 1
AND prisoner_id = #{prisonerId}
ORDER BY id DESC
LIMIT 1
""")
java.math.BigDecimal selectLatestBalance(@Param("prisonerId") Long prisonerId);
}

View File

@ -9,6 +9,7 @@ import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* 监管看板 Mapper
@ -18,6 +19,69 @@ import java.util.List;
@Mapper
public interface PrisonDashboardMapper {
/**
* 查询最近6个月罪犯的汇款记录收入
*/
@Select("""
SELECT
DATE_FORMAT(arrive_date, '%Y-%m') AS month,
SUM(amount) AS total_amount
FROM prison_remittance
WHERE deleted = 0
AND status = 1
AND prisoner_id = #{prisonerId}
AND arrive_date >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
GROUP BY DATE_FORMAT(arrive_date, '%Y-%m')
ORDER BY month DESC
""")
List<Map<String, Object>> selectRecentRemittances(@Param("prisonerId") Long prisonerId);
/**
* 查询最近6个月罪犯的汇款详情用于展示列表
*/
@Select("""
SELECT
arrive_date,
remitter_name,
amount,
relationship,
remark
FROM prison_remittance
WHERE deleted = 0
AND status = 1
AND prisoner_id = #{prisonerId}
AND arrive_date >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
ORDER BY arrive_date DESC
LIMIT 10
""")
List<Map<String, Object>> selectRecentRemittanceDetails(@Param("prisonerId") Long prisonerId);
/**
* 查询最近6个月罪犯的风险评估记录
*/
@Select("""
SELECT
id,
assessment_date,
assessment_type,
overall_score,
risk_level,
violence_risk,
escape_risk,
self_harm_risk,
mental_state,
recommendation,
assessor_name
FROM prison_risk
WHERE deleted = 0
AND status = 3
AND prisoner_id = #{prisonerId}
AND assessment_date >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
ORDER BY assessment_date DESC
LIMIT 10
""")
List<Map<String, Object>> selectRecentRiskAssessments(@Param("prisonerId") Long prisonerId);
/**
* 查询核心指标卡片数据
*

View File

@ -0,0 +1,90 @@
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.controller.admin.questionnaire_task.vo.QuestionnaireTaskPageReqVO;
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<QuestionnaireTaskDO> {
default PageResult<QuestionnaireTaskDO> selectPage(QuestionnaireTaskPageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<QuestionnaireTaskDO>()
.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<String, Object> 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<String, Object> selectTaskStatisticsSummary();
@Select("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 " +
"INNER JOIN prison_questionnaire_record qr ON qt.id = qr.task_id " +
"WHERE qt.deleted = 0 " +
"AND qt.questionnaire_id = #{questionnaireId} " +
"AND qr.area_id IN " +
"<foreach item='id' collection='areaIds' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach> " +
"GROUP BY qt.id, qt.task_name")
List<Map<String, Object>> selectAreaComparisonByQuestionnaire(
@Param("questionnaireId") Long questionnaireId,
@Param("areaIds") List<Long> areaIds);
@Select("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 " +
"INNER JOIN prison_questionnaire_record qr ON qt.id = qr.task_id " +
"WHERE qt.deleted = 0 " +
"AND qr.area_id IN " +
"<foreach item='id' collection='areaIds' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach> " +
"GROUP BY qt.id, qt.task_name")
List<Map<String, Object>> selectAreaComparisonByAreas(
@Param("areaIds") List<Long> areaIds);
}

View File

@ -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, "请选择监区");
}

View File

@ -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;
}
}

View File

@ -76,8 +76,13 @@ public interface AnswerService {
* @param questionnaireId 问卷ID
* @param prisonerId 罪犯ID
* @param answers 答题详情列表
* @param isAgentFill 是否代填
* @param agentOperatorId 代填操作人ID
* @param agentOperatorName 代填操作人姓名
*/
void saveAnswers(Long assessmentRecordId, Long questionnaireId, Long prisonerId, List<AssessmentAnswerSubmitReqVO.AnswerItem> answers);
void saveAnswers(Long assessmentRecordId, Long questionnaireId, Long prisonerId,
List<AssessmentAnswerSubmitReqVO.AnswerItem> answers,
Boolean isAgentFill, Long agentOperatorId, String agentOperatorName);
/**
* 计算客观题得分并更新

View File

@ -13,6 +13,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import cn.iocoder.yudao.module.prison.controller.admin.answer.vo.*;
import cn.iocoder.yudao.module.prison.controller.admin.questionnairerecord.vo.AssessmentAnswerSubmitReqVO;
import cn.iocoder.yudao.module.prison.dal.dataobject.answer.AnswerDO;
@ -113,10 +114,13 @@ public class AnswerServiceImpl implements AnswerService {
@Override
@Transactional(rollbackFor = Exception.class)
public void saveAnswers(Long assessmentRecordId, Long questionnaireId, Long prisonerId,
List<AssessmentAnswerSubmitReqVO.AnswerItem> answers) {
List<AssessmentAnswerSubmitReqVO.AnswerItem> answers,
Boolean isAgentFill, Long agentOperatorId, String agentOperatorName) {
if (CollUtil.isEmpty(answers)) {
return;
}
// 删除该测评已有的旧答题记录
deleteByAssessmentRecordId(assessmentRecordId);
for (AssessmentAnswerSubmitReqVO.AnswerItem answerItem : answers) {
// 获取问题信息
@ -151,6 +155,16 @@ public class AnswerServiceImpl implements AnswerService {
answer.setIsCorrect(isCorrect);
}
// 设置代填信息
if (Boolean.TRUE.equals(isAgentFill)) {
answer.setIsAgentFill(true);
answer.setAgentOperatorId(agentOperatorId);
answer.setAgentOperatorName(agentOperatorName);
answer.setAgentFillTime(LocalDateTime.now());
} else {
answer.setIsAgentFill(false);
}
answerMapper.insert(answer);
}
}

View File

@ -20,7 +20,6 @@ import cn.iocoder.yudao.module.prison.dal.mysql.situation.SituationMapper;
import cn.iocoder.yudao.module.prison.enums.GenderEnum;
import cn.iocoder.yudao.module.prison.service.dashboard.PrisonDashboardService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
@ -30,7 +29,6 @@ import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
@ -202,16 +200,37 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
}
// 入狱和出狱时间
if (prisoner.getImprisonmentDate() != null) {
if (prisoner.getImprisonmentDate() != null && prisoner.getReleaseDate() != null) {
vo.setImprisonmentDate(prisoner.getImprisonmentDate().toString());
vo.setReleaseDate(prisoner.getReleaseDate().toString());
// 计算刑期总天数从入监日期到释放日期
long sentenceDays = ChronoUnit.DAYS.between(prisoner.getImprisonmentDate(), prisoner.getReleaseDate());
vo.setSentenceDays((int) sentenceDays);
// 计算已服刑天数从入监日期到当前日期
vo.setServedDays((int) ChronoUnit.DAYS.between(prisoner.getImprisonmentDate(), LocalDate.now()));
// 计算剩余刑期天数从当前日期到释放日期
long remainingDays = ChronoUnit.DAYS.between(LocalDate.now(), prisoner.getReleaseDate());
vo.setRemainingDays(remainingDays > 0 ? (int) remainingDays : 0);
} else if (prisoner.getImprisonmentDate() != null) {
vo.setImprisonmentDate(prisoner.getImprisonmentDate().toString());
vo.setServedDays((int) ChronoUnit.DAYS.between(prisoner.getImprisonmentDate(), LocalDate.now()));
}
if (prisoner.getReleaseDate() != null) {
vo.setReleaseDate(prisoner.getReleaseDate().toString());
if (prisoner.getReleaseDate() != null) {
vo.setReleaseDate(prisoner.getReleaseDate().toString());
long remainingDays = ChronoUnit.DAYS.between(LocalDate.now(), prisoner.getReleaseDate());
vo.setRemainingDays(remainingDays > 0 ? (int) remainingDays : 0);
}
} else {
vo.setRemainingDays(null);
vo.setSentenceDays(null);
}
// 监区信息简单拼接
vo.setPrisonArea(prisoner.getAreaId() != null ? "监区" + prisoner.getAreaId() : "未分配");
// 监区信息查询实际监区名称
if (prisoner.getAreaId() != null) {
AreaDO area = areaMapper.selectById(prisoner.getAreaId());
vo.setPrisonArea(area != null ? area.getName() : "监区" + prisoner.getAreaId());
} else {
vo.setPrisonArea("未分配");
}
// ==================== 查询关联数据 ====================
@ -233,7 +252,7 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
List<ConsumptionDO> monthlyConsumptions = consumptionMapper.selectList(new LambdaQueryWrapper<ConsumptionDO>()
.eq(ConsumptionDO::getPrisonerId, prisonerId)
.eq(ConsumptionDO::getStatus, 1)
.eq(ConsumptionDO::getType, 1) // 消费类型
// type: 1-购物 2-餐饮 3-医疗 4-通讯 5-其他都属于消费
.apply("YEAR(trade_time) = {0} AND MONTH(trade_time) = {1}", currentYear, currentMonth)
.orderByDesc(ConsumptionDO::getTradeTime));
@ -243,9 +262,9 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
.sum();
// 获取最新余额
Double latestBalance = null;
java.math.BigDecimal latestBalance = null;
if (!monthlyConsumptions.isEmpty() && monthlyConsumptions.get(0).getBalance() != null) {
latestBalance = monthlyConsumptions.get(0).getBalance().doubleValue();
latestBalance = java.math.BigDecimal.valueOf(monthlyConsumptions.get(0).getBalance().doubleValue());
}
if (latestBalance == null) {
ConsumptionDO latestConsumption = consumptionMapper.selectOne(new LambdaQueryWrapper<ConsumptionDO>()
@ -254,7 +273,7 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
.orderByDesc(ConsumptionDO::getTradeTime)
.last("LIMIT 1"));
if (latestConsumption != null && latestConsumption.getBalance() != null) {
latestBalance = latestConsumption.getBalance().doubleValue();
latestBalance = java.math.BigDecimal.valueOf(latestConsumption.getBalance().doubleValue());
}
}
@ -283,6 +302,9 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
.orderByDesc(ConsumptionDO::getTradeTime)
.last("LIMIT 20"));
// 5.2 查询最近6个月汇款记录收入
List<Map<String, Object>> recentRemittances = dashboardMapper.selectRecentRemittances(prisonerId);
// 6. 查询最近6个月的危险评估
List<RiskAssessmentDO> recentAssessments = riskAssessmentMapper.selectList(new LambdaQueryWrapper<RiskAssessmentDO>()
.eq(RiskAssessmentDO::getPrisonerId, prisonerId)
@ -290,14 +312,45 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
.orderByDesc(RiskAssessmentDO::getAssessmentDate)
.last("LIMIT 6"));
// 7. 查询狱情收集心理访谈记录按监区查询
// 8. 查询狱情收集心理访谈记录按监区查询
List<SituationDO> situations = situationMapper.selectList(new LambdaQueryWrapper<SituationDO>()
.eq(SituationDO::getAreaId, prisoner.getAreaId())
.eq(SituationDO::getStatus, 3) // 已处理
.eq(SituationDO::getCategory, 2) // 教育改造类型
.eq(SituationDO::getCategory, 1) // 心理访谈类型
.orderByDesc(SituationDO::getOccurTime)
.last("LIMIT 10"));
// 8. 查询累计违规次数狱情收集-监管安全类型
Long violationCount = situationMapper.selectCount(new LambdaQueryWrapper<SituationDO>()
.eq(SituationDO::getAreaId, prisoner.getAreaId())
.eq(SituationDO::getStatus, 3) // 已处理
.eq(SituationDO::getCategory, 1)); // 监管安全类型
vo.setViolationCount(violationCount != null ? violationCount.intValue() : 0);
// 8.1 查询累计表扬次数狱情收集-表扬类型
Long praiseCount = situationMapper.selectCount(new LambdaQueryWrapper<SituationDO>()
.eq(SituationDO::getAreaId, prisoner.getAreaId())
.eq(SituationDO::getStatus, 3) // 已处理
.eq(SituationDO::getCategory, 2)); // 表扬类型
vo.setPraiseCount(praiseCount != null ? praiseCount.intValue() : 0);
// 9. 查询累计计分考核记录统计加分和扣分次数
List<ScoreDO> allScores = scoreMapper.selectList(new LambdaQueryWrapper<ScoreDO>()
.eq(ScoreDO::getPrisonerId, prisonerId)
.eq(ScoreDO::getStatus, 1)); // 已通过
long rewardCount = allScores.stream()
.filter(s -> s.getRewardScore() != null && s.getRewardScore().doubleValue() > 0)
.count();
long penaltyCount = allScores.stream()
.filter(s -> s.getPenaltyScore() != null && s.getPenaltyScore().doubleValue() > 0)
.count();
vo.setRewardCount((int) rewardCount);
vo.setPenaltyCount((int) penaltyCount);
// 10. 设置累计表扬天数固定返回"-"
vo.setPraiseDays("-");
// ==================== 构建中心数据 ====================
// 左侧数据本月消费奖励惩罚余额
@ -305,7 +358,7 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
double monthlyPenalty = 0;
if (currentMonthScore != null) {
monthlyReward = currentMonthScore.getRewardScore() != null ? currentMonthScore.getRewardScore().doubleValue() : 0;
monthlyPenalty = currentMonthScore.getPenaltyScore() != null ? currentMonthScore.getPenaltyScore().doubleValue() : 0;
monthlyPenalty = currentMonthScore.getPenaltyScore() != null ? Math.abs(currentMonthScore.getPenaltyScore().doubleValue()) : 0;
}
vo.setCenterLeftData(PrisonerDashboardStatsRespVO.CenterLeftData.builder()
.topValue(String.valueOf((int) monthlyTotal))
@ -339,7 +392,7 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
.middleLeftLabel("基础分")
.middleRightValue(String.valueOf((int) monthlyReward))
.middleRightLabel("加分项")
.bottomLeftValue(String.valueOf((int) monthlyPenalty))
.bottomLeftValue(String.valueOf((int) Math.abs(monthlyPenalty)))
.bottomLeftLabel("扣分项")
.bottomRightValue(levelText)
.bottomRightLabel("考核等级")
@ -361,21 +414,59 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
// ==================== 构建消费月度数据 ====================
List<PrisonerDashboardStatsRespVO.MonthlyConsumptionData> monthlyDataList = new ArrayList<>();
// 按月份汇总消费
// 按月份汇总消费支出和汇款收入
Map<String, Double> monthlyConsumptionMap = recentConsumptions.stream()
.filter(c -> c.getTradeTime() != null)
.collect(Collectors.groupingBy(
c -> c.getTradeTime().getYear() + "-" + String.format("%02d", c.getTradeTime().getMonthValue()),
Collectors.summingDouble(c -> c.getTotalAmount() != null ? c.getTotalAmount().doubleValue() : 0)
));
// 解析汇款数据收入
Map<String, Double> monthlyIncomeMap = new java.util.HashMap<>();
for (Map<String, Object> remittance : recentRemittances) {
Object monthObj = remittance.get("month");
Object amountObj = remittance.get("total_amount");
if (monthObj != null && amountObj != null) {
String month = monthObj.toString();
double amount = Double.parseDouble(amountObj.toString());
monthlyIncomeMap.merge(month, amount, Double::sum);
}
}
// 构建月度数据monthlyStandard=支出perCapita=收入
for (Map.Entry<String, Double> entry : monthlyConsumptionMap.entrySet()) {
String month = entry.getKey();
double consumption = entry.getValue();
double income = monthlyIncomeMap.getOrDefault(month, 0.0);
monthlyDataList.add(PrisonerDashboardStatsRespVO.MonthlyConsumptionData.builder()
.category(entry.getKey())
.perCapita(entry.getValue().intValue())
.category(month)
.monthlyStandard((int) consumption) // 支出
.perCapita((int) income) // 收入
.build());
}
// 如果某月只有收入没有消费也添加进去
for (Map.Entry<String, Double> entry : monthlyIncomeMap.entrySet()) {
String month = entry.getKey();
if (!monthlyConsumptionMap.containsKey(month)) {
monthlyDataList.add(PrisonerDashboardStatsRespVO.MonthlyConsumptionData.builder()
.category(month)
.monthlyStandard(0) // 支出
.perCapita(entry.getValue().intValue()) // 收入
.build());
}
}
// 按月份排序从左到右时间正序 1月2月3月...
monthlyDataList.sort((a, b) -> a.getCategory().compareTo(b.getCategory()));
vo.setConsumptionMonthlyData(monthlyDataList);
// ==================== 查询账户余额 ====================
latestBalance = consumptionMapper.selectLatestBalance(prisonerId);
vo.setBalance(latestBalance != null ? latestBalance.intValue() : 0);
// ==================== 构建计分考核记录 ====================
List<PrisonerDashboardStatsRespVO.ScoreRecord> scoreRecords = recentScores.stream()
.map(s -> {
@ -402,11 +493,24 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
List<PrisonerDashboardStatsRespVO.ConsumptionRecord> consumptionRecords = recentConsumptions.stream()
.limit(10)
.map(c -> {
String typeName = c.getType() == 1 ? "消费" : "存款";
// type: 1-购物 2-餐饮 3-医疗 4-通讯 5-其他
String typeName = "消费";
String nameColor = "#f56c6c"; // 红色-消费
if (c.getType() == null) {
typeName = "未知";
} else if (c.getType() == 2) {
typeName = "餐饮";
} else if (c.getType() == 3) {
typeName = "医疗";
} else if (c.getType() == 4) {
typeName = "通讯";
} else if (c.getType() == 5) {
typeName = "其他";
}
return PrisonerDashboardStatsRespVO.ConsumptionRecord.builder()
.date(c.getTradeTime() != null ? c.getTradeTime().toLocalDate().toString() : "")
.name(typeName)
.nameColor(c.getType() == 1 ? "#f56c6c" : "#67c23a")
.nameColor(nameColor)
.category("普通消费")
.amount(c.getTotalAmount() != null ? c.getTotalAmount().intValue() : 0)
.build();
@ -414,16 +518,41 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
.collect(Collectors.toList());
vo.setConsumptionRecords(consumptionRecords);
// ==================== 构建危险评估记录 ====================
List<PrisonerDashboardStatsRespVO.RewardsPunishment> rewardsPunishments = recentAssessments.stream()
.map(a -> {
// ==================== 构建风险评估记录 ====================
List<Map<String, Object>> riskList = dashboardMapper.selectRecentRiskAssessments(prisonerId);
List<PrisonerDashboardStatsRespVO.RewardsPunishment> rewardsPunishments = riskList.stream()
.map(r -> {
Object dateObj = r.get("assessment_date");
Object scoreObj = r.get("overall_score");
Object levelObj = r.get("risk_level");
Object violenceObj = r.get("violence_risk");
Object escapeObj = r.get("escape_risk");
Object selfHarmObj = r.get("self_harm_risk");
Object mentalObj = r.get("mental_state");
Object assessorObj = r.get("assessor_name");
String date = dateObj != null ? dateObj.toString().substring(0, 10) : "";
String localLevelText = "低风险";
if (levelObj != null) {
int level = Integer.parseInt(levelObj.toString());
localLevelText = switch (level) {
case 1 -> "低风险";
case 2 -> "中风险";
case 3 -> "高风险";
case 4 -> "极高风险";
default -> "未知";
};
}
String content = String.format("综合得分: %s | 风险等级: %s",
scoreObj != null ? scoreObj.toString() : "0",
localLevelText);
return PrisonerDashboardStatsRespVO.RewardsPunishment.builder()
.date(a.getAssessmentDate() != null ? a.getAssessmentDate().toString() : "")
.date(date)
.type("danger")
.typeText("危险评估")
.content("暴力倾向:" + (a.getViolenceScore() != null ? a.getViolenceScore() : 0) +
" | 脱逃倾向:" + (a.getEscapeScore() != null ? a.getEscapeScore() : 0) +
" | 自杀倾向:" + (a.getSuicideScore() != null ? a.getSuicideScore() : 0))
.typeText("风险评估")
.content(content)
.build();
})
.collect(Collectors.toList());
@ -440,16 +569,21 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService {
vo.setInterviewRecords(interviewRecords);
// ==================== 构建汇款记录 ====================
List<PrisonerDashboardStatsRespVO.RemittanceRecord> remittanceRecords = recentConsumptions.stream()
.filter(c -> c.getType() == 2) // 存款类型
.limit(10)
.map(c -> PrisonerDashboardStatsRespVO.RemittanceRecord.builder()
.date(c.getTradeTime() != null ? c.getTradeTime().toLocalDate().toString() : "")
.name("存款")
.nameColor("#409eff")
.category("亲属汇款")
.amount(c.getTotalAmount() != null ? c.getTotalAmount().intValue() : 0)
.build())
// 查询最近6个月的汇款记录详情
List<Map<String, Object>> remittanceList = dashboardMapper.selectRecentRemittanceDetails(prisonerId);
List<PrisonerDashboardStatsRespVO.RemittanceRecord> remittanceRecords = remittanceList.stream()
.map(r -> {
Object dateObj = r.get("arrive_date");
Object nameObj = r.get("remitter_name");
Object amountObj = r.get("amount");
return PrisonerDashboardStatsRespVO.RemittanceRecord.builder()
.date(dateObj != null ? dateObj.toString() : "")
.name(nameObj != null ? nameObj.toString() : "")
.nameColor("#67c23a")
.category("汇款")
.amount(amountObj != null ? new java.math.BigDecimal(amountObj.toString()).intValue() : 0)
.build();
})
.collect(Collectors.toList());
vo.setRemittanceRecords(remittanceRecords);

View File

@ -0,0 +1,169 @@
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<Long> ids);
/**
* 获得问卷任务
*
* @param id 任务ID
* @return 问卷任务
*/
QuestionnaireTaskDO getQuestionnaireTask(Long id);
/**
* 获得问卷任务分页
*
* @param pageReqVO 分页查询
* @return 问卷任务分页
*/
PageResult<QuestionnaireTaskDO> 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<QuestionnaireRecordDO> getPendingPrisoners(Long id, PageParam pageReqVO);
/**
* 获取任务的人员填写进度列表
*
* @param id 任务ID
* @return 人员填写进度列表
*/
List<PrisonerProgressRespVO> getPrisonerProgress(Long id);
/**
* 提醒未完成人员
*
* @param id 任务ID
* @return 提醒人数
*/
Integer remindPendingPrisoners(Long id);
/**
* 通知单个人员
*
* @param recordId 记录ID
*/
void notifyPrisoner(Long recordId);
/**
* 重置人员答题记录
*
* @param recordId 记录ID
*/
void resetPrisonerRecord(Long recordId);
/**
* 按监区统计任务完成情况
*
* @param id 任务ID
* @return 监区统计列表
*/
List<TaskAreaStatisticsRespVO> getTaskAreaStatistics(Long id);
/**
* 获取全局任务统计汇总
*
* @return 统计汇总
*/
TaskStatisticsSummaryRespVO getStatisticsSummary();
/**
* 按监区对比分析
*
* @param questionnaireId 问卷ID可选为空则统计所有问卷
* @param areaIds 监区ID列表
* @return 对比数据
*/
List<AreaComparisonRespVO> compareAreasByQuestionnaire(Long questionnaireId, List<Long> areaIds);
}

View File

@ -0,0 +1,556 @@
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.framework.mybatis.core.query.LambdaQueryWrapperX;
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<PrisonerDO> 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<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO>()
.eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getTaskId, id));
questionnaireTaskMapper.deleteById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteQuestionnaireTaskListByIds(List<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
List<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO> records = questionnaireRecordMapper.selectList(
new LambdaQueryWrapper<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO>()
.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<QuestionnaireTaskDO> getQuestionnaireTaskPage(QuestionnaireTaskPageReqVO pageReqVO) {
return questionnaireTaskMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<QuestionnaireTaskDO>()
.likeIfPresent(QuestionnaireTaskDO::getTaskName, pageReqVO.getTaskName())
.eqIfPresent(QuestionnaireTaskDO::getStatus, pageReqVO.getStatus())
.eqIfPresent(QuestionnaireTaskDO::getQuestionnaireId, pageReqVO.getQuestionnaireId())
.betweenIfPresent(QuestionnaireTaskDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(QuestionnaireTaskDO::getCreateTime));
}
@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<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO>()
.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<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO>()
.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<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO>()
.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<String, Object> stats = questionnaireRecordMapper.selectMaps(
new QueryWrapper<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO>()
.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<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO> getPendingPrisoners(Long id, PageParam pageReqVO) {
Page<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
LambdaQueryWrapper<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO> wrapper = new LambdaQueryWrapper<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO>()
.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<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO> result = questionnaireRecordMapper.selectPage(page, wrapper);
return new PageResult<>(result.getRecords(), result.getTotal());
}
@Override
public Integer remindPendingPrisoners(Long id) {
List<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO> pendingRecords = questionnaireRecordMapper.selectList(
new LambdaQueryWrapper<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO>()
.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<PrisonerProgressRespVO> getPrisonerProgress(Long id) {
List<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO> records = questionnaireRecordMapper.selectList(
new LambdaQueryWrapper<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO>()
.eq(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getTaskId, id)
.orderByAsc(cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO::getCreateTime)
);
if (CollUtil.isEmpty(records)) {
return Collections.emptyList();
}
return records.stream().map(record -> {
PrisonerProgressRespVO vo = new PrisonerProgressRespVO();
vo.setId(record.getId());
vo.setPrisonerId(record.getPrisonerId());
vo.setPrisonerNo(record.getPrisonerNo());
vo.setPrisonerName(record.getPrisonerName());
vo.setAreaId(record.getPrisonerAreaId());
vo.setAreaName(record.getPrisonerAreaName());
vo.setStatus(record.getStatus());
vo.setObjectiveScore(record.getObjectiveScore() != null ? record.getObjectiveScore().intValue() : null);
vo.setSubjectiveScore(record.getSubjectiveScore() != null ? record.getSubjectiveScore().intValue() : null);
vo.setTotalScore(record.getTotalScore() != null ? record.getTotalScore().intValue() : null);
vo.setRiskLevel(record.getRiskLevel());
vo.setStartTime(record.getStartTime());
vo.setFinishTime(record.getEndTime());
vo.setDuration(record.getDuration());
return vo;
}).collect(Collectors.toList());
}
@Override
public void notifyPrisoner(Long recordId) {
cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO record = questionnaireRecordMapper.selectById(recordId);
if (record == null) {
throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_RECORD_NOT_EXISTS);
}
// TODO: 发送站内信通知,此处为占位实现
log.info("通知任务 {} 中的罪犯 {} 完成问卷", record.getTaskId(), record.getPrisonerName());
}
@Override
public void resetPrisonerRecord(Long recordId) {
cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO record = questionnaireRecordMapper.selectById(recordId);
if (record == null) {
throw new ServiceException(ErrorCodeConstants.QUESTIONNAIRE_RECORD_NOT_EXISTS);
}
// 重置记录状态为待测评
cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO updateRecord = new cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO();
updateRecord.setId(recordId);
updateRecord.setStatus(QuestionnaireRecordStatusEnum.PENDING.getCode());
updateRecord.setObjectiveScore(null);
updateRecord.setSubjectiveScore(null);
updateRecord.setTotalScore(null);
updateRecord.setRiskLevel(null);
updateRecord.setStartTime(null);
updateRecord.setEndTime(null);
updateRecord.setDuration(null);
questionnaireRecordMapper.updateById(updateRecord);
log.info("重置任务 {} 中的罪犯 {} 答题记录", record.getTaskId(), record.getPrisonerName());
}
@Override
public List<TaskAreaStatisticsRespVO> getTaskAreaStatistics(Long id) {
List<Map<String, Object>> stats = questionnaireRecordMapper.selectMaps(
new QueryWrapper<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO>()
.select("prisoner_area_id", "prisoner_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("prisoner_area_id", "prisoner_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("prisoner_area_id") != null ? ((Number) stat.get("prisoner_area_id")).longValue() : null)
.areaName((String) stat.get("prisoner_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<String, Object> stats = questionnaireTaskMapper.selectMaps(
new QueryWrapper<QuestionnaireTaskDO>()
.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<AreaComparisonRespVO> compareAreasByQuestionnaire(Long questionnaireId, List<Long> areaIds) {
if (CollUtil.isEmpty(areaIds)) {
return Collections.emptyList();
}
List<Map<String, Object>> stats;
if (questionnaireId != null) {
stats = questionnaireTaskMapper.selectAreaComparisonByQuestionnaire(questionnaireId, areaIds);
} else {
stats = questionnaireTaskMapper.selectAreaComparisonByAreas(areaIds);
}
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<PrisonerDO> getTargetPrisoners(QuestionnaireTaskCreateReqVO createReqVO) {
switch (createReqVO.getTargetType()) {
case 1:
return prisonerMapper.selectBatchIds(createReqVO.getPrisonerIds());
case 2:
return prisonerMapper.selectList(
new LambdaQueryWrapper<PrisonerDO>()
.eq(PrisonerDO::getPrisonAreaId, createReqVO.getAreaId())
.eq(PrisonerDO::getStatus, 1)
);
case 3:
return prisonerMapper.selectList(
new LambdaQueryWrapper<PrisonerDO>()
.eq(PrisonerDO::getStatus, 1)
);
default:
return Collections.emptyList();
}
}
private void createRecordsForPrisoners(QuestionnaireTaskDO task, List<PrisonerDO> prisoners) {
QuestionnaireDO questionnaire = questionnaireMapper.selectById(task.getQuestionnaireId());
List<cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO> 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);
}
}

View File

@ -86,6 +86,13 @@ public interface QuestionnaireRecordService {
*/
void submitAnswer(@Valid AssessmentAnswerSubmitReqVO reqVO);
/**
* 代为提交答卷民警代填
*
* @param reqVO 提交信息包含代填操作人信息
*/
void submitAnswerByAgent(@Valid AssessmentAnswerSubmitReqVO reqVO);
/**
* 结束测评
*

View File

@ -161,6 +161,25 @@ public class QuestionnaireRecordServiceImpl implements QuestionnaireRecordServic
@Override
@Transactional(rollbackFor = Exception.class)
public void submitAnswer(AssessmentAnswerSubmitReqVO reqVO) {
submitAnswerInternal(reqVO, false, null, null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submitAnswerByAgent(AssessmentAnswerSubmitReqVO reqVO) {
// 获取当前登录的民警信息
Long currentUserId = SecurityFrameworkUtils.getLoginUserId();
String currentUserName = SecurityFrameworkUtils.getLoginUser().getInfo() != null
? SecurityFrameworkUtils.getLoginUser().getInfo().get("nickname")
: "未知";
submitAnswerInternal(reqVO, true, currentUserId, currentUserName);
}
/**
* 内部提交答卷方法
*/
private void submitAnswerInternal(AssessmentAnswerSubmitReqVO reqVO, Boolean isAgentFill,
Long agentOperatorId, String agentOperatorName) {
QuestionnaireRecordDO record = validateQuestionnaireRecordExists(reqVO.getRecordId());
if (!QuestionnaireRecordStatusEnum.IN_PROGRESS.getStatus().equals(record.getStatus())) {
throw exception(QUESTIONNAIRE_RECORD_STATUS_ERROR);
@ -171,7 +190,8 @@ public class QuestionnaireRecordServiceImpl implements QuestionnaireRecordServic
record.setDuration((int) java.time.Duration.between(record.getStartTime(), LocalDateTime.now()).getSeconds());
}
// 保存答题记录并计算客观题得分
answerService.saveAnswers(reqVO.getRecordId(), record.getQuestionnaireId(), reqVO.getPrisonerId(), reqVO.getAnswers());
answerService.saveAnswers(reqVO.getRecordId(), record.getQuestionnaireId(), reqVO.getPrisonerId(),
reqVO.getAnswers(), isAgentFill, agentOperatorId, agentOperatorName);
// 计算客观题得分
BigDecimal objectiveScore = answerService.calculateObjectiveScore(reqVO.getRecordId());
record.setObjectiveScore(objectiveScore);

View File

@ -0,0 +1,110 @@
-- =====================================================
-- XL监狱综合管理平台 - 问卷代填权限SQL
-- 生成时间: 2026-01-26
-- 功能: 添加问卷代填按钮权限
-- =====================================================
-- =====================================================
-- 方法1: 自动查找并插入(推荐)
-- =====================================================
-- 1. 查找问卷记录管理(assessment-record)的父菜单ID
SELECT @parentId := parent_id
FROM system_menu
WHERE name = '测评记录管理' AND type = 2 AND deleted = 0
LIMIT 1;
-- 如果找不到,尝试查找 questionnaire-record
SELECT @parentId := id
FROM system_menu
WHERE path = 'assessment-record' AND type = 2 AND deleted = 0
LIMIT 1;
-- 2. 插入代填权限
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, status)
SELECT '代为填写', 'prison:questionnaire-record:agent-fill', 3, 8, @parentId, '', '', '', 0
WHERE @parentId IS NOT NULL;
-- 3. 验证插入结果
SELECT * FROM system_menu
WHERE permission = 'prison:questionnaire-record:agent-fill';
-- =====================================================
-- 方法2: 手动指定父菜单ID插入
-- =====================================================
-- 请将 5047 替换为实际的测评记录管理父菜单ID
-- INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, status)
-- VALUES ('代为填写', 'prison:questionnaire-record:agent-fill', 3, 8, 5047, '', '', '', 0);
-- =====================================================
-- 方法3: 完整的查找和插入脚本(包含错误处理)
-- =====================================================
-- 设置临时变量
SET @parentId := NULL;
-- 查找问卷记录管理菜单的父ID
SELECT parent_id INTO @parentId
FROM system_menu
WHERE name = '测评记录管理'
AND type = 2
AND deleted = 0
ORDER BY id DESC
LIMIT 1;
-- 如果没找到尝试通过path查找
IF @parentId IS NULL THEN
SELECT parent_id INTO @parentId
FROM system_menu
WHERE path = 'assessment-record'
AND type = 2
AND deleted = 0
ORDER BY id DESC
LIMIT 1;
END IF;
-- 如果还是没找到,输出提示
SELECT
CASE
WHEN @parentId IS NULL THEN '未找到测评记录管理菜单,请手动检查!'
ELSE CONCAT('找到父菜单ID: ', @parentId, ',即将插入代填权限...')
END AS result;
-- 插入代填权限(使用 INSERT IGNORE 避免重复插入)
INSERT IGNORE INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
VALUES (
'代为填写',
'prison:questionnaire-record:agent-fill',
3, -- type=3 表示按钮
8, -- sort=8 排在最后(根据现有排序调整)
@parentId, -- 父菜单ID
'', -- path
'', -- icon
'', -- component
0, -- status
b'1', -- visible
b'0', -- keep_alive
b'1', -- always_show
'system',
NOW(),
'',
NOW(),
b'0'
);
-- =====================================================
-- 验证结果
-- =====================================================
-- 查看所有 questionnaire-record 相关的权限
SELECT id, name, permission, type, sort, parent_id
FROM system_menu
WHERE (permission LIKE 'prison:questionnaire-record:%'
OR permission LIKE 'prison:assessment-record:%')
AND deleted = 0
ORDER BY parent_id, sort;
-- 查看插入的代填权限
SELECT * FROM system_menu
WHERE permission = 'prison:questionnaire-record:agent-fill';

View File

@ -0,0 +1,35 @@
-- ================================================
-- 代填功能数据库迁移脚本
-- 添加 prison_answer 表的代填相关字段
-- ================================================
-- 检查字段是否已存在,如果不存在则添加
-- 注意MySQL 语法
-- 1. 添加是否代填字段
ALTER TABLE `prison_answer`
ADD COLUMN `is_agent_fill` TINYINT(1) NULL COMMENT '是否代填1-是0-否null-未知' AFTER `duration`;
-- 2. 添加代填操作人ID字段
ALTER TABLE `prison_answer`
ADD COLUMN `agent_operator_id` BIGINT NULL COMMENT '代填操作人ID民警ID' AFTER `is_agent_fill`;
-- 3. 添加代填操作人姓名字段
ALTER TABLE `prison_answer`
ADD COLUMN `agent_operator_name` VARCHAR(100) NULL COMMENT '代填操作人姓名' AFTER `agent_operator_id`;
-- 4. 添加代填时间字段
ALTER TABLE `prison_answer`
ADD COLUMN `agent_fill_time` DATETIME NULL COMMENT '代填时间' AFTER `agent_operator_name`;
-- ================================================
-- 回滚脚本(如需回滚)
-- ================================================
-- ALTER TABLE `prison_answer`
-- DROP COLUMN IF EXISTS `is_agent_fill`;
-- ALTER TABLE `prison_answer`
-- DROP COLUMN IF EXISTS `agent_operator_id`;
-- ALTER TABLE `prison_answer`
-- DROP COLUMN IF EXISTS `agent_operator_name`;
-- ALTER TABLE `prison_answer`
-- DROP COLUMN IF EXISTS `agent_fill_time`;

View File

@ -95,16 +95,15 @@
<artifactId>justauth-spring-boot-starter</artifactId>
</dependency>
<!-- 注释掉微信登录依赖(监狱系统不需要)
<!-- 微信登录依赖 -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
</dependency>
-->
<dependency>
<groupId>com.anji-plus</groupId>