tuqin
2026-09-24 034f720f0bbc7f253c15613c28d994d7abf401eb
feat: 现场需求后端改动

- 计量器具:新增 /measuringInstrumentLedger/options 下拉接口(校准选择用)
- 客户拜访:修复实体 Jackson 反序列化(@Data+@Builder 补无参构造)
- 设备台账:修复区域设备数双倍(countMap 改为空 map 起始)
- 设备报修:saveDeviceRepair 增加空值防护(不再 NPE)
- 库存管理:修复合格库存数量列表 SQL 的 only_full_group_by 报错
- 部门管理:新增 PUT /system/dept/sort 批量排序接口
- 首页待办:/home/todos 追加“会议开始前1小时”提醒(含参会人员名单)
已修改19个文件
273 ■■■■■ 文件已修改
src/main/java/com/ruoyi/collaborativeApproval/service/MeetingService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/MeetingServiceImpl.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/customervisits/pojo/CustomerVisits.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceAreaServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/controller/MeasuringInstrumentLedgerController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/mapper/MeasuringInstrumentLedgerMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysDeptController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/mapper/SysDeptMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/ISysDeptService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysDeptServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/ProcurementBusinessSummaryDto.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysDeptMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/MeetingService.java
@@ -14,6 +14,7 @@
import com.ruoyi.collaborativeApproval.vo.SearchMeetingDraftVo;
import com.ruoyi.collaborativeApproval.vo.SearchMeetingRoomVo;
import com.ruoyi.collaborativeApproval.vo.SearchMeetingUseVo;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.framework.web.domain.R;
@@ -71,4 +72,10 @@
    List<MeetingSimpleDto> getMeetSummaryItems();
    /**
     * 即将开始的会议提醒:已发布、开始前1小时内、且当前登录用户为参会人。
     * 返回结构复用 ApproveProcess,直接并入首页待办事项。
     */
    List<ApproveProcess> getUpcomingMeetingReminders();
}
src/main/java/com/ruoyi/collaborativeApproval/service/impl/MeetingServiceImpl.java
@@ -7,6 +7,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.xiaoymin.knife4j.core.util.StrUtil;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.collaborativeApproval.dto.MeetSummaryDto;
import com.ruoyi.collaborativeApproval.dto.MeetUseDto;
import com.ruoyi.collaborativeApproval.dto.MeetingItem;
@@ -27,7 +28,9 @@
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.project.system.domain.SysPost;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysPostMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.project.system.service.ISysNoticeService;
import com.ruoyi.staff.mapper.StaffOnJobMapper;
@@ -54,6 +57,7 @@
    private final MeetingMinutesMapper meetingMinutesMapper;
    private final SysUserMapper userMapper;
    private final SysPostMapper sysPostMapper;
    private final ISysNoticeService sysNoticeService;
@@ -324,6 +328,114 @@
    public List<MeetingSimpleDto> getMeetSummaryItems() {
        return meetApplicationMapper.getMeetSummaryItems();
    }
    @Override
    public List<ApproveProcess> getUpcomingMeetingReminders() {
        List<ApproveProcess> reminders = new ArrayList<>();
        LocalDateTime now = LocalDateTime.now();
        List<MeetApplication> meetings = meetApplicationMapper.selectList(
                Wrappers.<MeetApplication>lambdaQuery()
                        .eq(MeetApplication::getPublishStatus, 1)
                        .gt(MeetApplication::getStartTime, now)
                        .le(MeetApplication::getStartTime, now.plusHours(1))
                        .orderByAsc(MeetApplication::getStartTime));
        if (meetings == null || meetings.isEmpty()) {
            return reminders;
        }
        Long currentUserId = SecurityUtils.getLoginUser().getUserId();
        Map<Integer, SysPost> postCache = new HashMap<>();
        for (MeetApplication meeting : meetings) {
            if (meeting.getStartTime() == null) {
                continue;
            }
            List<Long> staffIds = parseParticipantStaffIds(meeting.getParticipants());
            if (staffIds.isEmpty()) {
                continue;
            }
            List<StaffOnJob> staffs = staffOnJobMapper.selectBatchIds(staffIds);
            Map<Long, StaffOnJob> staffMap = (staffs == null) ? new HashMap<>()
                    : staffs.stream().collect(Collectors.toMap(StaffOnJob::getId, s -> s, (a, b) -> a));
            boolean isParticipant = false;
            List<String> participantNames = new ArrayList<>();
            for (Long staffId : staffIds) {
                StaffOnJob staff = staffMap.get(staffId);
                if (staff == null) {
                    continue;
                }
                String display = staff.getStaffName() == null ? "" : staff.getStaffName();
                if (staff.getSysPostId() != null) {
                    SysPost post = postCache.get(staff.getSysPostId());
                    if (post == null) {
                        post = sysPostMapper.selectPostById(staff.getSysPostId().longValue());
                        if (post != null) {
                            postCache.put(staff.getSysPostId(), post);
                        }
                    }
                    if (post != null && post.getPostName() != null && !post.getPostName().isEmpty()) {
                        display = display + " (" + post.getPostName() + ")";
                    }
                }
                if (!display.isEmpty()) {
                    participantNames.add(display);
                }
                if (!isParticipant && staff.getStaffNo() != null) {
                    SysUser user = userMapper.selectOne(
                            Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUserName, staff.getStaffNo()));
                    if (user != null && currentUserId.equals(user.getUserId())) {
                        isParticipant = true;
                    }
                }
            }
            if (!isParticipant) {
                continue;
            }
            String roomName = "会议";
            if (meeting.getRoomId() != null) {
                MeetingRoom room = meetingRoomMapper.selectById(meeting.getRoomId());
                if (room != null && room.getName() != null && !room.getName().isEmpty()) {
                    roomName = room.getName();
                }
            }
            ApproveProcess ap = new ApproveProcess();
            ap.setId(100000000L + meeting.getId());
            ap.setApproveId("会议提醒");
            ap.setApproveDeptName(roomName);
            String reason = "会议《" + meeting.getTitle() + "》将于 "
                    + meeting.getStartTime().format(DateTimeFormatter.ofPattern("HH:mm")) + " 开始";
            if (!participantNames.isEmpty()) {
                reason += ";参会人员:" + String.join("、", participantNames);
            }
            ap.setApproveReason(reason);
            ap.setApproveUserName(meeting.getApplicant());
            ap.setApproveTime(java.sql.Timestamp.valueOf(meeting.getStartTime()));
            ap.setApproveTypeName("会议提醒");
            reminders.add(ap);
        }
        return reminders;
    }
    private List<Long> parseParticipantStaffIds(String participants) {
        List<Long> ids = new ArrayList<>();
        if (participants == null || participants.trim().isEmpty()) {
            return ids;
        }
        String cleanStr = participants.replaceAll("^\\[|\\]$", "").trim();
        if (cleanStr.isEmpty()) {
            return ids;
        }
        for (String s : cleanStr.split(",")) {
            String val = s.trim();
            if (val.isEmpty()) {
                continue;
            }
            try {
                ids.add(Long.valueOf(val));
            } catch (NumberFormatException ignored) {
            }
        }
        return ids;
    }
}
src/main/java/com/ruoyi/customervisits/pojo/CustomerVisits.java
@@ -5,8 +5,10 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
@@ -20,6 +22,8 @@
@TableName("customer_visits")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema
public class CustomerVisits {
src/main/java/com/ruoyi/device/service/impl/DeviceAreaServiceImpl.java
@@ -17,6 +17,7 @@
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -51,8 +52,9 @@
                .collect(Collectors.toMap(DeviceArea::getId, a -> a));
        // 递归汇总每个节点的设备数量(含所有子级)
        Map<Long, Long> countMap = directCountMap.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        // 必须从空 map 开始:每个区域的自身直挂数只在 accumulate 中累加一次,
        // 否则会与“初始即含自身直挂数”叠加,导致每个区域自身设备数被计两次(双倍)
        Map<Long, Long> countMap = new HashMap<>();
        for (DeviceArea area : all) {
            accumulate(area.getId(), directCountMap.getOrDefault(area.getId(), 0L), countMap, areaMap);
        }
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java
@@ -64,7 +64,13 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult saveDeviceRepair(DeviceRepairDto deviceRepairDto) {
        if (deviceRepairDto.getDeviceLedgerId() == null) {
            return AjaxResult.error("请选择设备");
        }
        DeviceLedger byId = deviceLedgerService.getById(deviceRepairDto.getDeviceLedgerId());
        if (byId == null) {
            return AjaxResult.error("所选设备不存在");
        }
        deviceRepairDto.setDeviceName(byId.getDeviceName());
        deviceRepairDto.setDeviceModel(byId.getDeviceModel());
        if (deviceRepairDto.getStatus() == null) {
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
@@ -31,6 +31,7 @@
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.collaborativeApproval.mapper.NoticeMapper;
import com.ruoyi.collaborativeApproval.pojo.Notice;
import com.ruoyi.collaborativeApproval.service.MeetingService;
import com.ruoyi.common.enums.ApproveTypeEnum;
import com.ruoyi.common.enums.TypeEnums;
import com.ruoyi.common.utils.SecurityUtils;
@@ -116,6 +117,8 @@
    private final ProductMapper productMapper;
    private final StaffOnJobMapper staffOnJobMapper;
    private final MeetingService meetingService;
    private final CustomerMapper customerMapper;
@@ -446,6 +449,16 @@
            }
        }
        // 会议提醒:已发布、开始前1小时内、当前用户为参会人(失败不影响待办其它数据)
        try {
            List<ApproveProcess> meetingReminders = meetingService.getUpcomingMeetingReminders();
            if (!CollectionUtils.isEmpty(meetingReminders)) {
                approveProcesses.addAll(meetingReminders);
            }
        } catch (Exception e) {
            log.warn("加载会议提醒失败: {}", e.getMessage());
        }
        return approveProcesses;
    }
src/main/java/com/ruoyi/measuringinstrumentledger/controller/MeasuringInstrumentLedgerController.java
@@ -109,6 +109,12 @@
        return update ? AjaxResult.success("检定成功") : AjaxResult.error("检定失败");
    }
    @GetMapping("/options")
    @Operation(summary = "计量器具台账-下拉列表")
    public AjaxResult options() {
        return AjaxResult.success(measuringInstrumentLedgerService.listOptions());
    }
    @PostMapping("/downloadTemplate")
    @Operation(summary = "下载计量器具台账导入模板")
    public void downloadTemplate(HttpServletResponse response) {
src/main/java/com/ruoyi/measuringinstrumentledger/mapper/MeasuringInstrumentLedgerMapper.java
@@ -27,4 +27,12 @@
    List<MeasuringInstrumentLedger> listPage(@Param("req") MeasuringInstrumentLedger measuringInstrumentLedger);
    List<String> selectExistingCodes(@Param("tenantId") Long tenantId, @Param("codes") Collection<String> codes);
    /**
     * 下拉:当前租户全部计量器具(用于校准/检定选择,豁免数据范围过滤)
     *
     * @param tenantId 租户ID
     * @return 计量器具列表
     */
    List<MeasuringInstrumentLedger> listOptions(@Param("tenantId") Long tenantId);
}
src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerService.java
@@ -10,6 +10,7 @@
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
 * @author :yys
@@ -47,4 +48,11 @@
     * @return
     */
    boolean add(MeasuringInstrumentLedger measuringInstrumentLedger) throws IOException;
    /**
     * 下拉:当前租户全部计量器具(用于校准/检定选择)
     *
     * @return 计量器具列表
     */
    List<MeasuringInstrumentLedger> listOptions();
}
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java
@@ -76,7 +76,8 @@
        if(measuringInstrumentLedger == null) {
            return false;
        }
        SysUser sysUser = sysUserMapper.selectUserById(measuringInstrumentLedger.getUserId());
        Long nameUserId = measuringInstrumentLedger.getUserId() != null ? measuringInstrumentLedger.getUserId() : req.getUserId();
        SysUser sysUser = nameUserId == null ? null : sysUserMapper.selectUserById(nameUserId);
        measuringInstrumentLedger.setValid(req.getValid());
        measuringInstrumentLedger.setMostDate(req.getRecordDate());
        measuringInstrumentLedger.setNextDate(new Date(req.getRecordDate().getTime() + 1000L * 60 * 60 * 24 * req.getValid()));
@@ -87,7 +88,7 @@
            measuringInstrumentLedgerRecord.setEntryDate(req.getEntryDate());
            measuringInstrumentLedgerRecord.setValid(req.getValid());
            measuringInstrumentLedgerRecord.setUserId(req.getUserId());
            measuringInstrumentLedgerRecord.setUserName(sysUser.getUserName());
            measuringInstrumentLedgerRecord.setUserName(sysUser == null ? null : sysUser.getUserName());
            measuringInstrumentLedgerRecordMapper.insert(measuringInstrumentLedgerRecord);
            // 台账绑定一次
//            if(!CollectionUtils.isEmpty(req.getTempFileIds())){
@@ -200,4 +201,10 @@
        return true;
    }
    @Override
    public List<MeasuringInstrumentLedger> listOptions() {
        Long tenantId = SecurityUtils.getLoginUser().getTenantId();
        return measuringInstrumentLedgerMapper.listOptions(tenantId);
    }
}
src/main/java/com/ruoyi/project/system/controller/SysDeptController.java
@@ -131,4 +131,15 @@
        deptService.checkDeptDataScope(deptId);
        return toAjax(deptService.deleteDeptById(deptId));
    }
    /**
     * 修改部门排序(批量)
     */
    @PreAuthorize("@ss.hasPermi('system:dept:edit')")
    @Log(title = "部门管理", businessType = BusinessType.UPDATE)
    @PutMapping("/sort")
    public AjaxResult updateSort(@RequestBody List<SysDept> depts)
    {
        return toAjax(deptService.updateDeptSort(depts));
    }
}
src/main/java/com/ruoyi/project/system/mapper/SysDeptMapper.java
@@ -110,7 +110,15 @@
     * @param depts 子元素
     * @return 结果
     */
    public int updateDeptChildren(@Param("depts") List<SysDept> depts);
    public int updateDeptChildren(@Param("depts") List<SysDept> depts);
    /**
     * 批量修改部门排序
     *
     * @param depts 部门列表(deptId + orderNum)
     * @return 结果
     */
    public int updateDeptSort(@Param("depts") List<SysDept> depts);
    /**
     * 删除部门管理信息
src/main/java/com/ruoyi/project/system/service/ISysDeptService.java
@@ -128,4 +128,12 @@
     * @return
     */
    Long maxLevelDeptId(Long deptId);
    /**
     * 批量修改部门排序
     *
     * @param depts 部门列表(deptId + orderNum)
     * @return 结果
     */
    public int updateDeptSort(List<SysDept> depts);
}
src/main/java/com/ruoyi/project/system/service/impl/SysDeptServiceImpl.java
@@ -249,6 +249,22 @@
    }
    /**
     * 批量修改部门排序
     *
     * @param depts 部门列表(deptId + orderNum)
     * @return 结果
     */
    @Override
    public int updateDeptSort(List<SysDept> depts)
    {
        if (depts == null || depts.isEmpty())
        {
            return 0;
        }
        return deptMapper.updateDeptSort(depts);
    }
    /**
     * 修改该部门的父级部门状态
     * 
     * @param dept 当前部门
src/main/java/com/ruoyi/purchase/dto/ProcurementBusinessSummaryDto.java
@@ -93,4 +93,14 @@
     */
    @Excel(name = "退货金额")
    private BigDecimal returnAmount;
    /**
     * 排序字段(白名单:entryDate)
     */
    private String orderByColumn;
    /**
     * 排序方向:asc / desc
     */
    private String isAsc;
}
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml
@@ -74,4 +74,12 @@
            #{code}
        </foreach>
    </select>
    <!-- 下拉选择:当前租户全部计量器具。SQL 带 /*data_scope*/ 标记,仅此查询豁免数据范围过滤 -->
    <select id="listOptions" resultType="com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedger">
        SELECT /*data_scope*/ id, code, name, model, unit, most_date, valid, record_date
        FROM measuring_instrument_ledger
        WHERE tenant_id = #{tenantId}
        ORDER BY create_time DESC
    </select>
</mapper>
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml
@@ -241,7 +241,19 @@
            AND slp.product_category = #{req.productCategory}
        </if>
        GROUP BY slp.product_category, slp.specification_model, sl.supplier_name
        ORDER BY slp.product_category
        <!-- 排序:支持按录入日期(聚合列)排序,白名单防止注入;默认按产品大类 -->
        <choose>
            <when test="req.orderByColumn == 'entryDate'">
                ORDER BY MAX(sl.entry_date)
                <choose>
                    <when test="req.isAsc == 'asc'">ASC</when>
                    <otherwise>DESC</otherwise>
                </choose>
            </when>
            <otherwise>
                ORDER BY slp.product_category ASC
            </otherwise>
        </choose>
    </select>
    <select id="selectProductBomStructure" resultType="com.ruoyi.sales.dto.LossProductModelDto">
        select
src/main/resources/mapper/stock/StockInventoryMapper.xml
@@ -222,7 +222,7 @@
        unit,
        product_name,
        product_id
        order by combined.create_time desc
        order by create_time desc
    </select>
    <select id="listStockInventoryExportData" resultType="com.ruoyi.stock.execl.StockInventoryExportData">
src/main/resources/mapper/system/SysDeptMapper.xml
@@ -155,6 +155,19 @@
        </foreach>
    </update>
    <update id="updateDeptSort" parameterType="java.util.List">
        update sys_dept set order_num =
        <foreach collection="depts" item="item" index="index"
            separator=" " open="case dept_id" close="end">
            when #{item.deptId} then #{item.orderNum}
        </foreach>
        where dept_id in
        <foreach collection="depts" item="item" index="index"
            separator="," open="(" close=")">
            #{item.deptId}
        </foreach>
    </update>
    <update id="updateDeptStatusNormal" parameterType="Long">
         update sys_dept set status = '0' where dept_id in
         <foreach collection="array" item="deptId" open="(" separator="," close=")">