Files
MES/yawei-mes/.tasks/2025-10-17_兴万达改进(库存预警).md

1468 lines
54 KiB
Markdown
Raw Permalink Normal View History

2026-04-02 10:38:23 +08:00
# 兴万达MES系统 - 库存预警功能实现文档
## 📋 目录
- [需求说明](#需求说明)
- [数据库设计](#数据库设计)
- [后端实现](#后端实现)
- [前端实现](#前端实现)
- [首页预警弹窗](#首页预警弹窗)
- [实施步骤](#实施步骤)
- [测试用例](#测试用例)
---
## 📝 需求说明
### 1. 功能概述
在仓库管理模块下新增"库存预警"功能,与"即时库存"同级菜单,用于监控物料库存是否超出设定的上下限范围,并在登录首页时弹窗提醒。
**重要说明**
- 物料选择直接从物料表md_material读取不再从即时库存读取
- **菜单状态控制预警**:读取 `sys_menu` 表中"仓库管理"菜单menu_id=2082的状态
- 如果 `visible='1'`(隐藏)或 `status='1'`(停用),则不在首页显示预警
- 只有当 `visible='0'`(显示)且 `status='0'`(正常)时,才会在首页显示预警
### 2. 核心功能
1. **预警配置管理**
- 选择物料(从物料表中选择,只显示状态为"启用"的物料)
- 可选择特定仓库或全部仓库
- 设置库存上限数量
- 设置库存下限数量
- 支持增删改查和导出功能
2. **预警触发逻辑**
- 当前库存数量 > 上限数量:触发"高于上限"预警
- 当前库存数量 < 下限数量触发"低于下限"预警
- 在上下限之间:不触发预警
- **菜单状态控制**:检查"仓库管理"菜单ID=2082`visible``status` 字段
- `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`
```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`
```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`
```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`
```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`
```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
<?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`
```javascript
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`
```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组件在右上角显示
```vue
<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. 数据库变更
```bash
# 执行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
- 可手动关闭:点击右上角×号
```
---
## ✅ 完成标志
- [x] SQL脚本编写完成
- [x] 数据库表创建完成
- [x] 菜单和权限配置完成
- [x] 后端实体类、Mapper、Service、Controller实现完成
- [x] 前端API接口实现完成
- [x] 前端列表页面实现完成
- [x] 首页预警Notification实现完成
- [x] 预警触发条件优化(菜单状态、库存存在性、库存>0
- [x] 物料选择改为从md_material读取
- [x] 功能测试完成
- [ ] 上线部署(待部署)
---
## 🔄 更新记录
### 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