“mitao”
2026-01-21 5b56af3a13d7f36ad0d5b24fac83d14d7b622954
feat:新增委托方大屏首页接口
6个文件已删除
13个文件已添加
10个文件已修改
3749 ■■■■■ 已修改文件
pom.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/DashboardStatisticsController.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/ProjectController.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/enums/BigGiftPackageOrderStatusEnum.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/enums/ICBCRespOrderErrorEnum.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/enums/IOptionVo.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/enums/PhysicalGoodsOrderStatusEnum.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/enums/ProjectType.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/enums/ProjectUsageType.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/enums/UnionPayStatusEnum.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/enums/UserTypeEnum.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/EvalResultMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PlanMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/Check.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/EvalResult.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/OnDuty.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/Project.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ClientService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ProjectCategoryCalculator.java 567 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ClientServiceImpl.java 495 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PlanServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/ClientDashboardQuery.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/ClientDashboardVO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/FixedAssetDashboardVO.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/MachineryStatisticsVO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/OngoingProjectDashboardVO.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/ProjectMapPointVO.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/ProjectScoreData.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/jiashida.sql 1929 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml
@@ -16,7 +16,7 @@
        <ruoyi.version>3.8.6</ruoyi.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <java.version>17</java.version>
        <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
        <druid.version>1.2.16</druid.version>
        <bitwalker.version>1.21</bitwalker.version>
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/DashboardStatisticsController.java
@@ -9,6 +9,8 @@
import com.ruoyi.system.model.*;
import com.ruoyi.system.query.StoryListQuery;
import com.ruoyi.system.service.*;
import com.ruoyi.system.vo.ClientDashboardQuery;
import com.ruoyi.system.vo.ClientDashboardVO;
import com.ruoyi.system.vo.ScreenUserVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -125,7 +127,18 @@
        return R.ok(accidentBrief);
    }
    /**
     * 获取委托方大屏首页统计数据
     *
     * @param query 查询参数
     * @return 统计数据
     */
    @PostMapping("/index")
    @ApiOperation("获取委托方大屏首页统计数据")
    public R<ClientDashboardVO> index(@RequestBody ClientDashboardQuery query) {
        ClientDashboardVO dashboard = clientService.getClientDashboard(query);
        return R.ok(dashboard);
    }
    
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/ProjectController.java
@@ -12,7 +12,6 @@
import com.ruoyi.system.vo.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.var;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.parsing.Problem;
ruoyi-system/src/main/java/com/ruoyi/system/enums/BigGiftPackageOrderStatusEnum.java
File was deleted
ruoyi-system/src/main/java/com/ruoyi/system/enums/ICBCRespOrderErrorEnum.java
File was deleted
ruoyi-system/src/main/java/com/ruoyi/system/enums/IOptionVo.java
New file
@@ -0,0 +1,17 @@
package com.ruoyi.system.enums;
public interface IOptionVo {
    /**
     * 获取选项编码
     * @return
     */
    String getOptionCode();
    /**
     * 获取选项名称
     * @return
     */
    String getOptionName();
}
ruoyi-system/src/main/java/com/ruoyi/system/enums/PhysicalGoodsOrderStatusEnum.java
File was deleted
ruoyi-system/src/main/java/com/ruoyi/system/enums/ProjectType.java
New file
@@ -0,0 +1,74 @@
package com.ruoyi.system.enums;
import io.swagger.annotations.ApiModel;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@ApiModel("项目类型")
@Getter
public enum ProjectType implements IOptionVo {
    HOUSE_CONSTRUCTION("房建工程"), // 房建工程
    MUNICIPAL_ADMINISTRATION("市政工程"), // 市政工程
    FIXED_ASSETS("固定资产"); // 固定资产
    final String desc;
    private List<ProjectUsageType> projectUsageTypes;
    ProjectType(String desc) {
        this.desc = desc;
        this.projectUsageTypes = new ArrayList<>();
    }
    public static ProjectType getByDesc(String desc) {
        for (ProjectType projectType : ProjectType.values()) {
            if (projectType.getDesc().equals(desc)) {
                return projectType;
            }
        }
        return null;
    }
    public void linkPurposeType(ProjectUsageType projectUsageType) {
        this.projectUsageTypes.add(projectUsageType);
    }
    static {
        // 房建工程关联用途类型
        HOUSE_CONSTRUCTION.linkPurposeType(ProjectUsageType.RESIDENTIAL);
        HOUSE_CONSTRUCTION.linkPurposeType(ProjectUsageType.COMMERCIAL);
        // 市政工程关联用途类型
        MUNICIPAL_ADMINISTRATION.linkPurposeType(ProjectUsageType.ROAD);
        MUNICIPAL_ADMINISTRATION.linkPurposeType(ProjectUsageType.BRIDGE);
        MUNICIPAL_ADMINISTRATION.linkPurposeType(ProjectUsageType.TUNNEL_WORKS);
        // 固定资产关联用途类型
        FIXED_ASSETS.linkPurposeType(ProjectUsageType.RETAIL);
        FIXED_ASSETS.linkPurposeType(ProjectUsageType.FOOD);
        FIXED_ASSETS.linkPurposeType(ProjectUsageType.EDUCATION);
        FIXED_ASSETS.linkPurposeType(ProjectUsageType.MEDICAL_BEAUTY);
        FIXED_ASSETS.linkPurposeType(ProjectUsageType.ENTERTAINMENT);
        FIXED_ASSETS.linkPurposeType(ProjectUsageType.HOTEL);
        FIXED_ASSETS.linkPurposeType(ProjectUsageType.REFIT);
        FIXED_ASSETS.linkPurposeType(ProjectUsageType.PUBLIC_PLACE);
        FIXED_ASSETS.linkPurposeType(ProjectUsageType.ENERGY_STATION);
        FIXED_ASSETS.linkPurposeType(ProjectUsageType.MULTI_FUNCTION_COMPLEX);
    }
    @Override
    public String getOptionCode() {
        return name();
    }
    @Override
    public String getOptionName() {
        return desc;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/enums/ProjectUsageType.java
New file
@@ -0,0 +1,54 @@
package com.ruoyi.system.enums;
import io.swagger.annotations.ApiModel;
import lombok.Getter;
@ApiModel("项目用途类型")
@Getter
public enum ProjectUsageType implements IOptionVo {
    // 房建工程用途类型
    RESIDENTIAL("住宅"),
    COMMERCIAL("商业"),
    // 市政工程用途类型
    ROAD("道路"),
    BRIDGE("桥梁"),
    TUNNEL_WORKS("隧道"),
    // 固定资产用途类型
    RETAIL("仓储"),
    FOOD("餐饮"),
    EDUCATION("教育"),
    MEDICAL_BEAUTY("医疗美容"),
    ENTERTAINMENT("娱乐"),
    HOTEL("酒店"),
    REFIT("翻新改造"),
    PUBLIC_PLACE("公共场所"),
    ENERGY_STATION("能源站"),
    MULTI_FUNCTION_COMPLEX("综合体");
    private final String desc;
    ProjectUsageType(String desc) {
        this.desc = desc;
    }
    public static ProjectUsageType getByDesc(String desc) {
        for (ProjectUsageType projectUsageType : ProjectUsageType.values()) {
            if (projectUsageType.getDesc().equals(desc)) {
                return projectUsageType;
            }
        }
        return null;
    }
    @Override
    public String getOptionCode() {
        return name();
    }
    @Override
    public String getOptionName() {
        return desc;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/enums/UnionPayStatusEnum.java
File was deleted
ruoyi-system/src/main/java/com/ruoyi/system/enums/UserTypeEnum.java
File was deleted
ruoyi-system/src/main/java/com/ruoyi/system/mapper/EvalResultMapper.java
New file
@@ -0,0 +1,14 @@
package com.ruoyi.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.system.model.EvalResult;
/**
 * 评估结果 Mapper 接口
 *
 * @author MiTao
 * @date 2026-01-21
 */
public interface EvalResultMapper extends BaseMapper<EvalResult> {
    // 继承 BaseMapper 即可,MyBatis-Plus 提供基础 CRUD
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PlanMapper.java
@@ -1,6 +1,9 @@
package com.ruoyi.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.system.model.Plan;
/**
 * <p>
 *  Mapper 接口
ruoyi-system/src/main/java/com/ruoyi/system/model/Check.java
@@ -1,10 +1,7 @@
package com.ruoyi.system.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
@@ -81,7 +78,17 @@
    @ApiModelProperty("检查标题")
    private String title;
    /**
     * 创建者
     */
    @TableField("creator_id")
    @ApiModelProperty("创建人id")
    private Integer creatorId;
    @TableField("deleted")
    @TableLogic
    @ApiModelProperty("是否删除 false正常true删除")
    private Boolean deleted;
    @TableField(exist = false)
    @ApiModelProperty("计划名称")
ruoyi-system/src/main/java/com/ruoyi/system/model/EvalResult.java
New file
@@ -0,0 +1,106 @@
package com.ruoyi.system.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
 * 评估结果实体类
 *
 * @author MiTao
 * @date 2026-01-21
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("eval_result")
@ApiModel(value = "EvalResult对象", description = "评估结果")
public class EvalResult extends Model<EvalResult> {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    @ApiModelProperty(value = "评价类型:fuzzy-模糊评价,score-打分评价")
    @TableField("type")
    private String type;
    @ApiModelProperty(value = "安全检查得分")
    @TableField("score")
    private Float score;
    @ApiModelProperty(value = "设备评估结果映射(JSON字符串)")
    @TableField("device_res_map")
    private String deviceResMap;
    @ApiModelProperty(value = "系统评估结果(JSON字符串)")
    @TableField("system_res")
    private String systemRes;
    @ApiModelProperty(value = "关联的检查任务ID")
    @TableField("check_task_id")
    private Integer checkTaskId;
    @ApiModelProperty(value = "关联的委托方ID")
    @TableField("client_id")
    private Integer clientId;
    @ApiModelProperty(value = "关联的计划ID")
    @TableField("plan_id")
    private Integer planId;
    @ApiModelProperty(value = "关联的项目ID")
    @TableField("project_id")
    private Integer projectId;
    @ApiModelProperty(value = "创建时间")
    @TableField("create_time")
    private Date createTime;
    @ApiModelProperty(value = "更新时间")
    @TableField("update_time")
    private Date updateTime;
    @ApiModelProperty(value = "逻辑删除标记")
    @TableField("deleted")
    private Boolean deleted;
    @ApiModelProperty(value = "名称")
    @TableField("name")
    private String name;
    @ApiModelProperty(value = "是否包含设备评估结果")
    @TableField("contain_device_res")
    private Boolean containDeviceRes;
    @ApiModelProperty(value = "是否包含系统评估结果")
    @TableField("contain_system_res")
    private Boolean containSystemRes;
    @ApiModelProperty(value = "显示分数")
    @TableField("display_score")
    private Float displayScore;
    @ApiModelProperty(value = "风险等级")
    @TableField("risk_level")
    private Integer riskLevel;
    @ApiModelProperty(value = "创建者ID")
    @TableField("creator_id")
    private Integer creatorId;
    @ApiModelProperty(value = "错误计数")
    @TableField("error_counts")
    private String errorCounts;
}
ruoyi-system/src/main/java/com/ruoyi/system/model/OnDuty.java
@@ -1,11 +1,9 @@
package com.ruoyi.system.model;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.*;
import java.time.LocalDate;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@@ -75,5 +73,12 @@
    @TableField("create_time")
    private LocalDateTime createTime;
    @TableField("creator_id")
    private Integer creatorId;
    @TableField("deleted")
    @TableLogic
    private Boolean deleted;
}
ruoyi-system/src/main/java/com/ruoyi/system/model/Project.java
@@ -65,11 +65,5 @@
     */
    private String permission;
    private String scale;
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ClientService.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.system.model.Client;
import com.ruoyi.system.vo.ClientDashboardQuery;
import com.ruoyi.system.vo.ClientDashboardVO;
/**
 * <p>
@@ -13,4 +15,11 @@
 */
public interface ClientService extends IService<Client> {
    /**
     * 获取委托方大屏首页统计数据
     *
     * @param query 查询参数
     * @return 统计数据
     */
    ClientDashboardVO getClientDashboard(ClientDashboardQuery query);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ProjectCategoryCalculator.java
New file
@@ -0,0 +1,567 @@
package com.ruoyi.system.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.system.mapper.*;
import com.ruoyi.system.model.*;
import com.ruoyi.system.model.Record;
import com.ruoyi.system.vo.ProjectScoreData;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
 * 项目类别计算服务
 *
 * @author MiTao
 * @date 2026-01-21
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ProjectCategoryCalculator {
    private final EvalResultMapper evalResultMapper;
    private final ClientParamMapper clientParamMapper;
    private final CheckMapper checkMapper;
    private final RecordMapper recordMapper;
    /**
     * 计算在建工程项目类别
     *
     * @param projects  项目列表
     * @param clientId  委托方ID
     * @param startDate 开始日期
     * @param endDate   结束日期
     * @return 项目ID -> 类别(A/B/C)的映射
     */
    public Map<Integer, String> calculateOngoingProjectCategories(
            List<Project> projects, Long clientId, Date startDate, Date endDate) {
        Map<Integer, String> categoryMap = new HashMap<>();
        if (projects == null || projects.isEmpty()) {
            return categoryMap;
        }
        try {
            // 1. 获取委托方配置
            ClientParam clientParam = getClientParam(clientId);
            if (clientParam == null) {
                log.warn("委托方配置不存在,clientId: {}", clientId);
                return categoryMap;
            }
            // 2. 解析在建工程配置
            JSONObject config = parseJsonConfig(
                    clientParam.getUnderConstructionProject(), "在建工程配置");
            if (config == null) {
                return categoryMap;
            }
            // 3. 查询项目关联的检查任务
            List<Integer> projectIds = projects.stream()
                    .map(Project::getId)
                    .collect(Collectors.toList());
            LambdaQueryWrapper<Check> checkWrapper = new LambdaQueryWrapper<>();
            checkWrapper.in(Check::getProjectId, projectIds)
                    .ge(Check::getTime, startDate)
                    .le(Check::getTime, endDate);
            List<Check> checks = checkMapper.selectList(checkWrapper);
            if (checks.isEmpty()) {
                log.info("项目没有检查记录,projectIds: {}", projectIds);
                return categoryMap;
            }
            List<Integer> checkTaskIds = checks.stream()
                    .map(Check::getId)
                    .collect(Collectors.toList());
            // 4. 根据评价类型查询评估结果
            Integer commentType = clientParam.getCommentType();
            String evalType = (commentType != null && commentType == 1) ? "fuzzy" : "score";
            LambdaQueryWrapper<EvalResult> evalWrapper = new LambdaQueryWrapper<>();
            evalWrapper.eq(EvalResult::getType, evalType)
                    .in(EvalResult::getCheckTaskId, checkTaskIds)
                    .eq(EvalResult::getDeleted, false);
            List<EvalResult> evalResults = evalResultMapper.selectList(evalWrapper);
            if (evalResults.isEmpty()) {
                log.info("项目没有评估结果,projectIds: {}", projectIds);
                return categoryMap;
            }
            // 5. 计算项目平均分
            Map<Integer, ProjectScoreData> projectScoreMap = calculateProjectScores(
                    checks, evalResults);
            // 6. 根据配置判断项目类别
            for (Map.Entry<Integer, ProjectScoreData> entry : projectScoreMap.entrySet()) {
                Integer projectId = entry.getKey();
                ProjectScoreData scoreData = entry.getValue();
                try {
                    String category = determineOngoingCategory(scoreData, config);
                    if (category != null) {
                        categoryMap.put(projectId, category);
                    }
                } catch (Exception e) {
                    log.error("判断项目类别失败,projectId: {}", projectId, e);
                }
            }
        } catch (Exception e) {
            log.error("计算在建工程项目类别失败,clientId: {}", clientId, e);
        }
        return categoryMap;
    }
    /**
     * 计算固定资产项目类别
     *
     * @param projects  项目列表
     * @param clientId  委托方ID
     * @param startDate 开始日期
     * @param endDate   结束日期
     * @return 项目ID -> 类别(A/B/C)的映射
     */
    public Map<Integer, String> calculateFixedAssetCategories(
            List<Project> projects, Long clientId, Date startDate, Date endDate) {
        Map<Integer, String> categoryMap = new HashMap<>();
        if (projects == null || projects.isEmpty()) {
            return categoryMap;
        }
        try {
            // 1. 获取委托方配置
            ClientParam clientParam = getClientParam(clientId);
            if (clientParam == null) {
                log.warn("委托方配置不存在,clientId: {}", clientId);
                return categoryMap;
            }
            // 2. 解析固定资产配置
            JSONObject config = parseJsonConfig(
                    clientParam.getAssetProject(), "固定资产配置");
            if (config == null) {
                return categoryMap;
            }
            // 3. 查询项目关联的检查任务
            List<Integer> projectIds = projects.stream()
                    .map(Project::getId)
                    .collect(Collectors.toList());
            LambdaQueryWrapper<Check> checkWrapper = new LambdaQueryWrapper<>();
            checkWrapper.in(Check::getProjectId, projectIds)
                    .ge(Check::getTime, startDate)
                    .le(Check::getTime, endDate);
            List<Check> checks = checkMapper.selectList(checkWrapper);
            if (checks.isEmpty()) {
                log.info("项目没有检查记录,projectIds: {}", projectIds);
                return categoryMap;
            }
            List<Integer> checkTaskIds = checks.stream()
                    .map(Check::getId)
                    .collect(Collectors.toList());
            // 4. 查询隐患记录(record_type='error')
            LambdaQueryWrapper<Record> recordWrapper = new LambdaQueryWrapper<>();
            recordWrapper.eq(Record::getRecordType, "error")
                    .in(Record::getCheckTaskId, checkTaskIds)
                    .eq(Record::getDeleted, false);
            List<Record> hazardRecords = recordMapper.selectList(recordWrapper);
            if (hazardRecords.isEmpty()) {
                log.info("项目没有隐患记录,projectIds: {}", projectIds);
                return categoryMap;
            }
            // 5. 按项目分组统计隐患等级数量
            Map<Integer, Integer> checkToProjectMap = checks.stream()
                    .collect(Collectors.toMap(Check::getId, Check::getProjectId));
            Map<Integer, Map<Integer, Integer>> projectHazardMap = new HashMap<>();
            for (Record record : hazardRecords) {
                Integer projectId = checkToProjectMap.get(record.getCheckTaskId());
                if (projectId == null) {
                    continue;
                }
                Integer level = record.getLevel();
                if (level == null) {
                    continue;
                }
                projectHazardMap
                        .computeIfAbsent(projectId, k -> new HashMap<>())
                        .merge(level, 1, Integer::sum);
            }
            // 6. 根据配置判断项目类别
            for (Map.Entry<Integer, Map<Integer, Integer>> entry : projectHazardMap.entrySet()) {
                Integer projectId = entry.getKey();
                Map<Integer, Integer> hazardCounts = entry.getValue();
                try {
                    String category = determineFixedAssetCategory(hazardCounts, config);
                    if (category != null) {
                        categoryMap.put(projectId, category);
                    }
                } catch (Exception e) {
                    log.error("判断项目类别失败,projectId: {}", projectId, e);
                }
            }
        } catch (Exception e) {
            log.error("计算固定资产项目类别失败,clientId: {}", clientId, e);
        }
        return categoryMap;
    }
    /**
     * 获取委托方配置
     *
     * @param clientId 委托方ID
     * @return 委托方配置
     */
    private ClientParam getClientParam(Long clientId) {
        if (clientId == null) {
            return null;
        }
        LambdaQueryWrapper<ClientParam> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ClientParam::getClientId, clientId.intValue());
        return clientParamMapper.selectOne(wrapper);
    }
    /**
     * 解析 JSON 配置
     *
     * @param jsonStr    JSON 字符串
     * @param configName 配置名称
     * @return JSON 对象
     */
    private JSONObject parseJsonConfig(String jsonStr, String configName) {
        if (!StringUtils.hasText(jsonStr)) {
            log.warn("{}为空", configName);
            return null;
        }
        try {
            return JSON.parseObject(jsonStr);
        } catch (Exception e) {
            log.error("解析{}失败: {}", configName, jsonStr, e);
            return null;
        }
    }
    /**
     * 计算项目评分数据
     *
     * @param checks      检查任务列表
     * @param evalResults 评估结果列表
     * @return 项目ID -> 评分数据的映射
     */
    private Map<Integer, ProjectScoreData> calculateProjectScores(
            List<Check> checks, List<EvalResult> evalResults) {
        Map<Integer, ProjectScoreData> projectScoreMap = new HashMap<>();
        // 按 check_task_id 关联到 project_id
        Map<Integer, Integer> checkToProjectMap = checks.stream()
                .collect(Collectors.toMap(Check::getId, Check::getProjectId));
        // 按项目分组评估结果
        Map<Integer, List<EvalResult>> projectEvalMap = evalResults.stream()
                .filter(e -> checkToProjectMap.containsKey(e.getCheckTaskId()))
                .collect(Collectors.groupingBy(e -> checkToProjectMap.get(e.getCheckTaskId())));
        // 计算每个项目的平均分
        for (Map.Entry<Integer, List<EvalResult>> entry : projectEvalMap.entrySet()) {
            Integer projectId = entry.getKey();
            List<EvalResult> results = entry.getValue();
            // 计算安全检查平均分
            double avgSafetyScore = results.stream()
                    .filter(e -> e.getScore() != null)
                    .mapToDouble(EvalResult::getScore)
                    .average()
                    .orElse(0.0);
            // 计算设备检查平均分(需要解析 device_res_map)
            List<Float> deviceScores = results.stream()
                    .filter(e -> StringUtils.hasText(e.getDeviceResMap()))
                    .map(e -> this.calculateDeviceScore(e.getDeviceResMap()))
                    .filter(score -> score != null && score > 0)
                    .collect(Collectors.toList());
            ProjectScoreData scoreData = new ProjectScoreData();
            scoreData.setProjectId(projectId);
            scoreData.setSafetyScore((float) avgSafetyScore);
            if (!deviceScores.isEmpty()) {
                double avgDeviceScore = deviceScores.stream()
                        .mapToDouble(Float::doubleValue)
                        .average()
                        .orElse(0.0);
                scoreData.setDeviceScore((float) avgDeviceScore);
                scoreData.setHasDeviceScore(true);
            } else {
                scoreData.setHasDeviceScore(false);
            }
            projectScoreMap.put(projectId, scoreData);
        }
        return projectScoreMap;
    }
    /**
     * 计算设备得分
     * <p>
     * device_res_map 字段的 JSON 结构(来自 estate_expert 项目):
     * <pre>
     * {
     *   "设备ID1": {
     *     "content": {
     *       "data": {
     *         "score": 85.5,
     *         "weight": 100.0,
     *         "total": 100.0,
     *         "totalDeduct": 14.5,
     *         "totalErrorCounts": [1, 2, 0, 0]
     *       },
     *       "children": [...]
     *     }
     *   },
     *   "设备ID2": {
     *     "content": {
     *       "data": {
     *         "score": 90.0,
     *         ...
     *       },
     *       "children": [...]
     *     }
     *   }
     * }
     * </pre>
     * <p>
     * 解析路径: deviceMap[deviceId].content.data.score
     * <p>
     * 计算逻辑: 提取所有设备的 score 字段并计算平均值
     *
     * @param deviceResMapJson device_res_map 字段的 JSON 字符串
     * @return 设备平均分,如果无法计算则返回 null
     */
    private static Float calculateDeviceScore(String deviceResMapJson) {
        try {
            // 1. 解析 JSON 字符串
            JSONObject deviceMap = JSON.parseObject(deviceResMapJson);
            if (deviceMap == null || deviceMap.isEmpty()) {
                log.debug("设备评估结果映射为空");
                return null;
            }
            // 2. 提取所有设备的分数
            List<Float> scores = new ArrayList<>();
            for (String deviceId : deviceMap.keySet()) {
                try {
                    // 获取设备数据对象
                    JSONObject deviceData = deviceMap.getJSONObject(deviceId);
                    if (deviceData == null) {
                        log.debug("设备数据为空,deviceId: {}", deviceId);
                        continue;
                    }
                    // 获取 content 对象
                    JSONObject content = deviceData.getJSONObject("content");
                    if (content == null) {
                        log.debug("设备 content 为空,deviceId: {}", deviceId);
                        continue;
                    }
                    // 获取 data 对象
                    JSONObject data = content.getJSONObject("data");
                    if (data == null) {
                        log.debug("设备 data 为空,deviceId: {}", deviceId);
                        continue;
                    }
                    // 获取 score 字段
                    if (!data.containsKey("score")) {
                        log.debug("设备 data 中不包含 score 字段,deviceId: {}", deviceId);
                        continue;
                    }
                    Float score = data.getFloat("score");
                    if (score != null && score > 0) {
                        scores.add(score);
                        log.debug("成功提取设备得分,deviceId: {}, score: {}", deviceId, score);
                    }
                } catch (Exception e) {
                    log.warn("提取设备得分失败,deviceId: {}", deviceId, e);
                }
            }
            // 3. 计算平均分
            if (scores.isEmpty()) {
                log.debug("没有有效的设备得分数据");
                return null;
            }
            float avgScore = (float) scores.stream()
                    .mapToDouble(Float::doubleValue)
                    .average()
                    .orElse(0.0);
            log.debug("设备平均得分计算完成,设备数量: {}, 平均分: {}", scores.size(), avgScore);
            return avgScore;
        } catch (Exception e) {
            log.warn("解析设备得分失败,deviceResMapJson: {}", deviceResMapJson, e);
            return null;
        }
    }
    public static void main(String[] args) {
        String json = "{\"47\": {\"content\": {\"id\": 0, \"data\": {\"score\": 83.111336, \"weight\": 1.0, \"include\": true, \"riskLevel\": \"P3\", \"membershipList\": [0.025, 0.5, 0.3917, 0.0833, 0.0], \"totalErrorCounts\": [0, 1, 0, 0]}, \"name\": \"设备体系-标准-层级\", \"path\": \"/\", \"children\": {\"0\": {\"id\": 0, \"data\": {\"score\": 83.111336, \"weight\": 1.0, \"include\": true, \"riskLevel\": \"P3\", \"membershipList\": [0.025, 0.5, 0.3917, 0.0833, 0.0], \"totalErrorCounts\": [0, 1, 0, 0]}, \"name\": \"塔式起重机\", \"path\": \"/0\", \"children\": {\"1\": {\"id\": 1, \"data\": {\"score\": 83.111336, \"weight\": 1.0, \"include\": true, \"riskLevel\": \"P3\", \"membershipList\": [0.025, 0.5, 0.3917, 0.0833, 0.0], \"totalErrorCounts\": [0, 1, 0, 0]}, \"name\": \"塔式起重机实体(市站)\", \"path\": \"/0/1\", \"children\": {\"0\": {\"id\": 0, \"data\": {\"score\": 83.111336, \"weight\": 1.0, \"include\": true, \"riskLevel\": \"P3\", \"membershipList\": [0.025, 0.5, 0.3917, 0.0833, 0.0], \"totalErrorCounts\": [0, 1, 0, 0]}, \"name\": \"一般项目\", \"path\": \"/0/1/0\", \"children\": {\"0\": {\"id\": 0, \"data\": {\"score\": 0.0, \"weight\": 1.0, \"include\": true, \"riskLevel\": null, \"membershipList\": null, \"totalErrorCounts\": [0, 1, 0, 0]}, \"name\": \"基础与轨道\", \"path\": \"/0/1/0/0\", \"children\": {}, \"childIdCnt\": 0}}, \"childIdCnt\": 0}}, \"childIdCnt\": 0}}, \"childIdCnt\": 0}}, \"childIdCnt\": 0}}}";
        Float v = calculateDeviceScore(json);
        System.out.println(v);
    }
    /**
     * 根据在建工程配置确定项目类别
     *
     * @param scoreData 项目评分数据
     * @param config    在建工程配置
     * @return 项目类别(A/B/C)
     */
    private String determineOngoingCategory(ProjectScoreData scoreData, JSONObject config) {
        if (config == null) {
            log.warn("在建工程配置为空,无法计算项目类别");
            return null;
        }
        // 解析配置
        Float thresholdA = config.getFloat("a");
        Float thresholdB = config.getFloat("b");
        Float thresholdC = config.getFloat("c");
        Float safeProp = config.getFloat("safe_prop");
        Float deviceProp = config.getFloat("device_prop");
        if (thresholdA == null || thresholdB == null || thresholdC == null
                || safeProp == null || deviceProp == null) {
            log.warn("在建工程配置不完整: {}", config.toJSONString());
            return null;
        }
        // 计算综合得分
        float finalScore;
        if (scoreData.isHasDeviceScore()) {
            // 有设备得分:按比例计算
            finalScore = scoreData.getSafetyScore() * (safeProp / 100.0f)
                    + scoreData.getDeviceScore() * (deviceProp / 100.0f);
        } else {
            // 无设备得分:安全检查得分按 100% 计算
            finalScore = scoreData.getSafetyScore();
        }
        // 根据阈值判断类别(从高到低判断)
        if (finalScore >= thresholdC) {
            return "A";
        } else if (finalScore >= thresholdB) {
            return "B";
        } else if (finalScore >= thresholdA) {
            return "C";
        } else {
            return null; // 不满足任何类别
        }
    }
    /**
     * 根据固定资产配置确定项目类别
     *
     * @param hazardCounts 隐患等级统计 Map<level, count>
     * @param config       固定资产配置 JSON
     * @return 项目类别(A/B/C)
     */
    private String determineFixedAssetCategory(
            Map<Integer, Integer> hazardCounts, JSONObject config) {
        if (config == null) {
            log.warn("固定资产配置为空,无法计算项目类别");
            return null;
        }
        // 按优先级顺序判断:A -> B -> C
        String[] categories = {"a", "b", "c"};
        for (String category : categories) {
            JSONArray categoryRules = config.getJSONArray(category);
            if (categoryRules == null || categoryRules.isEmpty()) {
                continue;
            }
            // 检查是否满足该类别的任一规则(OR 条件)
            boolean matchesCategory = false;
            for (int i = 0; i < categoryRules.size(); i++) {
                JSONArray rule = categoryRules.getJSONArray(i);
                if (rule == null) {
                    continue;
                }
                // 检查规则中的所有条件(AND 条件)
                boolean matchesRule = true;
                for (int j = 0; j < rule.size(); j++) {
                    JSONObject condition = rule.getJSONObject(j);
                    if (condition == null) {
                        continue;
                    }
                    // 条件格式:{"0": 5} 表示 level=0 的隐患数量 >= 5
                    for (String levelStr : condition.keySet()) {
                        Integer level = Integer.parseInt(levelStr);
                        Integer requiredCount = condition.getInteger(levelStr);
                        Integer actualCount = hazardCounts.getOrDefault(level, 0);
                        if (actualCount < requiredCount) {
                            matchesRule = false;
                            break;
                        }
                    }
                    if (!matchesRule) {
                        break;
                    }
                }
                if (matchesRule) {
                    matchesCategory = true;
                    break;
                }
            }
            if (matchesCategory) {
                return category.toUpperCase(); // 返回 "A", "B", "C"
            }
        }
        return null; // 不满足任何类别
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ClientServiceImpl.java
@@ -1,11 +1,37 @@
package com.ruoyi.system.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.system.mapper.CheckMapper;
import com.ruoyi.system.mapper.ClientMapper;
import com.ruoyi.system.mapper.DeviceUsageMapper;
import com.ruoyi.system.mapper.PlanMapper;
import com.ruoyi.system.mapper.ProjectMapper;
import com.ruoyi.system.mapper.RecordMapper;
import com.ruoyi.system.model.Check;
import com.ruoyi.system.model.Client;
import com.ruoyi.system.model.DeviceUsage;
import com.ruoyi.system.model.Plan;
import com.ruoyi.system.model.Project;
import com.ruoyi.system.model.Record;
import com.ruoyi.system.service.ClientService;
import com.ruoyi.system.service.ProjectCategoryCalculator;
import com.ruoyi.system.vo.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
 * <p>
@@ -16,6 +42,475 @@
 * @since 2026-01-12
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> implements ClientService {
    private final ProjectMapper projectMapper;
    private final PlanMapper planMapper;
    private final CheckMapper checkMapper;
    private final DeviceUsageMapper deviceUsageMapper;
    private final RecordMapper recordMapper;
    private final ProjectCategoryCalculator projectCategoryCalculator;
    @Override
    public ClientDashboardVO getClientDashboard(ClientDashboardQuery query) {
        ClientDashboardVO vo = new ClientDashboardVO();
        vo.setProjectCategory(query.getProjectCategory());
        // 获取委托方ID(如果未传则需要从当前登录用户获取,这里暂时使用传入的值)
        Long clientId = query.getClientId();
        // 获取日期范围
        Date startDate = getStartDate(query.getStartDate());
        Date endDate = getEndDate(query.getEndDate());
        // 根据项目类型进行不同的统计
        if ("ongoing".equals(query.getProjectCategory())) {
            // 在建工程统计
            OngoingProjectDashboardVO ongoingVO = calculateOngoingProjectStatistics(clientId, startDate, endDate);
            vo.setOngoingProject(ongoingVO);
        } else if ("fixed".equals(query.getProjectCategory())) {
            // 固定资产统计
            FixedAssetDashboardVO fixedVO = calculateFixedAssetStatistics(clientId, startDate, endDate);
            vo.setFixedAsset(fixedVO);
        }
        return vo;
    }
    /**
     * 在建工程统计
     */
    private OngoingProjectDashboardVO calculateOngoingProjectStatistics(Long clientId, Date monthStart, Date monthEnd) {
        OngoingProjectDashboardVO vo = new OngoingProjectDashboardVO();
        // 查询在建工程项目(usage_type 表示项目用途类型,假设 0 表示在建工程)
        LambdaQueryWrapper<Project> projectWrapper = new LambdaQueryWrapper<>();
        projectWrapper.eq(Project::getDeleted, false)
                .eq(Project::getUsageType, 0); // 在建工程
        if (clientId != null) {
            projectWrapper.eq(Project::getClientId, clientId);
        }
        List<Project> projects = projectMapper.selectList(projectWrapper);
        // 1. 统计项目总数及分类
        vo.setProjectTotal(projects.size());
        long housingCount = projects.stream().filter(p -> p.getType() != null && p.getType() == 0).count();
        long municipalCount = projects.stream().filter(p -> p.getType() != null && p.getType() == 1).count();
        vo.setHousingProjectCount((int) housingCount);
        vo.setMunicipalProjectCount((int) municipalCount);
        // 计算检查覆盖率
        List<Integer> projectIds = projects.stream().map(Project::getId).collect(Collectors.toList());
        if (!projectIds.isEmpty()) {
            LambdaQueryWrapper<Check> checkWrapper = new LambdaQueryWrapper<>();
            checkWrapper.in(Check::getProjectId, projectIds);
            List<Check> checks = checkMapper.selectList(checkWrapper);
            List<Integer> checkedProjectIds = checks.stream()
                    .map(Check::getProjectId)
                    .distinct()
                    .collect(Collectors.toList());
            // 房建检查覆盖率
            List<Integer> housingProjectIds = projects.stream()
                    .filter(p -> p.getType() != null && p.getType() == 0)
                    .map(Project::getId)
                    .toList();
            long checkedHousingCount = checkedProjectIds.stream()
                    .filter(housingProjectIds::contains)
                    .count();
            vo.setHousingInspectionCoverage(
                    housingCount > 0 ? (int) Math.round(checkedHousingCount * 100.0 / housingCount) : 0
            );
            // 市政检查覆盖率
            List<Integer> municipalProjectIds = projects.stream()
                    .filter(p -> p.getType() != null && p.getType() == 1)
                    .map(Project::getId)
                    .toList();
            long checkedMunicipalCount = checkedProjectIds.stream()
                    .filter(municipalProjectIds::contains)
                    .count();
            vo.setMunicipalInspectionCoverage(
                    municipalCount > 0 ? (int) Math.round(checkedMunicipalCount * 100.0 / municipalCount) : 0
            );
        } else {
            vo.setHousingInspectionCoverage(0);
            vo.setMunicipalInspectionCoverage(0);
        }
        // 2. 统计本月检查情况
        calculateMonthlyInspectionForOngoing(vo, projects, projectIds, monthStart, monthEnd);
        // 3. 统计大型机械设备数
        vo.setMachineryStatistics(calculateMachineryStatistics(projectIds));
        // 4. 统计重大隐患数
        calculateHazardStatistics(vo, projectIds);
        // 5. 获取地图标点数据
        vo.setProjectMapPoints(getProjectMapPoints(projects, clientId, monthStart, monthEnd));
        return vo;
    }
    /**
     * 固定资产统计
     */
    private FixedAssetDashboardVO calculateFixedAssetStatistics(Long clientId, Date monthStart, Date monthEnd) {
        FixedAssetDashboardVO vo = new FixedAssetDashboardVO();
        // 查询固定资产项目(usage_type 表示项目用途类型,假设 1 表示固定资产)
        LambdaQueryWrapper<Project> projectWrapper = new LambdaQueryWrapper<>();
        projectWrapper.eq(Project::getDeleted, false)
                .eq(Project::getUsageType, 1); // 固定资产
        if (clientId != null) {
            projectWrapper.eq(Project::getClientId, clientId);
        }
        List<Project> projects = projectMapper.selectList(projectWrapper);
        // 1. 统计项目总数
        vo.setProjectTotal(projects.size());
        // 计算检查覆盖率
        List<Integer> projectIds = projects.stream().map(Project::getId).collect(Collectors.toList());
        if (!projectIds.isEmpty()) {
            LambdaQueryWrapper<Check> checkWrapper = new LambdaQueryWrapper<>();
            checkWrapper.in(Check::getProjectId, projectIds);
            List<Check> checks = checkMapper.selectList(checkWrapper);
            long checkedProjectCount = checks.stream()
                    .map(Check::getProjectId)
                    .distinct()
                    .count();
            vo.setInspectionCoverage(
                    projects.size() > 0 ? (int) Math.round(checkedProjectCount * 100.0 / projects.size()) : 0
            );
        } else {
            vo.setInspectionCoverage(0);
        }
        // 2. 统计本月检查情况
        calculateMonthlyInspectionForFixed(vo, projects, projectIds, monthStart, monthEnd);
        // 3. 统计重大隐患数
        calculateHazardStatisticsForFixed(vo, projectIds);
        // 4. 获取地图标点数据
        vo.setProjectMapPoints(getProjectMapPoints(projects, clientId, monthStart, monthEnd));
        return vo;
    }
    /**
     * 统计在建工程本月检查情况
     */
    private void calculateMonthlyInspectionForOngoing(OngoingProjectDashboardVO vo, List<Project> projects,
                                                      List<Integer> projectIds, Date monthStart, Date monthEnd) {
        if (projectIds.isEmpty()) {
            vo.setInspectedCount(0);
            vo.setInspectedHousingCount(0);
            vo.setInspectedMunicipalCount(0);
            vo.setUninspectedCount(projects.size());
            vo.setUninspectedHousingCount(vo.getHousingProjectCount());
            vo.setUninspectedMunicipalCount(vo.getMunicipalProjectCount());
            return;
        }
        // 查询本月有检查记录的项目
        LambdaQueryWrapper<Check> checkWrapper = new LambdaQueryWrapper<>();
        checkWrapper.in(Check::getProjectId, projectIds)
                .ge(Check::getTime, monthStart)
                .le(Check::getTime, monthEnd);
        List<Check> monthlyChecks = checkMapper.selectList(checkWrapper);
        List<Integer> inspectedProjectIds = monthlyChecks.stream()
                .map(Check::getProjectId)
                .distinct()
                .toList();
        // 已检查项目统计
        List<Project> inspectedProjects = projects.stream()
                .filter(p -> inspectedProjectIds.contains(p.getId()))
                .toList();
        vo.setInspectedCount(inspectedProjects.size());
        vo.setInspectedHousingCount((int) inspectedProjects.stream()
                .filter(p -> p.getType() != null && p.getType() == 0).count());
        vo.setInspectedMunicipalCount((int) inspectedProjects.stream()
                .filter(p -> p.getType() != null && p.getType() == 1).count());
        // 未检查项目统计
        List<Project> uninspectedProjects = projects.stream()
                .filter(p -> !inspectedProjectIds.contains(p.getId()))
                .toList();
        vo.setUninspectedCount(uninspectedProjects.size());
        vo.setUninspectedHousingCount((int) uninspectedProjects.stream()
                .filter(p -> p.getType() != null && p.getType() == 0).count());
        vo.setUninspectedMunicipalCount((int) uninspectedProjects.stream()
                .filter(p -> p.getType() != null && p.getType() == 1).count());
    }
    /**
     * 统计固定资产本月检查情况
     */
    private void calculateMonthlyInspectionForFixed(FixedAssetDashboardVO vo, List<Project> projects,
                                                    List<Integer> projectIds, Date monthStart, Date monthEnd) {
        if (projectIds.isEmpty()) {
            vo.setInspectedCount(0);
            vo.setUninspectedCount(projects.size());
            return;
        }
        // 查询本月有检查记录的项目
        LambdaQueryWrapper<Check> checkWrapper = new LambdaQueryWrapper<>();
        checkWrapper.in(Check::getProjectId, projectIds)
                .ge(Check::getTime, monthStart)
                .le(Check::getTime, monthEnd);
        List<Check> monthlyChecks = checkMapper.selectList(checkWrapper);
        long inspectedCount = monthlyChecks.stream()
                .map(Check::getProjectId)
                .distinct()
                .count();
        vo.setInspectedCount((int) inspectedCount);
        vo.setUninspectedCount(projects.size() - (int) inspectedCount);
    }
    /**
     * 统计大型机械设备数(含已检查/未检查)
     */
    private List<MachineryStatisticsVO> calculateMachineryStatistics(List<Integer> projectIds) {
        if (projectIds.isEmpty()) {
            return new ArrayList<>();
        }
        // 查询设备使用记录
        LambdaQueryWrapper<DeviceUsage> deviceWrapper = new LambdaQueryWrapper<>();
        deviceWrapper.eq(DeviceUsage::getDeleted, false)
                .in(DeviceUsage::getProjectId, projectIds);
        List<DeviceUsage> devices = deviceUsageMapper.selectList(deviceWrapper);
        // 按设备名称分组统计
        Map<String, List<DeviceUsage>> deviceGroupMap = devices.stream()
                .filter(d -> d.getName() != null && !d.getName().isEmpty())
                .collect(Collectors.groupingBy(DeviceUsage::getName));
        // 查询设备检查记录
        List<Integer> deviceIds = devices.stream().map(DeviceUsage::getId).collect(Collectors.toList());
        Map<Integer, Boolean> deviceInspectionMap = new HashMap<>();
        if (!deviceIds.isEmpty()) {
            LambdaQueryWrapper<Record> recordWrapper = new LambdaQueryWrapper<>();
            recordWrapper.eq(Record::getDeleted, false)
                    .in(Record::getDeviceId, deviceIds);
            List<Record> records = recordMapper.selectList(recordWrapper);
            Set<Integer> inspectedDeviceIds = records.stream()
                    .map(Record::getDeviceId)
                    .collect(Collectors.toSet());
            for (Integer deviceId : deviceIds) {
                deviceInspectionMap.put(deviceId, inspectedDeviceIds.contains(deviceId));
            }
        }
        // 构建统计结果
        List<MachineryStatisticsVO> result = new ArrayList<>();
        for (Map.Entry<String, List<DeviceUsage>> entry : deviceGroupMap.entrySet()) {
            MachineryStatisticsVO vo = new MachineryStatisticsVO();
            vo.setName(entry.getKey());
            vo.setTotal(entry.getValue().size());
            long inspectedCount = entry.getValue().stream()
                    .filter(d -> deviceInspectionMap.getOrDefault(d.getId(), false))
                    .count();
            vo.setInspectedCount((int) inspectedCount);
            vo.setUninspectedCount(entry.getValue().size() - (int) inspectedCount);
            result.add(vo);
        }
        return result;
    }
    /**
     * 统计在建工程重大隐患数
     */
    private void calculateHazardStatistics(OngoingProjectDashboardVO vo, List<Integer> projectIds) {
        if (projectIds.isEmpty()) {
            vo.setMajorHazardTotal(0);
            vo.setRectifiedHazardCount(0);
            vo.setUnrectifiedHazardCount(0);
            return;
        }
        // 查询检查任务
        LambdaQueryWrapper<Check> checkWrapper = new LambdaQueryWrapper<>();
        checkWrapper.in(Check::getProjectId, projectIds);
        List<Check> checks = checkMapper.selectList(checkWrapper);
        List<Integer> checkTaskIds = checks.stream().map(Check::getId).collect(Collectors.toList());
        if (checkTaskIds.isEmpty()) {
            vo.setMajorHazardTotal(0);
            vo.setRectifiedHazardCount(0);
            vo.setUnrectifiedHazardCount(0);
            return;
        }
        // 查询重大隐患记录(level 表示隐患等级,假设 3 表示重大隐患)
        LambdaQueryWrapper<Record> recordWrapper = new LambdaQueryWrapper<>();
        recordWrapper.eq(Record::getDeleted, false)
                .in(Record::getCheckTaskId, checkTaskIds)
                .eq(Record::getLevel, 3); // 重大隐患等级
        List<Record> majorHazards = recordMapper.selectList(recordWrapper);
        vo.setMajorHazardTotal(majorHazards.size());
        // 统计已整改和未整改数量
        long rectifiedCount = majorHazards.stream()
                .filter(r -> r.getReformed() != null && r.getReformed())
                .count();
        long unrectifiedCount = majorHazards.stream()
                .filter(r -> r.getReformed() == null || !r.getReformed())
                .count();
        vo.setRectifiedHazardCount((int) rectifiedCount);
        vo.setUnrectifiedHazardCount((int) unrectifiedCount);
    }
    /**
     * 统计固定资产重大隐患数
     */
    private void calculateHazardStatisticsForFixed(FixedAssetDashboardVO vo, List<Integer> projectIds) {
        if (projectIds.isEmpty()) {
            vo.setMajorHazardTotal(0);
            vo.setRectifiedHazardCount(0);
            vo.setUnrectifiedHazardCount(0);
            return;
        }
        // 查询检查任务
        LambdaQueryWrapper<Check> checkWrapper = new LambdaQueryWrapper<>();
        checkWrapper.in(Check::getProjectId, projectIds);
        List<Check> checks = checkMapper.selectList(checkWrapper);
        List<Integer> checkTaskIds = checks.stream().map(Check::getId).collect(Collectors.toList());
        if (checkTaskIds.isEmpty()) {
            vo.setMajorHazardTotal(0);
            vo.setRectifiedHazardCount(0);
            vo.setUnrectifiedHazardCount(0);
            return;
        }
        // 查询重大隐患记录(level 表示隐患等级,假设 3 表示重大隐患)
        LambdaQueryWrapper<Record> recordWrapper = new LambdaQueryWrapper<>();
        recordWrapper.eq(Record::getDeleted, false)
                .in(Record::getCheckTaskId, checkTaskIds)
                .eq(Record::getLevel, 3); // 重大隐患等级
        List<Record> majorHazards = recordMapper.selectList(recordWrapper);
        vo.setMajorHazardTotal(majorHazards.size());
        // 统计已整改和未整改数量
        long rectifiedCount = majorHazards.stream()
                .filter(r -> r.getReformed() != null && r.getReformed())
                .count();
        long unrectifiedCount = majorHazards.stream()
                .filter(r -> r.getReformed() == null || !r.getReformed())
                .count();
        vo.setRectifiedHazardCount((int) rectifiedCount);
        vo.setUnrectifiedHazardCount((int) unrectifiedCount);
    }
    /**
     * 获取项目地图标点数据
     */
    private List<ProjectMapPointVO> getProjectMapPoints(
            List<Project> projects, Long clientId, Date startDate, Date endDate) {
        List<ProjectMapPointVO> result = new ArrayList<>();
        // 计算项目类别
        Map<Integer, String> categoryMap;
        if (!projects.isEmpty()) {
            // 判断项目类型:在建工程 or 固定资产
            Integer usageType = projects.get(0).getUsageType();
            if (usageType != null && usageType == 0) {
                // 在建工程
                categoryMap = projectCategoryCalculator.calculateOngoingProjectCategories(
                        projects, clientId, startDate, endDate);
            } else if (usageType != null && usageType == 1) {
                // 固定资产
                categoryMap = projectCategoryCalculator.calculateFixedAssetCategories(
                        projects, clientId, startDate, endDate);
            } else {
                categoryMap = new HashMap<>();
            }
        } else {
            categoryMap = new HashMap<>();
        }
        // 构建地图标点数据
        for (Project p : projects) {
            if (!StringUtils.hasText(p.getAddressInfo())) {
                continue;
            }
            try {
                // 解析 address_info JSON 字符串
                JSONObject addressJson = JSON.parseObject(p.getAddressInfo());
                JSONArray gps = addressJson.getJSONArray("gps");
                if (gps != null && gps.size() >= 2) {
                    ProjectMapPointVO vo = new ProjectMapPointVO();
                    vo.setProjectId(p.getId());
                    vo.setProjectName(p.getName());
                    vo.setProjectType(p.getType());
                    // gps 数组格式:[经度, 纬度]
                    vo.setLongitude(gps.getString(0));
                    vo.setLatitude(gps.getString(1));
                    // 设置项目类别
                    vo.setCategory(categoryMap.get(p.getId()));
                    result.add(vo);
                }
            } catch (Exception e) {
                // 解析失败则跳过该项目
                log.warn("解析项目地址信息失败,项目ID: {}, address_info: {}", p.getId(), p.getAddressInfo(), e);
            }
        }
        return result;
    }
    /**
     * 获取开始日期
     */
    private Date getStartDate(Date startDate) {
        if (startDate != null) {
            return startDate;
        }
        // 默认为当前月份第一天
        YearMonth currentMonth = YearMonth.now();
        return Date.from(currentMonth.atDay(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    /**
     * 获取结束日期
     */
    private Date getEndDate(Date endDate) {
        if (endDate != null) {
            return endDate;
        }
        // 默认为当前月份最后一天
        YearMonth currentMonth = YearMonth.now();
        return Date.from(currentMonth.atEndOfMonth().atTime(23, 59, 59).atZone(ZoneId.systemDefault()).toInstant());
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PlanServiceImpl.java
@@ -1,6 +1,8 @@
package com.ruoyi.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.system.mapper.PlanMapper;
import com.ruoyi.system.model.Plan;
import com.ruoyi.system.service.PlanService;
import org.springframework.stereotype.Service;
ruoyi-system/src/main/java/com/ruoyi/system/vo/ClientDashboardQuery.java
New file
@@ -0,0 +1,33 @@
package com.ruoyi.system.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
 * 委托方大屏首页查询参数
 *
 * @author MiTao
 * @date 2026-01-20 15:30
 */
@Data
@ApiModel("委托方大屏首页查询参数")
public class ClientDashboardQuery {
    @ApiModelProperty("委托方ID(可选,不传则查询当前登录用户的委托方数据)")
    private Long clientId;
    @ApiModelProperty("开始日期(格式:yyyy-MM-dd,不传则默认当前月份第一天)")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date startDate;
    @ApiModelProperty("结束日期(格式:yyyy-MM-dd,不传则默认当前月份最后一天)")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date endDate;
    @ApiModelProperty("项目类型(ongoing-在建工程,fixed-固定资产,必填)")
    private String projectCategory;
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/ClientDashboardVO.java
New file
@@ -0,0 +1,26 @@
package com.ruoyi.system.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * 委托方大屏首页统计数据(包装类)
 *
 * @author MiTao
 * @date 2026-01-20 15:01
 */
@Data
@ApiModel("委托方大屏首页统计数据")
public class ClientDashboardVO {
    @ApiModelProperty("项目类型(ongoing-在建工程,fixed-固定资产)")
    private String projectCategory;
    @ApiModelProperty("在建工程统计数据(当 projectCategory=ongoing 时返回)")
    private OngoingProjectDashboardVO ongoingProject;
    @ApiModelProperty("固定资产统计数据(当 projectCategory=fixed 时返回)")
    private FixedAssetDashboardVO fixedAsset;
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/FixedAssetDashboardVO.java
New file
@@ -0,0 +1,46 @@
package com.ruoyi.system.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
 * 固定资产大屏统计数据
 *
 * @author MiTao
 * @date 2026-01-20 15:45
 */
@Data
@ApiModel("固定资产大屏统计数据")
public class FixedAssetDashboardVO {
    // ==================== 项目总数模块 ====================
    @ApiModelProperty("项目总数")
    private Integer projectTotal;
    @ApiModelProperty("检查覆盖率(百分比,如:77 表示 77%)")
    private Integer inspectionCoverage;
    // ==================== 本月检查情况 ====================
    @ApiModelProperty("已检查项目数")
    private Integer inspectedCount;
    @ApiModelProperty("未检查项目数")
    private Integer uninspectedCount;
    // ==================== 重大隐患数 ====================
    @ApiModelProperty("重大隐患总数")
    private Integer majorHazardTotal;
    @ApiModelProperty("已整改隐患数")
    private Integer rectifiedHazardCount;
    @ApiModelProperty("未整改隐患数")
    private Integer unrectifiedHazardCount;
    // ==================== 地图标点数据 ====================
    @ApiModelProperty("项目地图标点列表")
    private List<ProjectMapPointVO> projectMapPoints;
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/MachineryStatisticsVO.java
New file
@@ -0,0 +1,28 @@
package com.ruoyi.system.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * 机械设备统计数据
 *
 * @author MiTao
 * @date 2026-01-20 15:30
 */
@Data
@ApiModel("机械设备统计数据")
public class MachineryStatisticsVO {
    @ApiModelProperty("设备名称")
    private String name;
    @ApiModelProperty("设备总数")
    private Integer total;
    @ApiModelProperty("已检查数")
    private Integer inspectedCount;
    @ApiModelProperty("未检查数")
    private Integer uninspectedCount;
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/OngoingProjectDashboardVO.java
New file
@@ -0,0 +1,71 @@
package com.ruoyi.system.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
 * 在建工程大屏统计数据
 *
 * @author MiTao
 * @date 2026-01-20 15:40
 */
@Data
@ApiModel("在建工程大屏统计数据")
public class OngoingProjectDashboardVO {
    // ==================== 项目总数模块 ====================
    @ApiModelProperty("项目总数")
    private Integer projectTotal;
    @ApiModelProperty("房建项目数")
    private Integer housingProjectCount;
    @ApiModelProperty("市政项目数")
    private Integer municipalProjectCount;
    @ApiModelProperty("房建检查覆盖率(百分比,如:77 表示 77%)")
    private Integer housingInspectionCoverage;
    @ApiModelProperty("市政检查覆盖率(百分比,如:77 表示 77%)")
    private Integer municipalInspectionCoverage;
    // ==================== 本月检查情况 ====================
    @ApiModelProperty("已检查项目总数")
    private Integer inspectedCount;
    @ApiModelProperty("已检查房建项目数")
    private Integer inspectedHousingCount;
    @ApiModelProperty("已检查市政项目数")
    private Integer inspectedMunicipalCount;
    @ApiModelProperty("未检查项目总数")
    private Integer uninspectedCount;
    @ApiModelProperty("未检查房建项目数")
    private Integer uninspectedHousingCount;
    @ApiModelProperty("未检查市政项目数")
    private Integer uninspectedMunicipalCount;
    // ==================== 大型机械设备数 ====================
    @ApiModelProperty("大型机械设备统计列表")
    private List<MachineryStatisticsVO> machineryStatistics;
    // ==================== 重大隐患数 ====================
    @ApiModelProperty("重大隐患总数")
    private Integer majorHazardTotal;
    @ApiModelProperty("已整改隐患数")
    private Integer rectifiedHazardCount;
    @ApiModelProperty("未整改隐患数")
    private Integer unrectifiedHazardCount;
    // ==================== 地图标点数据 ====================
    @ApiModelProperty("项目地图标点列表")
    private List<ProjectMapPointVO> projectMapPoints;
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/ProjectMapPointVO.java
New file
@@ -0,0 +1,34 @@
package com.ruoyi.system.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * 项目地图标点数据
 *
 * @author MiTao
 * @date 2026-01-20 15:35
 */
@Data
@ApiModel("项目地图标点数据")
public class ProjectMapPointVO {
    @ApiModelProperty("项目ID")
    private Integer projectId;
    @ApiModelProperty("项目名称")
    private String projectName;
    @ApiModelProperty("项目类型(0-房建,1-市政)")
    private Integer projectType;
    @ApiModelProperty("项目类别(A类、B类、C类)")
    private String category;
    @ApiModelProperty("纬度")
    private String latitude;
    @ApiModelProperty("经度")
    private String longitude;
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/ProjectScoreData.java
New file
@@ -0,0 +1,28 @@
package com.ruoyi.system.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * 项目评分数据
 *
 * @author MiTao
 * @date 2026-01-21
 */
@Data
@ApiModel("项目评分数据")
public class ProjectScoreData {
    @ApiModelProperty("项目ID")
    private Integer projectId;
    @ApiModelProperty("安全检查平均分")
    private Float safetyScore;
    @ApiModelProperty("设备检查平均分")
    private Float deviceScore;
    @ApiModelProperty("是否有设备得分")
    private boolean hasDeviceScore;
}
sql/jiashida.sql
File was deleted