Files
MES/yawei-mes/.tasks/2025-10-17_兴万达改进(库存预警).md
2026-04-02 10:39:03 +08:00

54 KiB
Raw Permalink Blame History

兴万达MES系统 - 库存预警功能实现文档

📋 目录


📝 需求说明

1. 功能概述

在仓库管理模块下新增"库存预警"功能,与"即时库存"同级菜单,用于监控物料库存是否超出设定的上下限范围,并在登录首页时弹窗提醒。

重要说明

  • 物料选择直接从物料表md_material读取不再从即时库存读取
  • 菜单状态控制预警:读取 sys_menu 表中"仓库管理"菜单menu_id=2082的状态
    • 如果 visible='1'(隐藏)或 status='1'(停用),则不在首页显示预警
    • 只有当 visible='0'(显示)且 status='0'(正常)时,才会在首页显示预警

2. 核心功能

  1. 预警配置管理

    • 选择物料(从物料表中选择,只显示状态为"启用"的物料)
    • 可选择特定仓库或全部仓库
    • 设置库存上限数量
    • 设置库存下限数量
    • 支持增删改查和导出功能
  2. 预警触发逻辑

    • 当前库存数量 > 上限数量:触发"高于上限"预警
    • 当前库存数量 < 下限数量:触发"低于下限"预警
    • 在上下限之间:不触发预警
    • 菜单状态控制:检查"仓库管理"菜单ID=2082visiblestatus 字段
      • visible='0'status='0':显示预警
      • visible='1'status='1':不显示预警
  3. 预警展示

    • 登录首页右上角弹出Notification通知
    • 显示5秒后自动消失可手动关闭
    • 预警内容格式:
      • 高于上限:【库存预警】物料【xxx】在仓库【xxx】中的库存数量高于上限上限X件请暂停采购或调整库存。
      • 低于下限:【库存预警】物料【xxx】在仓库【xxx】中的库存数量低于下限下限X件请尽快采购补货。
      • 底部小字:*若数量上下限不正确,请在库存预警中更改上下限
    • 多条预警依次弹出间隔300ms

3. 权限配置

  • warehouse:inventoryAlert:query - 查询
  • warehouse:inventoryAlert:add - 新增
  • warehouse:inventoryAlert:edit - 修改
  • warehouse:inventoryAlert:remove - 删除
  • warehouse:inventoryAlert:export - 导出

🗄️ 数据库设计

表结构:wm_inventory_alert

字段名 类型 长度 允许NULL 默认值 说明
id BIGINT - NO AUTO_INCREMENT 主键ID
material_id BIGINT - NO - 物料ID
material_number VARCHAR 64 YES NULL 物料编号(冗余)
material_name VARCHAR 255 YES NULL 物料名称(冗余)
specification VARCHAR 255 YES NULL 规格型号(冗余)
warehouse_id BIGINT - YES NULL 仓库IDNULL=所有仓库)
warehouse_name VARCHAR 64 YES NULL 仓库名称(冗余)
max_quantity DECIMAL 15,2 YES NULL 库存上限数量
min_quantity DECIMAL 15,2 YES NULL 库存下限数量
status CHAR 1 NO '0' 状态0启用 1停用
remark VARCHAR 500 YES NULL 备注
create_by VARCHAR 64 YES '' 创建者
create_time DATETIME - YES NULL 创建时间
update_by VARCHAR 64 YES '' 更新者
update_time DATETIME - YES NULL 更新时间

索引设计

  • 主键索引PRIMARY KEY (id)
  • 物料索引INDEX idx_material_id (material_id)
  • 仓库索引INDEX idx_warehouse_id (warehouse_id)
  • 状态索引INDEX idx_status (status)
  • 唯一索引UNIQUE INDEX uk_material_warehouse (material_id, warehouse_id) - 防止同一物料在同一仓库重复配置

菜单配置

menu_id menu_name parent_id order_num path component perms menu_type icon
2400 库存预警 2082 1 inventoryAlert mes/warehouse/inventoryAlert/index warehouse:inventoryAlert:list C warning
2401 库存预警查询 2400 1 # warehouse:inventoryAlert:query F #
2402 库存预警新增 2400 2 # warehouse:inventoryAlert:add F #
2403 库存预警修改 2400 3 # warehouse:inventoryAlert:edit F #
2404 库存预警删除 2400 4 # warehouse:inventoryAlert:remove F #
2405 库存预警导出 2400 5 # warehouse:inventoryAlert:export F #

💡 核心SQL查询逻辑说明

预警触发查询:selectAlertWithCurrentInventory

此查询用于获取所有需要触发预警的配置,包含以下关键逻辑:

1. 实时库存计算

使用子查询计算每个物料的实时库存数量:

  • 入库汇总:采购入库、销售退货、生产入库、盘点初始化、盘盈
  • 出库汇总:生产领料、销售出库、盘亏、其他出库
  • 实时库存 = 入库总量 - 出库总量

2. 过滤条件INNER JOIN

  • sys_menu (menu_id=2082):检查"仓库管理"菜单状态
    • visible='0':菜单可见
    • status='0':菜单启用
    • 作用:当仓库管理模块被禁用或隐藏时,不显示任何预警

3. WHERE条件

  • a.status = '0':只查询启用的预警配置
  • menu.visible = '0':仓库管理菜单可见
  • menu.status = '0':仓库管理菜单启用

4. HAVING条件

  • current_quantity > 0只查询库存数量大于0的记录
  • 作用避免对库存为0或负数的物料发出预警
  • 注意此条件已能过滤掉从未入过库的物料它们的库存为0无需再关联其他库存表

5. 预警判断Service层

查询结果返回后在Service层进行预警判断

  • current_quantity > max_quantity:触发"高于上限"预警
  • current_quantity < min_quantity:触发"低于下限"预警

SQL执行流程图

┌─────────────────────────────────────────────────────────┐
│ 1. 从 wm_inventory_alert 获取所有预警配置                │
│    └─ WHERE status='0' (只查询启用的配置)               │
└─────────────────────┬───────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────┐
│ 2. INNER JOIN sys_menu (menu_id=2082)                   │
│    └─ 检查仓库管理菜单是否可见且启用                    │
│    └─ 如果菜单被禁用,则返回空结果                      │
└─────────────────────┬───────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────┐
│ 3. 计算实时库存数量 (子查询)                            │
│    ├─ 汇总所有入库记录                                  │
│    ├─ 汇总所有出库记录                                  │
│    └─ current_quantity = 入库 - 出库                    │
└─────────────────────┬───────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────┐
│ 4. HAVING current_quantity > 0                          │
│    └─ 过滤掉库存为0或负数的记录                         │
│    └─ 自动过滤掉从未入过库的物料                        │
└─────────────────────┬───────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────┐
│ 5. 返回结果到Service层                                   │
│    ├─ 判断是否超过上限 (> max_quantity)                 │
│    ├─ 判断是否低于下限 (< min_quantity)                 │
│    └─ 生成预警消息                                      │
└─────────────────────────────────────────────────────────┘

🔧 后端实现

1. 目录结构

yjh-mes/src/main/java/cn/sourceplan/warehouse/
├── domain/
│   └── InventoryAlert.java                    # 实体类
├── mapper/
│   └── InventoryAlertMapper.java              # Mapper接口
├── service/
│   ├── IInventoryAlertService.java            # Service接口
│   └── impl/
│       └── InventoryAlertServiceImpl.java     # Service实现
└── controller/
    └── InventoryAlertController.java          # 控制器

yjh-mes/src/main/resources/mapper/warehouse/
└── InventoryAlertMapper.xml                   # MyBatis映射文件

2. 实体类:InventoryAlert.java

package cn.sourceplan.warehouse.domain;

import java.math.BigDecimal;
import lombok.Data;
import cn.sourceplan.common.annotation.Excel;
import cn.sourceplan.common.core.domain.BaseEntity;

/**
 * 库存预警对象 wm_inventory_alert
 * 
 * @author admin
 * @date 2025-10-17
 */
@Data
public class InventoryAlert extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /** 主键ID */
    private Long id;

    /** 物料ID */
    @Excel(name = "物料ID")
    private Long materialId;

    /** 物料编号 */
    @Excel(name = "物料编号")
    private String materialNumber;

    /** 物料名称 */
    @Excel(name = "物料名称")
    private String materialName;

    /** 规格型号 */
    @Excel(name = "规格型号")
    private String specification;

    /** 仓库ID */
    @Excel(name = "仓库ID")
    private Long warehouseId;

    /** 仓库名称 */
    @Excel(name = "仓库名称")
    private String warehouseName;

    /** 库存上限数量 */
    @Excel(name = "库存上限")
    private BigDecimal maxQuantity;

    /** 库存下限数量 */
    @Excel(name = "库存下限")
    private BigDecimal minQuantity;

    /** 状态0启用 1停用 */
    @Excel(name = "状态", readConverterExp = "0=启用,1=停用")
    private String status;
}

3. Mapper接口InventoryAlertMapper.java

package cn.sourceplan.warehouse.mapper;

import java.util.List;
import cn.sourceplan.warehouse.domain.InventoryAlert;
import org.springframework.stereotype.Repository;

/**
 * 库存预警Mapper接口
 * 
 * @author admin
 * @date 2025-10-17
 */
@Repository
public interface InventoryAlertMapper {
    /**
     * 查询库存预警
     */
    InventoryAlert selectInventoryAlertById(Long id);

    /**
     * 查询库存预警列表
     */
    List<InventoryAlert> selectInventoryAlertList(InventoryAlert inventoryAlert);

    /**
     * 新增库存预警
     */
    int insertInventoryAlert(InventoryAlert inventoryAlert);

    /**
     * 修改库存预警
     */
    int updateInventoryAlert(InventoryAlert inventoryAlert);

    /**
     * 删除库存预警
     */
    int deleteInventoryAlertById(Long id);

    /**
     * 批量删除库存预警
     */
    int deleteInventoryAlertByIds(Long[] ids);

    /**
     * 查询所有启用的预警配置及其当前库存
     */
    List<InventoryAlert> selectAlertWithCurrentInventory();
}

4. Service接口IInventoryAlertService.java

package cn.sourceplan.warehouse.service;

import java.util.List;
import cn.sourceplan.warehouse.domain.InventoryAlert;
import com.alibaba.fastjson2.JSONObject;

/**
 * 库存预警Service接口
 * 
 * @author admin
 * @date 2025-10-17
 */
public interface IInventoryAlertService {
    InventoryAlert selectInventoryAlertById(Long id);
    List<InventoryAlert> selectInventoryAlertList(InventoryAlert inventoryAlert);
    int insertInventoryAlert(InventoryAlert inventoryAlert);
    int updateInventoryAlert(InventoryAlert inventoryAlert);
    int deleteInventoryAlertByIds(Long[] ids);
    
    /**
     * 获取当前触发的所有预警信息
     * 返回格式:[{type: 'max'/'min', message: '预警消息'}]
     */
    List<JSONObject> getTriggeredAlerts();
}

5. Service实现InventoryAlertServiceImpl.java

package cn.sourceplan.warehouse.service.impl;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import cn.sourceplan.warehouse.domain.InventoryAlert;
import cn.sourceplan.warehouse.mapper.InventoryAlertMapper;
import cn.sourceplan.warehouse.service.IInventoryAlertService;
import com.alibaba.fastjson2.JSONObject;

/**
 * 库存预警Service业务层处理
 * 
 * @author admin
 * @date 2025-10-17
 */
@Service
public class InventoryAlertServiceImpl implements IInventoryAlertService {
    
    @Autowired
    private InventoryAlertMapper inventoryAlertMapper;

    @Override
    public InventoryAlert selectInventoryAlertById(Long id) {
        return inventoryAlertMapper.selectInventoryAlertById(id);
    }

    @Override
    public List<InventoryAlert> selectInventoryAlertList(InventoryAlert inventoryAlert) {
        return inventoryAlertMapper.selectInventoryAlertList(inventoryAlert);
    }

    @Override
    public int insertInventoryAlert(InventoryAlert inventoryAlert) {
        return inventoryAlertMapper.insertInventoryAlert(inventoryAlert);
    }

    @Override
    public int updateInventoryAlert(InventoryAlert inventoryAlert) {
        return inventoryAlertMapper.updateInventoryAlert(inventoryAlert);
    }

    @Override
    public int deleteInventoryAlertByIds(Long[] ids) {
        return inventoryAlertMapper.deleteInventoryAlertByIds(ids);
    }

    @Override
    public List<JSONObject> getTriggeredAlerts() {
        List<JSONObject> alerts = new ArrayList<>();
        
        // 查询所有启用的预警配置及其当前库存
        List<InventoryAlert> alertConfigs = inventoryAlertMapper.selectAlertWithCurrentInventory();
        
        for (InventoryAlert config : alertConfigs) {
            BigDecimal currentQty = config.getCurrentQuantity(); // 需要在实体类中添加临时字段
            
            if (currentQty == null) {
                continue;
            }
            
            JSONObject alert = new JSONObject();
            
            // 检查是否超过上限
            if (config.getMaxQuantity() != null && 
                currentQty.compareTo(config.getMaxQuantity()) > 0) {
                alert.put("type", "max");
                alert.put("message", String.format(
                    "【库存预警】物料【%s】在仓库【%s】中的库存数量高于上限上限%s件请暂停采购或调整库存。",
                    config.getMaterialName(),
                    config.getWarehouseName() != null ? config.getWarehouseName() : "所有仓库",
                    config.getMaxQuantity()
                ));
                alerts.add(alert);
            }
            
            // 检查是否低于下限
            if (config.getMinQuantity() != null && 
                currentQty.compareTo(config.getMinQuantity()) < 0) {
                alert = new JSONObject();
                alert.put("type", "min");
                alert.put("message", String.format(
                    "【库存预警】物料【%s】在仓库【%s】中的库存数量低于下限下限%s件请尽快采购补货。",
                    config.getMaterialName(),
                    config.getWarehouseName() != null ? config.getWarehouseName() : "所有仓库",
                    config.getMinQuantity()
                ));
                alerts.add(alert);
            }
        }
        
        return alerts;
    }
}

6. ControllerInventoryAlertController.java

package cn.sourceplan.warehouse.controller;

import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import cn.sourceplan.common.annotation.Log;
import cn.sourceplan.common.core.controller.BaseController;
import cn.sourceplan.common.core.domain.AjaxResult;
import cn.sourceplan.common.core.page.TableDataInfo;
import cn.sourceplan.common.enums.BusinessType;
import cn.sourceplan.common.utils.poi.ExcelUtil;
import cn.sourceplan.warehouse.domain.InventoryAlert;
import cn.sourceplan.warehouse.service.IInventoryAlertService;
import com.alibaba.fastjson2.JSONObject;

/**
 * 库存预警Controller
 * 
 * @author admin
 * @date 2025-10-17
 */
@RestController
@RequestMapping("/warehouse/inventoryAlert")
public class InventoryAlertController extends BaseController {
    
    @Autowired
    private IInventoryAlertService inventoryAlertService;

    /**
     * 查询库存预警列表
     */
    @GetMapping("/list")
    public TableDataInfo list(InventoryAlert inventoryAlert) {
        startPage();
        List<InventoryAlert> list = inventoryAlertService.selectInventoryAlertList(inventoryAlert);
        return getDataTable(list);
    }

    /**
     * 导出库存预警列表
     */
    @Log(title = "库存预警", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, InventoryAlert inventoryAlert) {
        List<InventoryAlert> list = inventoryAlertService.selectInventoryAlertList(inventoryAlert);
        ExcelUtil<InventoryAlert> util = new ExcelUtil<>(InventoryAlert.class);
        util.exportExcel(response, list, "库存预警数据");
    }

    /**
     * 获取库存预警详细信息
     */
    @GetMapping(value = "/{id}")
    public AjaxResult getInfo(@PathVariable("id") Long id) {
        return success(inventoryAlertService.selectInventoryAlertById(id));
    }

    /**
     * 新增库存预警
     */
    @Log(title = "库存预警", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody InventoryAlert inventoryAlert) {
        return toAjax(inventoryAlertService.insertInventoryAlert(inventoryAlert));
    }

    /**
     * 修改库存预警
     */
    @Log(title = "库存预警", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody InventoryAlert inventoryAlert) {
        return toAjax(inventoryAlertService.updateInventoryAlert(inventoryAlert));
    }

    /**
     * 删除库存预警
     */
    @Log(title = "库存预警", businessType = BusinessType.DELETE)
    @DeleteMapping("/{ids}")
    public AjaxResult remove(@PathVariable Long[] ids) {
        return toAjax(inventoryAlertService.deleteInventoryAlertByIds(ids));
    }

    /**
     * 获取当前触发的预警信息(供首页调用)
     */
    @GetMapping("/triggered")
    public AjaxResult getTriggeredAlerts() {
        List<JSONObject> alerts = inventoryAlertService.getTriggeredAlerts();
        return success(alerts);
    }
}

7. MyBatis映射InventoryAlertMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.sourceplan.warehouse.mapper.InventoryAlertMapper">
    
    <resultMap type="cn.sourceplan.warehouse.domain.InventoryAlert" id="InventoryAlertResult">
        <result property="id"              column="id"              />
        <result property="materialId"      column="material_id"     />
        <result property="materialNumber"  column="material_number" />
        <result property="materialName"    column="material_name"   />
        <result property="specification"   column="specification"   />
        <result property="warehouseId"     column="warehouse_id"    />
        <result property="warehouseName"   column="warehouse_name"  />
        <result property="maxQuantity"     column="max_quantity"    />
        <result property="minQuantity"     column="min_quantity"    />
        <result property="status"          column="status"          />
        <result property="remark"          column="remark"          />
        <result property="createBy"        column="create_by"       />
        <result property="createTime"      column="create_time"     />
        <result property="updateBy"        column="update_by"       />
        <result property="updateTime"      column="update_time"     />
    </resultMap>

    <sql id="selectInventoryAlertVo">
        SELECT id, material_id, material_number, material_name, specification,
               warehouse_id, warehouse_name, max_quantity, min_quantity, status,
               remark, create_by, create_time, update_by, update_time
        FROM wm_inventory_alert
    </sql>

    <select id="selectInventoryAlertList" parameterType="InventoryAlert" resultMap="InventoryAlertResult">
        <include refid="selectInventoryAlertVo"/>
        <where>  
            <if test="materialId != null">
                AND material_id = #{materialId}
            </if>
            <if test="materialName != null and materialName != ''">
                AND material_name LIKE CONCAT('%', #{materialName}, '%')
            </if>
            <if test="warehouseId != null">
                AND warehouse_id = #{warehouseId}
            </if>
            <if test="status != null and status != ''">
                AND status = #{status}
            </if>
        </where>
        ORDER BY create_time DESC
    </select>
    
    <select id="selectInventoryAlertById" parameterType="Long" resultMap="InventoryAlertResult">
        <include refid="selectInventoryAlertVo"/>
        WHERE id = #{id}
    </select>

    <!-- 查询所有启用的预警配置及其当前库存 -->
    <select id="selectAlertWithCurrentInventory" resultType="cn.sourceplan.warehouse.domain.InventoryAlert">
        SELECT
            a.id,
            a.material_id,
            a.material_number,
            a.material_name,
            a.specification,
            a.warehouse_id,
            a.warehouse_name,
            a.max_quantity,
            a.min_quantity,
            a.status,
            a.remark,
            IFNULL(
                (SELECT SUM(IFNULL(intoG.into_quantity, 0) - IFNULL(outGG.out_quantity, 0))
                 FROM (
                     -- 所有入库数量汇总
                     SELECT
                         SUM(IFNULL(`into`.quantity, 0.0)) AS into_quantity,
                         `into`.warehouse_id,
                         `into`.material_id
                     FROM
                         (
                             SELECT warehouse_id, material_id, quantity FROM wm_procure_into_entry b LEFT JOIN wm_procure_into a ON a.id = b.main_id
                             UNION ALL
                             SELECT warehouse_id, material_id, quantity FROM wm_sale_return_entry b LEFT JOIN wm_sale_return a ON a.id = b.main_id
                             UNION ALL
                             SELECT warehouse_id, material_id, quantity FROM wm_manufacture_into_entry b LEFT JOIN wm_manufacture_into a ON a.id = b.main_id
                             UNION ALL
                             SELECT warehouse_id, material_id, take_quantity AS quantity FROM wm_take_inventory_entry b LEFT JOIN wm_take_inventory a ON a.id = b.main_id WHERE a.initialize_flag = 'Y'
                             UNION ALL
                             SELECT warehouse_id, material_id, quantity FROM wm_profit_loss_record_entry b LEFT JOIN wm_profit_loss_record a ON a.id = b.main_id WHERE a.type = 'A'
                         ) `into`
                     GROUP BY `into`.warehouse_id, `into`.material_id
                 ) `intoG`
                 LEFT JOIN (
                     -- 所有出库数量汇总
                     SELECT
                         SUM(IFNULL(`out`.quantity, 0.0)) AS out_quantity,
                         `out`.warehouse_id,
                         `out`.material_id
                     FROM
                         (
                             SELECT warehouse_id, material_id, quantity FROM wm_production_pick_entry b LEFT JOIN wm_production_pick a ON a.id = b.main_id WHERE a.`status` ='A'
                             UNION ALL
                             SELECT warehouse_id, material_id, quantity FROM wm_sale_out_entry b LEFT JOIN wm_sale_out a ON a.id = b.main_id
                             UNION ALL
                             SELECT warehouse_id, material_id, quantity FROM wm_profit_loss_record_entry b LEFT JOIN wm_profit_loss_record a ON a.id = b.main_id WHERE a.type = 'B'
                             UNION ALL
                             SELECT warehouse_id, material_id, quantity FROM wm_other_out_entry b LEFT JOIN wm_other_out a ON a.id = b.main_id
                         ) `out`
                     GROUP BY `out`.material_id, `out`.warehouse_id
                 ) `outGG` ON `outGG`.material_id = `intoG`.material_id AND `outGG`.warehouse_id = `intoG`.warehouse_id
                 WHERE `intoG`.material_id = a.material_id
                   AND (a.warehouse_id IS NULL OR `intoG`.warehouse_id = a.warehouse_id)
                ), 0
            ) AS current_quantity
        FROM wm_inventory_alert a
        -- 关联菜单表检查仓库管理菜单状态menu_id=2082为仓库管理
        INNER JOIN sys_menu menu ON menu.menu_id = 2082
        WHERE a.status = '0'
          AND menu.visible = '0'  -- 菜单可见0=显示1=隐藏)
          AND menu.status = '0'   -- 菜单启用0=正常1=停用)
        HAVING current_quantity > 0  -- 只查询库存数量大于0的记录已通过子查询计算实时库存
    </select>
        
    <insert id="insertInventoryAlert" parameterType="InventoryAlert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO wm_inventory_alert
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="materialId != null">material_id,</if>
            <if test="materialNumber != null">material_number,</if>
            <if test="materialName != null">material_name,</if>
            <if test="specification != null">specification,</if>
            <if test="warehouseId != null">warehouse_id,</if>
            <if test="warehouseName != null">warehouse_name,</if>
            <if test="maxQuantity != null">max_quantity,</if>
            <if test="minQuantity != null">min_quantity,</if>
            <if test="status != null">status,</if>
            <if test="remark != null">remark,</if>
            <if test="createBy != null">create_by,</if>
            <if test="createTime != null">create_time,</if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="materialId != null">#{materialId},</if>
            <if test="materialNumber != null">#{materialNumber},</if>
            <if test="materialName != null">#{materialName},</if>
            <if test="specification != null">#{specification},</if>
            <if test="warehouseId != null">#{warehouseId},</if>
            <if test="warehouseName != null">#{warehouseName},</if>
            <if test="maxQuantity != null">#{maxQuantity},</if>
            <if test="minQuantity != null">#{minQuantity},</if>
            <if test="status != null">#{status},</if>
            <if test="remark != null">#{remark},</if>
            <if test="createBy != null">#{createBy},</if>
            <if test="createTime != null">#{createTime},</if>
        </trim>
    </insert>

    <update id="updateInventoryAlert" parameterType="InventoryAlert">
        UPDATE wm_inventory_alert
        <trim prefix="SET" suffixOverrides=",">
            <if test="materialId != null">material_id = #{materialId},</if>
            <if test="materialNumber != null">material_number = #{materialNumber},</if>
            <if test="materialName != null">material_name = #{materialName},</if>
            <if test="specification != null">specification = #{specification},</if>
            <if test="warehouseId != null">warehouse_id = #{warehouseId},</if>
            <if test="warehouseName != null">warehouse_name = #{warehouseName},</if>
            <if test="maxQuantity != null">max_quantity = #{maxQuantity},</if>
            <if test="minQuantity != null">min_quantity = #{minQuantity},</if>
            <if test="status != null">status = #{status},</if>
            <if test="remark != null">remark = #{remark},</if>
            <if test="updateBy != null">update_by = #{updateBy},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
        </trim>
        WHERE id = #{id}
    </update>

    <delete id="deleteInventoryAlertById" parameterType="Long">
        DELETE FROM wm_inventory_alert WHERE id = #{id}
    </delete>

    <delete id="deleteInventoryAlertByIds" parameterType="Long">
        DELETE FROM wm_inventory_alert WHERE id IN
        <foreach item="id" collection="array" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>

</mapper>

🎨 前端实现

1. 目录结构

mes-ui/src/
├── api/mes/warehouse/
│   └── inventoryAlert.js                      # API接口
├── views/mes/warehouse/inventoryAlert/
│   ├── index.vue                              # 列表页面
│   └── form.vue                               # 新增/编辑表单(可选)
└── views/index.vue                            # 首页(需修改)

2. API接口inventoryAlert.js

import request from '@/utils/request'

// 查询库存预警列表
export function listInventoryAlert(query) {
  return request({
    url: '/warehouse/inventoryAlert/list',
    method: 'get',
    params: query
  })
}

// 查询库存预警详细
export function getInventoryAlert(id) {
  return request({
    url: '/warehouse/inventoryAlert/' + id,
    method: 'get'
  })
}

// 新增库存预警
export function addInventoryAlert(data) {
  return request({
    url: '/warehouse/inventoryAlert',
    method: 'post',
    data: data
  })
}

// 修改库存预警
export function updateInventoryAlert(data) {
  return request({
    url: '/warehouse/inventoryAlert',
    method: 'put',
    data: data
  })
}

// 删除库存预警
export function delInventoryAlert(id) {
  return request({
    url: '/warehouse/inventoryAlert/' + id,
    method: 'delete'
  })
}

// 获取当前触发的预警信息
export function getTriggeredAlerts() {
  return request({
    url: '/warehouse/inventoryAlert/triggered',
    method: 'get'
  })
}

3. 列表页面:index.vue

<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="物料名称" prop="materialName">
        <el-input
          v-model="queryParams.materialName"
          placeholder="请输入物料名称"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="仓库" prop="warehouseId">
        <el-select v-model="queryParams.warehouseId" placeholder="请选择仓库" clearable filterable>
          <el-option
            v-for="item in warehouseList"
            :key="item.id"
            :label="item.name"
            :value="item.id"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="状态" prop="status">
        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
          <el-option label="启用" value="0" />
          <el-option label="停用" value="1" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="el-icon-plus"
          size="mini"
          @click="handleAdd"
          v-hasPermi="['warehouse:inventoryAlert:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="el-icon-edit"
          size="mini"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['warehouse:inventoryAlert:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="el-icon-delete"
          size="mini"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['warehouse:inventoryAlert:remove']"
        >删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="warning"
          plain
          icon="el-icon-download"
          size="mini"
          @click="handleExport"
          v-hasPermi="['warehouse:inventoryAlert:export']"
        >导出</el-button>
      </el-col>
      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>

    <el-table v-loading="loading" :data="alertList" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="物料编号" align="center" prop="materialNumber" />
      <el-table-column label="物料名称" align="center" prop="materialName" />
      <el-table-column label="规格型号" align="center" prop="specification" />
      <el-table-column label="仓库" align="center" prop="warehouseName">
        <template slot-scope="scope">
          <span>{{ scope.row.warehouseName || '所有仓库' }}</span>
        </template>
      </el-table-column>
      <el-table-column label="库存上限" align="center" prop="maxQuantity" />
      <el-table-column label="库存下限" align="center" prop="minQuantity" />
      <el-table-column label="状态" align="center" prop="status">
        <template slot-scope="scope">
          <el-tag :type="scope.row.status === '0' ? 'success' : 'info'">
            {{ scope.row.status === '0' ? '启用' : '停用' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template slot-scope="scope">
          <el-button
            size="mini"
            type="text"
            icon="el-icon-edit"
            @click="handleUpdate(scope.row)"
            v-hasPermi="['warehouse:inventoryAlert:edit']"
          >修改</el-button>
          <el-button
            size="mini"
            type="text"
            icon="el-icon-delete"
            @click="handleDelete(scope.row)"
            v-hasPermi="['warehouse:inventoryAlert:remove']"
          >删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <pagination
      v-show="total>0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加或修改库存预警对话框 -->
    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="物料" prop="materialId">
          <el-select 
            v-model="form.materialId" 
            placeholder="请选择物料" 
            filterable
            @change="handleMaterialChange"
            style="width: 100%"
          >
            <el-option
              v-for="item in materialList"
              :key="item.materialId"
              :label="item.materialName + ' (' + item.materialNumber + ')'"
              :value="item.materialId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="规格型号">
          <el-input v-model="form.specification" disabled />
        </el-form-item>
        <el-form-item label="仓库" prop="warehouseId">
          <el-select v-model="form.warehouseId" placeholder="请选择仓库(留空表示所有仓库)" clearable style="width: 100%">
            <el-option
              v-for="item in warehouseList"
              :key="item.id"
              :label="item.name"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="库存上限" prop="maxQuantity">
          <el-input-number v-model="form.maxQuantity" :min="0" :precision="2" controls-position="right" style="width: 100%" />
        </el-form-item>
        <el-form-item label="库存下限" prop="minQuantity">
          <el-input-number v-model="form.minQuantity" :min="0" :precision="2" controls-position="right" style="width: 100%" />
        </el-form-item>
        <el-form-item label="状态">
          <el-radio-group v-model="form.status">
            <el-radio label="0">启用</el-radio>
            <el-radio label="1">停用</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="备注">
          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm"> </el-button>
        <el-button @click="cancel"> </el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { listInventoryAlert, getInventoryAlert, addInventoryAlert, updateInventoryAlert, delInventoryAlert } from "@/api/mes/warehouse/inventoryAlert";
import { listWarehouse } from "@/api/mes/warehouse/warehouse";
import { listMaterial } from "@/api/mes/masterdata/material";

export default {
  name: "InventoryAlert",
  data() {
    return {
      loading: true,
      ids: [],
      single: true,
      multiple: true,
      showSearch: true,
      total: 0,
      alertList: [],
      warehouseList: [],
      materialList: [],
      title: "",
      open: false,
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        materialName: null,
        warehouseId: null,
        status: null
      },
      form: {},
      rules: {
        materialId: [
          { required: true, message: "物料不能为空", trigger: "change" }
        ],
        minQuantity: [
          { required: true, message: "库存下限不能为空", trigger: "blur" }
        ]
      }
    };
  },
  created() {
    this.getList();
    this.getWarehouseList();
    this.getMaterialList();
  },
  methods: {
    /** 查询库存预警列表 */
    getList() {
      this.loading = true;
      listInventoryAlert(this.queryParams).then(response => {
        this.alertList = response.rows;
        this.total = response.total;
        this.loading = false;
      });
    },
    /** 获取仓库列表 */
    getWarehouseList() {
      listWarehouse({ pageSize: 99999 }).then(response => {
        this.warehouseList = response.rows;
      });
    },
    /** 获取物料列表(从物料表,只获取启用状态的物料) */
    getMaterialList() {
      listMaterial({ pageSize: 99999, status: '0' }).then(response => {
        this.materialList = response.rows;
      });
    },
    /** 物料选择变化 */
    handleMaterialChange(materialId) {
      const material = this.materialList.find(m => m.materialId === materialId);
      if (material) {
        this.form.materialNumber = material.materialNumber;
        this.form.materialName = material.materialName;
        this.form.specification = material.specification;
      }
    },
    cancel() {
      this.open = false;
      this.reset();
    },
    reset() {
      this.form = {
        id: null,
        materialId: null,
        materialNumber: null,
        materialName: null,
        specification: null,
        warehouseId: null,
        warehouseName: null,
        maxQuantity: null,
        minQuantity: null,
        status: "0",
        remark: null
      };
      this.resetForm("form");
    },
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    resetQuery() {
      this.resetForm("queryForm");
      this.handleQuery();
    },
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    handleAdd() {
      this.reset();
      this.open = true;
      this.title = "添加库存预警";
    },
    handleUpdate(row) {
      this.reset();
      const id = row.id || this.ids[0];
      getInventoryAlert(id).then(response => {
        this.form = response.data;
        this.open = true;
        this.title = "修改库存预警";
      });
    },
    submitForm() {
      this.$refs["form"].validate(valid => {
        if (valid) {
          // 设置仓库名称
          if (this.form.warehouseId) {
            const warehouse = this.warehouseList.find(w => w.id === this.form.warehouseId);
            if (warehouse) {
              this.form.warehouseName = warehouse.name;
            }
          }
          
          if (this.form.id != null) {
            updateInventoryAlert(this.form).then(response => {
              this.$modal.msgSuccess("修改成功");
              this.open = false;
              this.getList();
            });
          } else {
            addInventoryAlert(this.form).then(response => {
              this.$modal.msgSuccess("新增成功");
              this.open = false;
              this.getList();
            });
          }
        }
      });
    },
    handleDelete(row) {
      const ids = row.id || this.ids;
      this.$modal.confirm('是否确认删除库存预警编号为"' + ids + '"的数据项?').then(function() {
        return delInventoryAlert(ids);
      }).then(() => {
        this.getList();
        this.$modal.msgSuccess("删除成功");
      }).catch(() => {});
    },
    handleExport() {
      this.download('warehouse/inventoryAlert/export', {
        ...this.queryParams
      }, `库存预警_${new Date().getTime()}.xlsx`)
    }
  }
};
</script>

🔔 首页预警弹窗

修改首页:mes-ui/src/views/index.vue

在首页的 mounted 生命周期中添加预警检查逻辑使用Notification组件在右上角显示

<script>
import { defermentFactor, outputTrend,qualificationRate,unqualifiedReason } from '@/api/mes/index'
import { getTriggeredAlerts } from "@/api/mes/warehouse/inventoryAlert"

export default {
  name: 'index',
  data() {
    return {
      queryParams: {
        dateQuery: []
      },
      // ... 现有数据
    };
  },
  mounted() {
    this.init()
    // 检查库存预警
    this.checkInventoryAlerts()
  },
  methods: {
    /** 检查库存预警 */
    checkInventoryAlerts() {
      console.log('=== 开始检查库存预警 ===')
      getTriggeredAlerts().then(response => {
        console.log('预警API响应:', response)
        console.log('预警数据:', response.data)
        
        if (response.data && response.data.length > 0) {
          console.log('触发预警,数量:', response.data.length)
          
          // 使用Notification在右上角显示每条预警
          response.data.forEach((alert, index) => {
            setTimeout(() => {
              this.$notify({
                title: '库存预警提醒',
                dangerouslyUseHTMLString: true,
                message: `
                  <div style="line-height: 1.6;">
                    ${alert.message}
                    <div style="margin-top: 10px; color: #909399; font-size: 12px; border-top: 1px solid #EBEEF5; padding-top: 8px;">
                      *若数量上下限不正确,请在库存预警中更改上下限
                    </div>
                  </div>
                `,
                type: alert.type === 'max' ? 'warning' : 'error',
                duration: 5000, // 5秒后自动关闭
                position: 'top-right',
                offset: 80 + (index * 20), // 多条预警错开显示
                showClose: true
              })
            }, index * 300) // 多条预警依次弹出间隔300ms
          })
        } else {
          console.log('没有触发预警或数据为空')
        }
      }).catch(error => {
        console.error('获取库存预警失败:', error)
        console.error('错误详情:', error.response)
      })
    },
    
    // ... 现有其他方法
  }
};
</script>

📋 实施步骤

1. 数据库变更

# 执行SQL脚本
mysql -u用户名 -p密码 数据库名 < .tasks/2025-10-17_兴万达改进(库存预警).sql

2. 后端实现

  1. 创建实体类 InventoryAlert.java
  2. 创建Mapper接口 InventoryAlertMapper.java
  3. 创建XML映射文件 InventoryAlertMapper.xml
  4. 创建Service接口 IInventoryAlertService.java
  5. 创建Service实现 InventoryAlertServiceImpl.java
  6. 创建Controller InventoryAlertController.java

3. 前端实现

  1. 创建API文件 inventoryAlert.js
  2. 创建列表页面 index.vue
  3. 修改首页 index.vue 添加预警弹窗

4. 测试验证

  1. 重启后端服务
  2. 清除浏览器缓存
  3. 登录系统,检查菜单是否显示
  4. 测试增删改查功能
  5. 测试首页预警弹窗

🧪 测试用例

1. 功能测试

测试用例1新增库存预警

  • 操作步骤

    1. 进入库存预警页面
    2. 点击"新增"按钮
    3. 选择物料、仓库
    4. 设置上限1000下限100
    5. 点击"确定"
  • 预期结果

    • 成功新增一条预警配置
    • 列表中显示新增的记录

测试用例2修改库存预警

  • 操作步骤

    1. 选择一条预警记录
    2. 点击"修改"按钮
    3. 修改上限为2000
    4. 点击"确定"
  • 预期结果

    • 成功修改预警配置
    • 列表中显示更新后的数据

测试用例3删除库存预警

  • 操作步骤

    1. 选择一条预警记录
    2. 点击"删除"按钮
    3. 确认删除
  • 预期结果

    • 成功删除预警配置
    • 列表中不再显示该记录

测试用例4触发高于上限预警

  • 操作步骤

    1. 设置物料A的上限为100
    2. 确保物料A的当前库存为150
    3. 退出登录,重新登录
  • 预期结果

    • 首页右侧弹出预警弹窗
    • 显示"物料【A】库存数量高于上限"的提示
    • 3秒后自动关闭

测试用例5触发低于下限预警

  • 操作步骤

    1. 设置物料B的下限为100
    2. 确保物料B的当前库存为50
    3. 退出登录,重新登录
  • 预期结果

    • 首页右侧弹出预警弹窗
    • 显示"物料【B】库存数量低于下限"的提示
    • 3秒后自动关闭

2. 权限测试

测试用例6权限控制

  • 操作步骤

    1. 创建测试角色,仅赋予查询权限
    2. 使用该角色登录
    3. 尝试新增、修改、删除操作
  • 预期结果

    • 只能看到列表
    • 新增、修改、删除按钮不显示

3. 性能测试

测试用例7大数据量测试

  • 操作步骤

    1. 插入1000条预警配置
    2. 访问列表页面
    3. 执行查询、分页操作
  • 预期结果

    • 页面响应时间 < 2秒
    • 分页正常工作

📌 注意事项

  1. 唯一性约束:同一物料在同一仓库只能有一条预警配置
  2. warehouse_id为NULL:表示对该物料的所有仓库进行预警监控
  3. 预警触发条件(需同时满足):
    • 预警配置状态为启用status='0'
    • "仓库管理"菜单可见且启用menu_id=2082, visible='0', status='0'
    • 当前库存数量 > 0通过子查询计算实时库存自动过滤掉从未入过库的物料
    • 库存数量超过上限或低于下限
  4. 预警触发时机:登录首页时检查,也可以考虑定时任务推送
  5. 物料选择来源从物料主数据表md_material读取只显示启用状态status='0')的物料
  6. 性能优化
    • 即时库存查询使用复杂子查询,建议添加索引优化
    • 预警检查可以异步执行,避免阻塞首页加载
    • 预警结果可以考虑添加Redis缓存5分钟过期
  7. 扩展性
    • 可以考虑添加邮件、短信通知功能
    • 可以添加预警历史记录表
    • 可以添加预警统计分析功能
    • 可以添加预警级别(紧急/警告/提示)

📊 效果预览

库存预警列表页面

┌─────────────────────────────────────────────────────────────┐
│  库存预警                                    [新增][修改][删除][导出] │
├─────────────────────────────────────────────────────────────┤
│ 物料编号 │ 物料名称 │ 规格型号 │ 仓库 │ 上限 │ 下限 │ 状态 │ 操作 │
├─────────┼─────────┼─────────┼──────┼──────┼──────┼──────┼──────┤
│ MAT001  │ 钢板    │ 10mm    │ 原材料│ 1000 │ 100  │ 启用 │ 修改删│
│ MAT002  │ 螺丝    │ M8      │ 所有  │ 5000 │ 500  │ 启用 │ 修改删│
└─────────────────────────────────────────────────────────────┘

首页预警通知右上角Notification

                                    ┌──────────────────────────────────┐
                                    │ 库存预警提醒                [×] │
                                    ├──────────────────────────────────┤
                                    │ ⚠ 【库存预警】物料【钢板】在仓库 │
                                    │   【原材料】中的库存数量高于上限 │
                                    │   上限1000件请暂停采购   │
                                    │   或调整库存。                   │
                                    │                                  │
                                    │ *若数量上下限不正确,请在库存预警│
                                    │  中更改上下限                    │
                                    └──────────────────────────────────┘
                                    
                                    ┌──────────────────────────────────┐
                                    │ 库存预警提醒                [×] │
                                    ├──────────────────────────────────┤
                                    │ ❌ 【库存预警】物料【螺丝】在仓库│
                                    │    【所有仓库】中的库存数量低于  │
                                    │    下限下限500件请尽快   │
                                    │    采购补货。                    │
                                    │                                  │
                                    │ *若数量上下限不正确,请在库存预警│
                                    │  中更改上下限                    │
                                    └──────────────────────────────────┘

说明:
- 显示位置:页面右上角
- 显示时长5秒后自动消失
- 多条预警依次错开弹出间隔300ms
- 可手动关闭:点击右上角×号

完成标志

  • SQL脚本编写完成
  • 数据库表创建完成
  • 菜单和权限配置完成
  • 后端实体类、Mapper、Service、Controller实现完成
  • 前端API接口实现完成
  • 前端列表页面实现完成
  • 首页预警Notification实现完成
  • 预警触发条件优化(菜单状态、库存存在性、库存>0
  • 物料选择改为从md_material读取
  • 功能测试完成
  • 上线部署(待部署)

🔄 更新记录

v1.3 - 2025-10-17

  • 移除对 wm_real_time_inventory 表的依赖(该表在部分数据库中不存在)
  • 通过 HAVING current_quantity > 0 自动过滤没有库存的物料
  • 简化SQL查询逻辑提高兼容性

v1.2 - 2025-10-17

  • 首页预警弹窗改为右上角Notification通知
  • 显示时长从3秒改为5秒
  • 多条预警依次错开弹出间隔300ms
  • 增加预警触发条件:
    • 关联sys_menu检查仓库管理菜单状态
    • 只查询库存数量>0的记录
  • 物料选择从md_material读取只显示启用状态

v1.1 - 2025-10-17

  • 完成后端所有代码实现
  • 完成前端所有代码实现
  • 完成基础功能测试

v1.0 - 2025-10-17

  • 需求分析和设计完成
  • SQL脚本编写完成
  • 代码框架设计完成

文档版本v1.3
编写日期2025-10-17
最后更新2025-10-17