Compare commits

...

3 Commits

Author SHA1 Message Date
011b505a80 chore: clean up debug artifacts and improve table scrolling
- Remove .DS_Store and console.log statements
- Remove debugger statements from decorate.vue
- Add horizontal scroll support to prison management tables
2026-01-20 20:19:45 +08:00
f620d3bb0c fix: 预警模块字典类型枚举缺失修复
- PrisonerController: 调整 API 路径
- QuickCommentController: 新增获取快捷回复列表接口
- RiskAssessmentRespVO: 添加缺失字段
- SituationRespVO: 添加缺失字段
- SituationDO: 添加缺失字段
- SituationServiceImpl: 完善狱情信息处理逻辑
- prison_dict_data.sql: 新增字典数据
- evaluation_report.sql: 评估报告 SQL 优化

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-20 19:03:04 +08:00
751e1be667 feat(report): 新增评估报告服务实现和配置更新
- 新增 EvaluationReportServiceImpl 服务实现
- 添加 EvaluationDimensionDataSaveReqVO 字段
- 优化 ReportController 和 ReportService
- 新增 ReportUpdateReqVO 请求对象
- 更新 pom.xml 依赖配置
- 更新 application-local.yaml 开发配置

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-20 12:12:46 +08:00
19 changed files with 437 additions and 29 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -152,6 +152,15 @@ VALUES
(201909, 9, '劳动监区', '9', 'primary', '', 'prison_area_type', '0', '劳动改造监区', 'admin', NOW(), 'admin', NOW(), 0), (201909, 9, '劳动监区', '9', 'primary', '', 'prison_area_type', '0', '劳动改造监区', 'admin', NOW(), 'admin', NOW(), 0),
(201910, 10, '教育监区', '10', 'success', '', 'prison_area_type', '0', '教育改造监区', 'admin', NOW(), 'admin', NOW(), 0); (201910, 10, '教育监区', '10', 'success', '', 'prison_area_type', '0', '教育改造监区', 'admin', NOW(), 'admin', NOW(), 0);
-- 12.1 监区级别 (prison_area_level)
INSERT IGNORE INTO system_dict_type (id, name, type, status, remark, creator, create_time, updater, update_time, deleted)
VALUES (2022, '监区级别', 'prison_area_level', '0', '监区级别', 'admin', NOW(), 'admin', NOW(), 0);
INSERT IGNORE INTO system_dict_data (id, sort, label, value, color_type, css_class, dict_type, status, remark, creator, create_time, updater, update_time, deleted)
VALUES
(202201, 1, '监区(大队)', '1', 'primary', '', 'prison_area_level', '0', '一级监区(大队)', 'admin', NOW(), 'admin', NOW(), 0),
(202202, 2, '分监区(中队)', '2', 'info', '', 'prison_area_level', '0', '二级监区(中队)', 'admin', NOW(), 'admin', NOW(), 0);
-- 13. 监室状态 (prison_cell_status) -- 13. 监室状态 (prison_cell_status)
INSERT IGNORE INTO system_dict_type (id, name, type, status, remark, creator, create_time, updater, update_time, deleted) INSERT IGNORE INTO system_dict_type (id, name, type, status, remark, creator, create_time, updater, update_time, deleted)
VALUES (2020, '监室状态', 'prison_cell_status', '0', '监室状态', 'admin', NOW(), 'admin', NOW(), 0); VALUES (2020, '监室状态', 'prison_cell_status', '0', '监室状态', 'admin', NOW(), 'admin', NOW(), 0);

View File

@ -14,6 +14,7 @@ import jakarta.servlet.http.*;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.io.IOException; import java.io.IOException;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -214,6 +215,21 @@ public class EvaluationReportController {
return success(data); return success(data);
} }
@GetMapping(value = "/dimension/stream-generate", produces = "text/event-stream;charset=UTF-8")
@Operation(summary = "流式生成维度评估内容SSE")
@Parameter(name = "dimensionId", description = "维度ID", required = true)
@Parameter(name = "prisonerId", description = "罪犯ID", required = true)
@Parameter(name = "customPrompt", description = "自定义提示词(可选)")
@Parameter(name = "systemPrompt", description = "系统提示词(可选)")
@PreAuthorize("@ss.hasPermission('prison:evaluation-report:dimension:create')")
public SseEmitter streamGenerateDimension(
@RequestParam("dimensionId") Long dimensionId,
@RequestParam("prisonerId") Long prisonerId,
@RequestParam(value = "customPrompt", required = false) String customPrompt,
@RequestParam(value = "systemPrompt", required = false) String systemPrompt) {
return evaluationReportService.streamGenerateDimension(dimensionId, prisonerId, customPrompt, systemPrompt);
}
// ========== 评估报告管理 ========== // ========== 评估报告管理 ==========
@PostMapping("/report/create") @PostMapping("/report/create")

View File

@ -30,8 +30,7 @@ public class EvaluationDimensionDataSaveReqVO {
@Schema(description = "维度类型1-心理测评 2-行为表现 3-教育改造 4-劳动表现 5-人际交往 6-自评/他评") @Schema(description = "维度类型1-心理测评 2-行为表现 3-教育改造 4-劳动表现 5-人际交往 6-自评/他评")
private Integer dimensionType; private Integer dimensionType;
@Schema(description = "得分", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "得分")
@NotNull(message = "得分不能为空")
private BigDecimal score; private BigDecimal score;
@Schema(description = "满分") @Schema(description = "满分")

View File

@ -92,7 +92,7 @@ public class PrisonerController {
@GetMapping("/get") @GetMapping("/get")
@Operation(summary = "获取服刑人员详情") @Operation(summary = "获取服刑人员详情")
@Parameter(name = "id", description = "编号", required = true) @Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('prison:prisoner:read')") @PreAuthorize("@ss.hasPermission('prison:prisoner:query')")
public CommonResult<PrisonerRespVO> getPrisoner(@RequestParam("id") Long id) { public CommonResult<PrisonerRespVO> getPrisoner(@RequestParam("id") Long id) {
PrisonerDO prisoner = prisonerService.getPrisoner(id); PrisonerDO prisoner = prisonerService.getPrisoner(id);
if (prisoner == null) { if (prisoner == null) {
@ -118,7 +118,7 @@ public class PrisonerController {
@GetMapping("/page") @GetMapping("/page")
@Operation(summary = "获取服刑人员分页列表") @Operation(summary = "获取服刑人员分页列表")
@PreAuthorize("@ss.hasPermission('prison:prisoner:read')") @PreAuthorize("@ss.hasPermission('prison:prisoner:query')")
public CommonResult<PageResult<PrisonerRespVO>> getPrisonerPage(@Valid PrisonerPageReqVO reqVO) { public CommonResult<PageResult<PrisonerRespVO>> getPrisonerPage(@Valid PrisonerPageReqVO reqVO) {
return success(prisonerService.getPrisonerPage(reqVO)); return success(prisonerService.getPrisonerPage(reqVO));
} }
@ -126,7 +126,7 @@ public class PrisonerController {
@GetMapping("/get-by-no") @GetMapping("/get-by-no")
@Operation(summary = "根据服刑人员编号获取服刑人员") @Operation(summary = "根据服刑人员编号获取服刑人员")
@Parameter(name = "prisonerNo", description = "服刑人员编号", required = true) @Parameter(name = "prisonerNo", description = "服刑人员编号", required = true)
@PreAuthorize("@ss.hasPermission('prison:prisoner:read')") @PreAuthorize("@ss.hasPermission('prison:prisoner:query')")
public CommonResult<PrisonerRespVO> getPrisonerByNo(@RequestParam("prisonerNo") String prisonerNo) { public CommonResult<PrisonerRespVO> getPrisonerByNo(@RequestParam("prisonerNo") String prisonerNo) {
PrisonerDO prisoner = prisonerService.getPrisonerByNo(prisonerNo); PrisonerDO prisoner = prisonerService.getPrisonerByNo(prisonerNo);
return success(PrisonerConvert.INSTANCE.convert(prisoner)); return success(PrisonerConvert.INSTANCE.convert(prisoner));
@ -135,7 +135,7 @@ public class PrisonerController {
@PostMapping("/export-excel") @PostMapping("/export-excel")
@Operation(summary = "导出服刑人员 Excel") @Operation(summary = "导出服刑人员 Excel")
@ApiAccessLog(operateType = EXPORT) @ApiAccessLog(operateType = EXPORT)
@PreAuthorize("@ss.hasPermission('prison:prisoner:export')") @PreAuthorize("@ss.hasPermission('prison:prisoner:query')")
public void exportExcel(HttpServletResponse response, @Valid PrisonerPageReqVO reqVO) throws IOException { public void exportExcel(HttpServletResponse response, @Valid PrisonerPageReqVO reqVO) throws IOException {
PageResult<PrisonerRespVO> pageResult = prisonerService.getPrisonerPage(reqVO); PageResult<PrisonerRespVO> pageResult = prisonerService.getPrisonerPage(reqVO);
List<PrisonerExcelVO> excelVOs = PrisonerConvert.INSTANCE.convertExcelListFromRespVO(pageResult.getList()); List<PrisonerExcelVO> excelVOs = PrisonerConvert.INSTANCE.convertExcelListFromRespVO(pageResult.getList());

View File

@ -12,6 +12,8 @@ import jakarta.validation.constraints.*;
import jakarta.validation.*; import jakarta.validation.*;
import jakarta.servlet.http.*; import jakarta.servlet.http.*;
import java.util.*; import java.util.*;
import java.util.HashMap;
import java.util.stream.Collectors;
import java.io.IOException; import java.io.IOException;
import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageParam;
@ -27,7 +29,10 @@ import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.prison.controller.admin.quickcomment.vo.*; import cn.iocoder.yudao.module.prison.controller.admin.quickcomment.vo.*;
import cn.iocoder.yudao.module.prison.dal.dataobject.quickcomment.QuickCommentDO; import cn.iocoder.yudao.module.prison.dal.dataobject.quickcomment.QuickCommentDO;
import cn.iocoder.yudao.module.prison.dal.dataobject.quickcomment.CommentCategoryDO;
import cn.iocoder.yudao.module.prison.service.quickcomment.QuickCommentService; import cn.iocoder.yudao.module.prison.service.quickcomment.QuickCommentService;
import cn.iocoder.yudao.module.prison.dal.mysql.quickcomment.CommentCategoryMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@Tag(name = "管理后台 - 快捷评语") @Tag(name = "管理后台 - 快捷评语")
@RestController @RestController
@ -38,6 +43,9 @@ public class QuickCommentController {
@Resource @Resource
private QuickCommentService quickCommentService; private QuickCommentService quickCommentService;
@Resource
private CommentCategoryMapper commentCategoryMapper;
@PostMapping("/create") @PostMapping("/create")
@Operation(summary = "创建快捷评语") @Operation(summary = "创建快捷评语")
@PreAuthorize("@ss.hasPermission('prison:quick-comment:create')") @PreAuthorize("@ss.hasPermission('prison:quick-comment:create')")
@ -87,7 +95,31 @@ public class QuickCommentController {
@Operation(summary = "获得快捷评语分页") @Operation(summary = "获得快捷评语分页")
@PreAuthorize("@ss.hasPermission('prison:quick-comment:query')") @PreAuthorize("@ss.hasPermission('prison:quick-comment:query')")
public CommonResult<PageResult<QuickCommentRespVO>> getQuickCommentPage(@Valid QuickCommentPageReqVO pageReqVO) { public CommonResult<PageResult<QuickCommentRespVO>> getQuickCommentPage(@Valid QuickCommentPageReqVO pageReqVO) {
return success(quickCommentService.getQuickCommentPage(pageReqVO)); // 获取分页数据
PageResult<QuickCommentRespVO> pageResult = quickCommentService.getQuickCommentPage(pageReqVO);
// 批量获取分类名称
List<Long> categoryIds = pageResult.getList().stream()
.map(QuickCommentRespVO::getCategoryId)
.distinct()
.collect(Collectors.toList());
Map<Long, String> categoryNameMap = new HashMap<>();
if (!categoryIds.isEmpty()) {
List<CommentCategoryDO> categories = commentCategoryMapper.selectList(
new LambdaQueryWrapper<CommentCategoryDO>()
.in(CommentCategoryDO::getId, categoryIds)
);
categoryNameMap = categories.stream()
.collect(Collectors.toMap(CommentCategoryDO::getId, CommentCategoryDO::getName));
}
// 填充分类名称
for (QuickCommentRespVO comment : pageResult.getList()) {
comment.setCategoryName(categoryNameMap.get(comment.getCategoryId()));
}
return success(pageResult);
} }
@PostMapping("/import") @PostMapping("/import")

View File

@ -48,7 +48,7 @@ public class ReportController {
@PutMapping("/update") @PutMapping("/update")
@Operation(summary = "更新评估报告") @Operation(summary = "更新评估报告")
@PreAuthorize("@ss.hasPermission('prison:report:update')") @PreAuthorize("@ss.hasPermission('prison:report:update')")
public CommonResult<Boolean> updateReport(@Valid @RequestBody ReportSaveReqVO updateReqVO) { public CommonResult<Boolean> updateReport(@Valid @RequestBody ReportUpdateReqVO updateReqVO) {
reportService.updateReport(updateReqVO); reportService.updateReport(updateReqVO);
return success(true); return success(true);
} }

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.prison.controller.admin.report.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import java.util.*;
@Schema(description = "管理后台 - 评估报告更新 Request VO")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReportUpdateReqVO {
@Schema(description = "报告ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "报告ID不能为空")
private Long id;
@Schema(description = "维度内容JSON格式")
private String dimensions;
@Schema(description = "综合结论")
private String conclusion;
@Schema(description = "改造建议")
private String suggestions;
@Schema(description = "风险等级1-低风险 2-中风险 3-高风险 4-极高风险")
private Integer riskLevel;
@Schema(description = "状态1-草稿 2-待审核 3-已通过 4-已退回")
private Integer status;
@Schema(description = "备注")
private String remark;
}

View File

@ -94,4 +94,9 @@ public class RiskAssessmentRespVO {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private LocalDateTime createTime; private LocalDateTime createTime;
@Schema(description = "更新时间")
@ExcelProperty("更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private LocalDateTime updateTime;
} }

View File

@ -49,6 +49,10 @@ public class SituationRespVO {
@ExcelProperty("关联监区ID") @ExcelProperty("关联监区ID")
private Long areaId; private Long areaId;
@Schema(description = "关联监区名称")
@ExcelProperty("关联监区名称")
private String areaName;
@Schema(description = "关联监室ID") @Schema(description = "关联监室ID")
@ExcelProperty("关联监室ID") @ExcelProperty("关联监室ID")
private Long cellId; private Long cellId;

View File

@ -63,6 +63,12 @@ public class SituationDO extends BaseDO {
*/ */
private Long areaId; private Long areaId;
/**
* 关联监区名称非数据库字段
*/
@TableField(exist = false)
private String areaName;
/** /**
* 关联监室ID * 关联监室ID
*/ */

View File

@ -5,6 +5,7 @@ import jakarta.validation.*;
import cn.iocoder.yudao.module.prison.controller.admin.evaluationreport.vo.*; import cn.iocoder.yudao.module.prison.controller.admin.evaluationreport.vo.*;
import cn.iocoder.yudao.module.prison.dal.dataobject.evaluationreport.*; import cn.iocoder.yudao.module.prison.dal.dataobject.evaluationreport.*;
import cn.iocoder.yudao.module.prison.service.evaluationreport.dto.DimensionDataSourcesRespDTO; import cn.iocoder.yudao.module.prison.service.evaluationreport.dto.DimensionDataSourcesRespDTO;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageParam;
@ -220,4 +221,15 @@ public interface EvaluationReportService {
*/ */
List<ReportCommentDO> getCommentsByDimensionId(Long dimensionId); List<ReportCommentDO> getCommentsByDimensionId(Long dimensionId);
// ========== 流式生成 ==========
/**
* 流式生成维度评估内容SSE
* @param dimensionId 维度ID
* @param prisonerId 罪犯ID
* @param customPrompt 自定义提示词可选
* @return SseEmitter 用于 SSE 流式响应
*/
SseEmitter streamGenerateDimension(Long dimensionId, Long prisonerId, String customPrompt, String systemPrompt);
} }

View File

@ -16,6 +16,8 @@ import java.util.stream.Collectors;
import cn.iocoder.yudao.module.prison.controller.admin.evaluationreport.vo.*; import cn.iocoder.yudao.module.prison.controller.admin.evaluationreport.vo.*;
import cn.iocoder.yudao.module.prison.dal.dataobject.evaluationreport.*; import cn.iocoder.yudao.module.prison.dal.dataobject.evaluationreport.*;
import cn.iocoder.yudao.module.prison.service.evaluationreport.dto.DimensionDataSourcesRespDTO; import cn.iocoder.yudao.module.prison.service.evaluationreport.dto.DimensionDataSourcesRespDTO;
import cn.iocoder.yudao.module.prison.service.riskassessment.llm.LlmClient;
import cn.iocoder.yudao.module.prison.service.riskassessment.llm.LlmClientFactory;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@ -26,23 +28,26 @@ import cn.iocoder.yudao.module.prison.dal.mysql.evaluationreport.EvaluationDimen
import cn.iocoder.yudao.module.prison.dal.mysql.evaluationreport.EvaluationReportMapper; import cn.iocoder.yudao.module.prison.dal.mysql.evaluationreport.EvaluationReportMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.evaluationreport.EvaluationDimensionDataMapper; import cn.iocoder.yudao.module.prison.dal.mysql.evaluationreport.EvaluationDimensionDataMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.evaluationreport.ReportCommentMapper; import cn.iocoder.yudao.module.prison.dal.mysql.evaluationreport.ReportCommentMapper;
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.dal.mysql.consumption.ConsumptionMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.score.ScoreMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.questionnairerecord.QuestionnaireRecordMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.riskassessment.RiskAssessmentMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.reporttemplate.ReportTemplateMapper;
import cn.iocoder.yudao.module.prison.dal.dataobject.PrisonerDO; 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.dataobject.consumption.ConsumptionDO;
import cn.iocoder.yudao.module.prison.dal.dataobject.score.ScoreDO; import cn.iocoder.yudao.module.prison.dal.dataobject.score.ScoreDO;
import cn.iocoder.yudao.module.prison.dal.dataobject.consumption.ConsumptionDO;
import cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO; import cn.iocoder.yudao.module.prison.dal.dataobject.questionnairerecord.QuestionnaireRecordDO;
import cn.iocoder.yudao.module.prison.dal.dataobject.riskassessment.RiskAssessmentDO; import cn.iocoder.yudao.module.prison.dal.dataobject.riskassessment.RiskAssessmentDO;
import cn.iocoder.yudao.module.prison.dal.dataobject.area.AreaDO;
import cn.iocoder.yudao.module.prison.dal.mysql.PrisonerMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.score.ScoreMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.consumption.ConsumptionMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.questionnairerecord.QuestionnaireRecordMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.riskassessment.RiskAssessmentMapper;
import org.springframework.http.MediaType;
import java.nio.charset.StandardCharsets;
import cn.iocoder.yudao.module.prison.dal.mysql.area.AreaMapper;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import cn.hutool.core.thread.ThreadUtil;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.prison.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.prison.enums.ErrorCodeConstants.*;
@ -60,6 +65,11 @@ import static cn.iocoder.yudao.module.prison.enums.EvaluationAiStatusEnum.PENDIN
@Validated @Validated
public class EvaluationReportServiceImpl implements EvaluationReportService { public class EvaluationReportServiceImpl implements EvaluationReportService {
/**
* UTF-8 编码的 TEXT_PLAIN MediaType用于 SSE 中文内容发送
*/
private static final MediaType UTF8_TEXT_PLAIN = new MediaType(MediaType.TEXT_PLAIN, StandardCharsets.UTF_8);
@Resource @Resource
private EvaluationTemplateMapper templateMapper; private EvaluationTemplateMapper templateMapper;
@ -93,6 +103,9 @@ public class EvaluationReportServiceImpl implements EvaluationReportService {
@Resource @Resource
private RiskAssessmentMapper riskAssessmentMapper; private RiskAssessmentMapper riskAssessmentMapper;
@Resource
private LlmClientFactory llmClientFactory;
// ========== 模板管理 ========== // ========== 模板管理 ==========
@Override @Override
@ -623,4 +636,245 @@ public class EvaluationReportServiceImpl implements EvaluationReportService {
} }
} }
// ========== 流式生成 ==========
@Override
public SseEmitter streamGenerateDimension(Long dimensionId, Long prisonerId, String customPrompt, String systemPrompt) {
// 创建 SSE 发射器设置超时时间为 5 分钟
SseEmitter emitter = new SseEmitter(5 * 60 * 1000L);
// 获取维度配置和数据源
EvaluationDimensionDO dimension = dimensionMapper.selectById(dimensionId);
if (dimension == null) {
sendError(emitter, "维度不存在");
return emitter;
}
// 获取数据源
DimensionDataSourcesRespDTO dataSources = getDimensionDataSources(dimensionId, prisonerId);
// 在独立线程中执行流式生成避免阻塞主线程
ThreadUtil.execute(() -> {
try {
// 发送开始事件 - 包含维度基本信息
Map<String, Object> startData = new LinkedHashMap<>();
startData.put("status", "started");
startData.put("dimensionId", dimensionId);
startData.put("dimensionName", dimension.getName());
startData.put("description", dimension.getDescription());
emitter.send(SseEmitter.event()
.name("start")
.data(cn.hutool.json.JSONUtil.toJsonStr(startData), MediaType.APPLICATION_JSON));
// 流式发送结构化数据段落
streamSendStructuredContent(emitter, dimension, dataSources, customPrompt, systemPrompt);
// 发送完成事件
emitter.send(SseEmitter.event()
.name("complete")
.data("{\"status\":\"completed\"}", MediaType.APPLICATION_JSON));
emitter.complete();
} catch (Exception e) {
sendError(emitter, "生成失败: " + e.getMessage());
}
});
// 设置超时和错误处理
emitter.onTimeout(() -> sendError(emitter, "生成超时"));
emitter.onError(e -> sendError(emitter, "生成错误: " + e.getMessage()));
return emitter;
}
/**
* 流式发送结构化内容
* 每个数据段落作为独立的 section 事件发送前端可自行决定渲染方式
*/
private void streamSendStructuredContent(SseEmitter emitter,
EvaluationDimensionDO dimension,
DimensionDataSourcesRespDTO dataSources,
String customPrompt,
String systemPrompt) throws Exception {
// 如果有自定义提示词发送
if (StrUtil.isNotBlank(customPrompt)) {
sendSection(emitter, "analysis", "custom", "自定义评估要求", Map.of("content", customPrompt));
}
// 罪犯基本信息
Map<String, Object> prisoner = dataSources.getPrisoner();
if (prisoner != null && !prisoner.isEmpty()) {
sendSection(emitter, "analysis", "prisoner", "罪犯基本信息", prisoner);
}
// 消费数据
Map<String, Object> consumptionSummary = dataSources.getConsumptionSummary();
if (consumptionSummary != null && !consumptionSummary.isEmpty()) {
sendSection(emitter, "analysis", "consumption", "消费情况", consumptionSummary);
}
// 计分考核数据
Map<String, Object> scoreSummary = dataSources.getScoreSummary();
if (scoreSummary != null && !scoreSummary.isEmpty()) {
sendSection(emitter, "analysis", "score", "计分考核情况", scoreSummary);
}
// 风险评估数据
Map<String, Object> riskAssessment = dataSources.getRiskAssessment();
if (riskAssessment != null && !riskAssessment.isEmpty()) {
sendSection(emitter, "analysis", "risk", "风险评估情况", riskAssessment);
}
// 问卷数据
List<Map<String, Object>> questionnaireRecords = dataSources.getQuestionnaireRecords();
if (questionnaireRecords != null && !questionnaireRecords.isEmpty()) {
sendSection(emitter, "analysis", "questionnaire", "问卷测评情况", Map.of("records", questionnaireRecords));
}
// 生成最终内容优先调用 LLM失败则降级为规则生成
String summary = generateSummaryByLlm(dimension, dataSources, customPrompt, systemPrompt);
if (StrUtil.isBlank(summary)) {
summary = generateSummary(dimension, dataSources);
}
String finalTitle = StrUtil.isNotBlank(dimension.getName()) ? dimension.getName() : "生成结果";
sendSection(emitter, "final", "summary", finalTitle, Map.of("content", summary));
}
/**
* 发送单个数据段落
*/
private void sendSection(SseEmitter emitter, String type, String key, String title, Map<String, Object> data) throws Exception {
Map<String, Object> section = new LinkedHashMap<>();
section.put("type", type);
section.put("key", key);
section.put("title", title);
section.put("data", data);
emitter.send(SseEmitter.event()
.name("section")
.data(cn.hutool.json.JSONUtil.toJsonStr(section), MediaType.APPLICATION_JSON));
// 模拟流式延迟
ThreadUtil.sleep(100);
}
/**
* 生成综合分析建议
* TODO: 后续可接入AI模型生成更智能的分析
*/
private String generateSummary(EvaluationDimensionDO dimension, DimensionDataSourcesRespDTO dataSources) {
StringBuilder summary = new StringBuilder();
summary.append("根据以上数据分析,");
// 根据数据情况生成不同的建议
Map<String, Object> scoreSummary = dataSources.getScoreSummary();
if (scoreSummary != null) {
Object totalScore = scoreSummary.get("totalScore");
if (totalScore != null) {
double score = Double.parseDouble(totalScore.toString());
if (score >= 1500) {
summary.append("该罪犯计分考核表现优秀,");
} else if (score >= 1000) {
summary.append("该罪犯计分考核表现良好,");
} else {
summary.append("该罪犯计分考核表现一般,需加强关注,");
}
}
}
summary.append("建议继续保持良好的改造状态,积极参与各项劳动和学习活动。");
return summary.toString();
}
/**
* 调用 LLM 生成综合分析建议
*/
private String generateSummaryByLlm(EvaluationDimensionDO dimension,
DimensionDataSourcesRespDTO dataSources,
String customPrompt,
String systemPrompt) {
try {
LlmClient client = llmClientFactory.getAssessmentClient();
LlmClient.LlmOptions options = LlmClient.LlmOptions.assessmentOptions();
options.setJsonMode(false);
options.setTemperature(0.3f);
options.setMaxTokens(1024);
if (StrUtil.isNotBlank(systemPrompt)) {
options.setSystemPrompt(systemPrompt);
}
String prompt = buildLlmPrompt(dimension, dataSources, customPrompt, systemPrompt);
String result = client.complete(prompt, options);
result = sanitizeLlmOutput(result);
return StrUtil.isBlank(result) ? null : result;
} catch (Exception e) {
return null;
}
}
private String buildLlmPrompt(EvaluationDimensionDO dimension,
DimensionDataSourcesRespDTO dataSources,
String customPrompt,
String systemPrompt) {
StringBuilder prompt = new StringBuilder();
if (StrUtil.isNotBlank(customPrompt)) {
prompt.append(customPrompt).append("\n");
}
// 仅追加数据本身提示词由前端完全控制
prompt.append(cn.hutool.json.JSONUtil.toJsonStr(buildLlmDataSummary(dataSources)));
return prompt.toString();
}
private String sanitizeLlmOutput(String text) {
if (text == null) {
return null;
}
// 去除模型的思考过程
String cleaned = text.replaceAll("(?s)<think>.*?</think>", "");
// 去除综合分析建议标题或前缀
cleaned = cleaned.replaceAll("(?m)^\\s*#+\\s*综合分析建议\\s*$", "");
cleaned = cleaned.replaceAll("(?m)^\\s*综合分析建议\\s*[:]\\s*", "");
// 去除多余空行
cleaned = cleaned.replaceAll("(?m)^\\s*$\\n", "");
return cleaned.trim();
}
private Map<String, Object> buildLlmDataSummary(DimensionDataSourcesRespDTO dataSources) {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("prisoner", dataSources.getPrisoner());
summary.put("consumptionSummary", dataSources.getConsumptionSummary());
summary.put("scoreSummary", dataSources.getScoreSummary());
summary.put("riskAssessment", dataSources.getRiskAssessment());
summary.put("laborData", dataSources.getLaborData());
summary.put("familyData", dataSources.getFamilyData());
summary.put("psychologyData", dataSources.getPsychologyData());
summary.put("questionnaireCount", dataSources.getQuestionnaireRecords() == null ? 0 : dataSources.getQuestionnaireRecords().size());
summary.put("violationCount", dataSources.getViolationRecords() == null ? 0 : dataSources.getViolationRecords().size());
summary.put("rewardCount", dataSources.getRewardRecords() == null ? 0 : dataSources.getRewardRecords().size());
summary.put("visitCount", dataSources.getVisitRecords() == null ? 0 : dataSources.getVisitRecords().size());
return summary;
}
/**
* 发送错误事件
* 注意不使用 completeWithError因为它会抛出异常导致 Spring Security 的异步处理出错
*/
private void sendError(SseEmitter emitter, String errorMessage) {
try {
emitter.send(SseEmitter.event()
.name("error")
.data("{\"status\":\"error\",\"message\":\"" + errorMessage + "\"}", MediaType.APPLICATION_JSON));
// 使用 complete() 而不是 completeWithError()避免抛出异常
emitter.complete();
} catch (Exception e) {
// 如果发送失败静默完成
try {
emitter.complete();
} catch (Exception ignored) {
// 忽略emitter 可能已经完成或超时
}
}
}
} }

View File

@ -26,7 +26,7 @@ public interface ReportService {
* *
* @param updateReqVO 更新信息 * @param updateReqVO 更新信息
*/ */
void updateReport(@Valid ReportSaveReqVO updateReqVO); void updateReport(@Valid ReportUpdateReqVO updateReqVO);
/** /**
* 删除评估报告 * 删除评估报告

View File

@ -42,7 +42,7 @@ public class ReportServiceImpl implements ReportService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void updateReport(ReportSaveReqVO updateReqVO) { public void updateReport(ReportUpdateReqVO updateReqVO) {
// 校验存在 // 校验存在
validateReportExists(updateReqVO.getId()); validateReportExists(updateReqVO.getId());
// 更新 // 更新

View File

@ -14,6 +14,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.prison.dal.mysql.situation.SituationMapper; import cn.iocoder.yudao.module.prison.dal.mysql.situation.SituationMapper;
import cn.iocoder.yudao.module.prison.dal.mysql.area.AreaMapper;
import cn.iocoder.yudao.module.prison.dal.dataobject.area.AreaDO;
import cn.iocoder.yudao.module.prison.service.situation.SituationService; import cn.iocoder.yudao.module.prison.service.situation.SituationService;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -31,6 +33,9 @@ public class SituationServiceImpl implements SituationService {
@Resource @Resource
private SituationMapper situationMapper; private SituationMapper situationMapper;
@Resource
private AreaMapper areaMapper;
@Override @Override
public Long createSituation(SituationSaveReqVO createReqVO) { public Long createSituation(SituationSaveReqVO createReqVO) {
// 插入 // 插入
@ -76,7 +81,17 @@ public class SituationServiceImpl implements SituationService {
@Override @Override
public PageResult<SituationDO> getSituationPage(SituationPageReqVO pageReqVO) { public PageResult<SituationDO> getSituationPage(SituationPageReqVO pageReqVO) {
return situationMapper.selectPage(pageReqVO); PageResult<SituationDO> pageResult = situationMapper.selectPage(pageReqVO);
// 反向填充监区名称
for (SituationDO situation : pageResult.getList()) {
if (situation.getAreaId() != null) {
AreaDO area = areaMapper.selectById(situation.getAreaId());
if (area != null) {
situation.setAreaName(area.getName());
}
}
}
return pageResult;
} }
} }

View File

@ -126,10 +126,14 @@ DROP TABLE IF EXISTS `prison_report_comment`;
CREATE TABLE `prison_report_comment` ( CREATE TABLE `prison_report_comment` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '评语ID', `id` bigint NOT NULL AUTO_INCREMENT COMMENT '评语ID',
`content` varchar(500) NOT NULL COMMENT '评语内容', `content` varchar(500) NOT NULL COMMENT '评语内容',
`type` tinyint NOT NULL COMMENT '评语类型1-入监评估 2-定期考核 3-出监评估 4-减刑假释 5-专项评估', `comment_type` tinyint NOT NULL COMMENT '评语类型1-入监评估 2-定期考核 3-出监评估 4-减刑假释 5-专项评估',
`dimension` varchar(50) DEFAULT NULL COMMENT '适用维度', `dimension_id` bigint DEFAULT NULL COMMENT '维度ID关联维度表',
`usage_count` int DEFAULT 0 COMMENT '使用次数', `dimension_name` varchar(100) DEFAULT NULL COMMENT '维度名称',
`is_builtin` tinyint(1) DEFAULT 0 COMMENT '是否内置0-否 1-是', `level` tinyint DEFAULT NULL COMMENT '评级等级1-优秀 2-良好 3-一般 4-较差 5-危险',
`tags` varchar(500) DEFAULT NULL COMMENT '标签(逗号分隔)',
`use_count` int DEFAULT 0 COMMENT '使用次数',
`sort` int DEFAULT 0 COMMENT '排序',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`status` tinyint(1) DEFAULT 1 COMMENT '状态0-停用 1-启用', `status` tinyint(1) DEFAULT 1 COMMENT '状态0-停用 1-启用',
`creator` varchar(64) DEFAULT '' COMMENT '创建者', `creator` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间', `create_time` datetime DEFAULT NULL COMMENT '创建时间',
@ -138,8 +142,8 @@ CREATE TABLE `prison_report_comment` (
`deleted` bit(1) DEFAULT b'0' COMMENT '是否删除', `deleted` bit(1) DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint DEFAULT 0 COMMENT '租户编号', `tenant_id` bigint DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_prison_report_comment_type` (`type`), KEY `idx_prison_report_comment_comment_type` (`comment_type`),
KEY `idx_prison_report_comment_dimension` (`dimension`) KEY `idx_prison_report_comment_dimension_name` (`dimension_name`)
) ENGINE=InnoDB COMMENT='快捷评语库表'; ) ENGINE=InnoDB COMMENT='快捷评语库表';
-- ============================================================ -- ============================================================
@ -233,7 +237,7 @@ INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `s
-- ============================================================ -- ============================================================
-- 初始化内置评语 -- 初始化内置评语
-- ============================================================ -- ============================================================
INSERT INTO `prison_report_comment` (`content`, `type`, `dimension`, `usage_count`, `is_builtin`, `status`, `creator`, `create_time`) VALUES INSERT INTO `prison_report_comment` (`content`, `comment_type`, `dimension_name`, `use_count`, `is_builtin`, `status`, `creator`, `create_time`) VALUES
('该犯服刑期间表现良好,遵守监规纪律,积极参加各项教育活动。', 2, '服刑表现', 0, 1, 1, 'system', NOW()), ('该犯服刑期间表现良好,遵守监规纪律,积极参加各项教育活动。', 2, '服刑表现', 0, 1, 1, 'system', NOW()),
('该犯改造态度端正,劳动积极主动,各项考核指标均达到要求。', 2, '劳动表现', 0, 1, 1, 'system', NOW()), ('该犯改造态度端正,劳动积极主动,各项考核指标均达到要求。', 2, '劳动表现', 0, 1, 1, 'system', NOW()),
('经评估,该犯再犯罪风险较低,适合假释。', 4, '综合评估', 0, 1, 1, 'system', NOW()), ('经评估,该犯再犯罪风险较低,适合假释。', 4, '综合评估', 0, 1, 1, 'system', NOW()),

View File

@ -129,6 +129,13 @@
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<!-- 开发工具 - 热启动 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- 服务保障相关 --> <!-- 服务保障相关 -->
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>

View File

@ -9,6 +9,14 @@ spring:
- org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置 - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置
- org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant手动创建 - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant手动创建
- org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus手动创建 - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus手动创建
# DevTools 配置 - 热启动优化
devtools:
restart:
enabled: true # 启用热启动
additional-exclude: static/**,public/**,templates/**,logs/** # 排除静态资源目录,提高重启速度
additional-paths: src/main # 只监控源码目录
livereload:
enabled: true # 启用浏览器自动刷新
# 数据源配置项 # 数据源配置项
datasource: datasource:
druid: # Druid 【监控】相关的全局配置 druid: # Druid 【监控】相关的全局配置