54 KiB
54 KiB
兴万达MES系统 - 库存预警功能实现文档
📋 目录
📝 需求说明
1. 功能概述
在仓库管理模块下新增"库存预警"功能,与"即时库存"同级菜单,用于监控物料库存是否超出设定的上下限范围,并在登录首页时弹窗提醒。
重要说明:
- 物料选择直接从物料表(md_material)读取,不再从即时库存读取
- 菜单状态控制预警:读取
sys_menu表中"仓库管理"菜单(menu_id=2082)的状态- 如果
visible='1'(隐藏)或status='1'(停用),则不在首页显示预警 - 只有当
visible='0'(显示)且status='0'(正常)时,才会在首页显示预警
- 如果
2. 核心功能
-
预警配置管理:
- 选择物料(从物料表中选择,只显示状态为"启用"的物料)
- 可选择特定仓库或全部仓库
- 设置库存上限数量
- 设置库存下限数量
- 支持增删改查和导出功能
-
预警触发逻辑:
- 当前库存数量 > 上限数量:触发"高于上限"预警
- 当前库存数量 < 下限数量:触发"低于下限"预警
- 在上下限之间:不触发预警
- 菜单状态控制:检查"仓库管理"菜单(ID=2082)的
visible和status字段visible='0'且status='0':显示预警 ✅visible='1'或status='1':不显示预警 ❌
-
预警展示:
- 登录首页右上角弹出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 | 仓库ID(NULL=所有仓库) |
| 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. Controller:InventoryAlertController.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. 后端实现
- 创建实体类
InventoryAlert.java - 创建Mapper接口
InventoryAlertMapper.java - 创建XML映射文件
InventoryAlertMapper.xml - 创建Service接口
IInventoryAlertService.java - 创建Service实现
InventoryAlertServiceImpl.java - 创建Controller
InventoryAlertController.java
3. 前端实现
- 创建API文件
inventoryAlert.js - 创建列表页面
index.vue - 修改首页
index.vue添加预警弹窗
4. 测试验证
- 重启后端服务
- 清除浏览器缓存
- 登录系统,检查菜单是否显示
- 测试增删改查功能
- 测试首页预警弹窗
🧪 测试用例
1. 功能测试
测试用例1:新增库存预警
-
操作步骤:
- 进入库存预警页面
- 点击"新增"按钮
- 选择物料、仓库
- 设置上限1000,下限100
- 点击"确定"
-
预期结果:
- 成功新增一条预警配置
- 列表中显示新增的记录
测试用例2:修改库存预警
-
操作步骤:
- 选择一条预警记录
- 点击"修改"按钮
- 修改上限为2000
- 点击"确定"
-
预期结果:
- 成功修改预警配置
- 列表中显示更新后的数据
测试用例3:删除库存预警
-
操作步骤:
- 选择一条预警记录
- 点击"删除"按钮
- 确认删除
-
预期结果:
- 成功删除预警配置
- 列表中不再显示该记录
测试用例4:触发高于上限预警
-
操作步骤:
- 设置物料A的上限为100
- 确保物料A的当前库存为150
- 退出登录,重新登录
-
预期结果:
- 首页右侧弹出预警弹窗
- 显示"物料【A】库存数量高于上限"的提示
- 3秒后自动关闭
测试用例5:触发低于下限预警
-
操作步骤:
- 设置物料B的下限为100
- 确保物料B的当前库存为50
- 退出登录,重新登录
-
预期结果:
- 首页右侧弹出预警弹窗
- 显示"物料【B】库存数量低于下限"的提示
- 3秒后自动关闭
2. 权限测试
测试用例6:权限控制
-
操作步骤:
- 创建测试角色,仅赋予查询权限
- 使用该角色登录
- 尝试新增、修改、删除操作
-
预期结果:
- 只能看到列表
- 新增、修改、删除按钮不显示
3. 性能测试
测试用例7:大数据量测试
-
操作步骤:
- 插入1000条预警配置
- 访问列表页面
- 执行查询、分页操作
-
预期结果:
- 页面响应时间 < 2秒
- 分页正常工作
📌 注意事项
- 唯一性约束:同一物料在同一仓库只能有一条预警配置
- warehouse_id为NULL:表示对该物料的所有仓库进行预警监控
- 预警触发条件(需同时满足):
- 预警配置状态为启用(status='0')
- "仓库管理"菜单可见且启用(menu_id=2082, visible='0', status='0')
- 当前库存数量 > 0(通过子查询计算实时库存,自动过滤掉从未入过库的物料)
- 库存数量超过上限或低于下限
- 预警触发时机:登录首页时检查,也可以考虑定时任务推送
- 物料选择来源:从物料主数据表(md_material)读取,只显示启用状态(status='0')的物料
- 性能优化:
- 即时库存查询使用复杂子查询,建议添加索引优化
- 预警检查可以异步执行,避免阻塞首页加载
- 预警结果可以考虑添加Redis缓存(5分钟过期)
- 扩展性:
- 可以考虑添加邮件、短信通知功能
- 可以添加预警历史记录表
- 可以添加预警统计分析功能
- 可以添加预警级别(紧急/警告/提示)
📊 效果预览
库存预警列表页面
┌─────────────────────────────────────────────────────────────┐
│ 库存预警 [新增][修改][删除][导出] │
├─────────────────────────────────────────────────────────────┤
│ 物料编号 │ 物料名称 │ 规格型号 │ 仓库 │ 上限 │ 下限 │ 状态 │ 操作 │
├─────────┼─────────┼─────────┼──────┼──────┼──────┼──────┼──────┤
│ 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