From cea9ed73352ef2df2322322bd5a41d79115e3878 Mon Sep 17 00:00:00 2001 From: tangweijie <877588133@qq.com> Date: Mon, 26 Jan 2026 18:16:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20=E4=BC=98=E5=8C=96=E5=A4=A7?= =?UTF-8?q?=E5=B8=90=E7=BB=9F=E8=AE=A1=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 大帐统计改为显示账户余额,移除出入库卡片 - 柱状图展示收入和支出数据,按月份正序排列 - 奖惩记录从数据库真实查询,区分奖励和惩罚 - 修复惩罚记录显示问题(类型匹配) - 新增查询方法:selectRecentRewardsPunishments、selectLatestBalance - PrisonerDashboardStatsRespVO新增balance字段 - ConsumptionMapper新增selectLatestBalance方法 --- .../vo/PrisonerDashboardStatsRespVO.java | 3 + .../mysql/consumption/ConsumptionMapper.java | 14 ++ .../dashboard/PrisonDashboardMapper.java | 58 ++++++++ .../impl/PrisonDashboardServiceImpl.java | 133 +++++++++++++----- 4 files changed, 172 insertions(+), 36 deletions(-) diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/dashboard/vo/PrisonerDashboardStatsRespVO.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/dashboard/vo/PrisonerDashboardStatsRespVO.java index be195286a2..eb414d207d 100644 --- a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/dashboard/vo/PrisonerDashboardStatsRespVO.java +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/controller/admin/dashboard/vo/PrisonerDashboardStatsRespVO.java @@ -93,6 +93,9 @@ public class PrisonerDashboardStatsRespVO { @Schema(description = "月度消费数据") private List consumptionMonthlyData; + @Schema(description = "账户余额") + private Integer balance; + @Schema(description = "消费汇总") private ConsumptionSummary consumptionSummary; diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/consumption/ConsumptionMapper.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/consumption/ConsumptionMapper.java index 56888c99c1..1553d1c2bf 100644 --- a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/consumption/ConsumptionMapper.java +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/consumption/ConsumptionMapper.java @@ -123,4 +123,18 @@ public interface ConsumptionMapper extends BaseMapperX { @ResultMap("ConsumptionDetailResultMap") List 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); + } diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/dashboard/PrisonDashboardMapper.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/dashboard/PrisonDashboardMapper.java index 4f7411d564..c7dd277b7d 100644 --- a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/dashboard/PrisonDashboardMapper.java +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/dal/mysql/dashboard/PrisonDashboardMapper.java @@ -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,63 @@ 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> 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> selectRecentRemittanceDetails(@Param("prisonerId") Long prisonerId); + + /** + * 查询最近6个月罪犯的奖惩记录 + */ + @Select(""" + SELECT + id, + occur_date, + type, + category, + content + FROM prison_rewards_punishments + WHERE deleted = 0 + AND status = 1 + AND prisoner_id = #{prisonerId} + AND occur_date >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) + ORDER BY occur_date DESC + LIMIT 10 + """) + List> selectRecentRewardsPunishments(@Param("prisonerId") Long prisonerId); + /** * 查询核心指标卡片数据 * diff --git a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/dashboard/impl/PrisonDashboardServiceImpl.java b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/dashboard/impl/PrisonDashboardServiceImpl.java index bfcea53067..09065d373a 100644 --- a/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/dashboard/impl/PrisonDashboardServiceImpl.java +++ b/yudao-module-prison/src/main/java/cn/iocoder/yudao/module/prison/service/dashboard/impl/PrisonDashboardServiceImpl.java @@ -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; @@ -215,8 +213,13 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService { vo.setRemainingDays(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("未分配"); + } // ==================== 查询关联数据 ==================== @@ -238,7 +241,7 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService { List monthlyConsumptions = consumptionMapper.selectList(new LambdaQueryWrapper() .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)); @@ -288,6 +291,9 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService { .orderByDesc(ConsumptionDO::getTradeTime) .last("LIMIT 20")); + // 5.2 查询最近6个月汇款记录(收入) + List> recentRemittances = dashboardMapper.selectRecentRemittances(prisonerId); + // 6. 查询最近6个月的危险评估 List recentAssessments = riskAssessmentMapper.selectList(new LambdaQueryWrapper() .eq(RiskAssessmentDO::getPrisonerId, prisonerId) @@ -295,17 +301,17 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService { .orderByDesc(RiskAssessmentDO::getAssessmentDate) .last("LIMIT 6")); - // 7. 查询狱情收集(心理访谈记录),按监区查询 + // 8. 查询狱情收集(心理访谈记录),按监区查询 List situations = situationMapper.selectList(new LambdaQueryWrapper() .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() - .eq(SituationDO::getPrisonerId, prisonerId) + .eq(SituationDO::getAreaId, prisoner.getAreaId()) .eq(SituationDO::getStatus, 3) // 已处理 .eq(SituationDO::getCategory, 1)); // 监管安全类型 vo.setViolationCount(violationCount != null ? violationCount.intValue() : 0); @@ -334,7 +340,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)) @@ -368,7 +374,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("考核等级") @@ -390,23 +396,59 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService { // ==================== 构建消费月度数据 ==================== List monthlyDataList = new ArrayList<>(); - // 按月份汇总消费 + // 按月份汇总消费(支出)和汇款(收入) Map 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 monthlyIncomeMap = new java.util.HashMap<>(); + for (Map 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 entry : monthlyConsumptionMap.entrySet()) { - Double consumption = entry.getValue(); + String month = entry.getKey(); + double consumption = entry.getValue(); + double income = monthlyIncomeMap.getOrDefault(month, 0.0); monthlyDataList.add(PrisonerDashboardStatsRespVO.MonthlyConsumptionData.builder() - .category(entry.getKey()) - .monthlyStandard(consumption.intValue()) // 支出 = 该月实际消费总额 - .perCapita(consumption.intValue()) // 人均消费 = 该月实际消费总额 + .category(month) + .monthlyStandard((int) consumption) // 支出 + .perCapita((int) income) // 收入 .build()); } + + // 如果某月只有收入没有消费,也添加进去 + for (Map.Entry 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); + // ==================== 查询账户余额 ==================== + java.math.BigDecimal latestBalance = consumptionMapper.selectLatestBalance(prisonerId); + vo.setBalance(latestBalance != null ? latestBalance.intValue() : 0); + // ==================== 构建计分考核记录 ==================== List scoreRecords = recentScores.stream() .map(s -> { @@ -433,11 +475,24 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService { List 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(); @@ -445,16 +500,17 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService { .collect(Collectors.toList()); vo.setConsumptionRecords(consumptionRecords); - // ==================== 构建危险评估记录 ==================== - List rewardsPunishments = recentAssessments.stream() - .map(a -> { + // ==================== 构建奖惩记录 ==================== + List> rewardsPunishmentsList = dashboardMapper.selectRecentRewardsPunishments(prisonerId); + List rewardsPunishments = rewardsPunishmentsList.stream() + .map(rp -> { + Object typeObj = rp.get("type"); + int type = typeObj != null ? Integer.parseInt(typeObj.toString()) : 1; return PrisonerDashboardStatsRespVO.RewardsPunishment.builder() - .date(a.getAssessmentDate() != null ? a.getAssessmentDate().toString() : "") - .type("danger") - .typeText("危险评估") - .content("暴力倾向:" + (a.getViolenceScore() != null ? a.getViolenceScore() : 0) + - " | 脱逃倾向:" + (a.getEscapeScore() != null ? a.getEscapeScore() : 0) + - " | 自杀倾向:" + (a.getSuicideScore() != null ? a.getSuicideScore() : 0)) + .date(rp.get("occur_date") != null ? rp.get("occur_date").toString().substring(0, 10) : "") + .type(type == 1 ? "reward" : "punishment") + .typeText(rp.get("category") != null ? rp.get("category").toString() : (type == 1 ? "表扬奖励" : "惩罚")) + .content(rp.get("content") != null ? rp.get("content").toString() : "") .build(); }) .collect(Collectors.toList()); @@ -471,16 +527,21 @@ public class PrisonDashboardServiceImpl implements PrisonDashboardService { vo.setInterviewRecords(interviewRecords); // ==================== 构建汇款记录 ==================== - List 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> remittanceList = dashboardMapper.selectRecentRemittances(prisonerId); + List 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 ? Integer.parseInt(amountObj.toString()) : 0) + .build(); + }) .collect(Collectors.toList()); vo.setRemittanceRecords(remittanceRecords);