基于Java的仓库管理系统设计与实现(六)

最近比较忙,每天进度比较慢,不过大部分都比较顺利,菜单树等等,目前正在处理后端出入库管理,对其重新考虑了一下思路,改动还是比较大,并且还使用到了触发器。除此之外,考虑到用户注销,需要实现token过期思路,这里考虑本地部署或服务器问题,没有使用redis,暂时使用了map去维护一个黑名单列表,用户取消登陆后请求接口,添加到黑名单实现失效。

1.后端

1.1 使用Map集合维护黑名单

这里是在JwtUtil中编写的,编写添加黑名单方法,校验是否在黑名单中存在,以及定时清除(因为添加黑名单时,值为过期时间)。

  1. JwtUtil.java
package cn.xy21lin.wms_lin.util;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.convert.NumberWithFormat;
import cn.hutool.core.date.DateTime;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import cn.xy21lin.wms_lin.config.CustomerAuthenticationException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


@Component
public class JwtUtil {
    // JWT密钥
    private static final String SECRET_KEY = "23306010225";
    // Token过期时间,单位为毫秒,这里设置为1天
    private static final long EXPIRATION_TIME = 86400000;
    // 维护黑名单,用于存储注销的Token
    private static final ConcurrentHashMap<String, Long> blackList = new ConcurrentHashMap<>();

    /**
     * 生成JWT Token
     *
     * @param payload 包含用户信息的Map对象
     * @return 生成的JWT Token
     */
    public static String createToken(Map<String, Object> payload) {
        // 获取当前时间
        DateTime now = DateTime.now();
        // 计算Token的过期时间
        DateTime date = new DateTime(now.getTime() + EXPIRATION_TIME);

        // 设置签发时间
        payload.put(JWTPayload.ISSUED_AT, now);
        // 设置过期时间
        payload.put(JWTPayload.EXPIRES_AT, date);
        // 设置生效时间,确保Token在签发后立即生效
        payload.put(JWTPayload.NOT_BEFORE, now);

        // 使用Hutool工具类生成JWT Token
        return JWTUtil.createToken(payload, SECRET_KEY.getBytes());
    }


    /**
     * 解析JWT Token,获取Payload信息
     *
     * @param token 需要解析的JWT Token
     * @return 解析后的Payload对象
     */
    public static JWTPayload parseToken(String token) {
        JWT jwt = null;
        // 使用Hutool工具类解析JWT Token
        try {
            jwt = JWTUtil.parseToken(token);
        } catch (Exception e) {
            throw new CustomerAuthenticationException("token无效");
        }

        // 设置密钥并验证Token的签名
        if (!jwt.setKey(SECRET_KEY.getBytes()).verify() || !jwt.validate(0)) {
            throw new CustomerAuthenticationException("token无效");
        }

//        // 验证Token是否过期
//        if (!jwt.validate(0)) {
//            throw new CustomerAuthenticationException("Token已过期");
//        }

        // 返回解析后的Payload对象
        return jwt.getPayload();
    }

    /**
     * 将Token添加到黑名单
     *
     * @param token 需要添加到黑名单的Token
     */
    public static void addToBlacklist(String token) {
        try {
            // 解析 JWT 令牌
            JWT jwt = JWTUtil.parseToken(token);
            //获取一下过期时间
            NumberWithFormat claim = (NumberWithFormat) JwtUtil.parseToken(token).getClaim(JWTPayload.EXPIRES_AT);
            long expiredTime = Convert.toDate(claim).getTime();
            //System.out.println("获取到的过期时间:" + expiredTime);
            blackList.put(token, expiredTime);
        } catch (Exception e) {
            // 捕获并抛出自定义异常
            throw new CustomerAuthenticationException("添加黑名单出错: " + e.getMessage());
        }
    }

    /**
     * 检查Token是否在黑名单中
     *
     * @param token 需要检查的Token
     * @return 如果Token在黑名单中,返回true;否则返回false
     */
    public static boolean isTokenBlacklisted(String token) {
        Long expirationTime = blackList.get(token);
        if (expirationTime == null) {
            return false;
        }
        // 检查Token是否已过期
        return System.currentTimeMillis() < expirationTime;
    }

    /**
     * 定期清理过期的Token
     */
    @Scheduled(fixedRate = 3600000) // 每小时执行一次
    public void cleanExpiredTokens() {
        //System.out.println("执行了自动清除");
        long currentTime = System.currentTimeMillis();
        blackList.entrySet().removeIf(entry -> entry.getValue() < currentTime);
    }
}
  1. 需要在SpringBootApplication启动类上添加注解@EnableScheduling,启动定时功能。
  2. 编写注销接口,添加黑名单(我为了省事,写在了UserController里,本来打算写在LoginController中,但是前端封装的axios,过滤了/auth/**,不携带token)
    @GetMapping("/logout")
    @Operation(summary = "注销登录", description = "注销登录")
    @ApiResponse(responseCode = "200", description = "注销登录成功", content = @Content(schema =  @Schema(example = "{\"code\": 200, \"message\": \"注销登录成功\", \"data\": null, \"success\": \"true\"}")))
    public Result<?> logout(
            @RequestHeader String token
    ) {
        //先将token放入黑名单
        JwtUtil.addToBlacklist(token);
        //返回响应
        return Result.success().setMessage("注销登录成功");
    }
  1. 在Jwt过滤器中校验是否在黑名单中JwtFilter.java
    JwtFilter.java
@Component
public class JwtFilter extends OncePerRequestFilter {
    //...省略其他方法

    /**
     * 验证请求中的Token。
     *
     * @param request 请求对象
     * @param response 响应对象
     * @throws AuthenticationException 如果Token无效或缺失,则抛出该异常
     */
    private void verifyToken(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 从请求头中获取Token
        String token = request.getHeader("token");
        // 如果请求头中没有Token,则从请求参数中获取
        if (token == null || token.isEmpty()) {
            token = request.getParameter("token");
        }
        // 如果仍然没有Token,则抛出异常
        if (token == null || token.isEmpty()) {
            throw new CustomerAuthenticationException("缺少必要参数token");
        }
        // Token存在,尝试从Redis中获取对应的Token值
//        String tokenKey = "token:" + token;
//        String redisToken = redisUtils.get(tokenKey);
        // 校验Redis中的Token是否存在并且与提供的Token一致
//        if (redisToken == null || !redisToken.equals(token)) {
//            throw new CustomerAuthenticationException("token已过期或无效");
//        }
        // 检测是否在黑名单中
        if (JwtUtil.isTokenBlacklisted(token)) {
            System.out.println("token在黑名单,token无效");
            throw new CustomerAuthenticationException("token无效");
        }

        // 解析Token获取用户名
        String username = JwtUtil.parseToken(token).getClaim("username").toString();
        if (username == null) {
            throw new CustomerAuthenticationException("token无效");
        }
        // 根据用户名加载用户详情
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails == null) {
            throw new CustomerAuthenticationException("token无效");
        }
        // 所有验证通过,创建认证令牌
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        // 设置请求的详细信息
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        // 将认证信息存入SecurityContext
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
}

1.2 菜单树

  1. 菜单路由树(具体路由处理交给前端)MenusTreeUtil.java
package cn.xy21lin.wms_lin.util;

import cn.xy21lin.wms_lin.entity.Menu;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MenusTreeUtil {
    public static  List<Menu> buildMenuTree(List<Menu> menus) {
        Map<Long, Menu> menuMap = new HashMap<>();
        List<Menu> rootMenus = new ArrayList<>();
        // 将所有菜单放入map中
        for (Menu menu : menus) {
            menuMap.put(menu.getId(), menu);
        }

        // 构建菜单树
        for (Menu menu : menus) {
            if (menu.getParentId() == null || menu.getParentId() == 0) {
                rootMenus.add(menu);
            } else {
                Menu parentMenu = menuMap.get(menu.getParentId());
                if (parentMenu != null) {
                    if (parentMenu.getChildren() == null) {
                        parentMenu.setChildren(new ArrayList<>());
                    }
                    parentMenu.getChildren().add(menu);
                }
            }
        }

        return rootMenus;
    }
}

  1. 前端处理动态构建路由
  import { useRouter } from 'vue-router';
  import request from '@/utils/request.js';
  const router = useRouter();
  // 首先把你需要动态路由的组件地址全部获取
  let modules = import.meta.glob('@/views/**/*.vue');

  // 路由处理逻辑
  function generateRoutes(menus, parentPath = '') {
    menus.forEach((menu) => {
      const currentPath = [];
      // 只处理菜单类型为1的项(菜单项)
      if (menu.menuType === 1) {
        // 处理路径拼接逻辑(兼容带斜杠和不带斜杠的情况)
        const currentPath = [parentPath, menu.path]
          .join('/')
          .replace(/\/+/g, '/')
          .replace(/\/$/, ''); // 移除末尾斜杠
        // 检查是否已存在该路由
        if (!router.hasRoute(currentPath)) {
          // 构建正确的路径
          const correctPath = `/src/views${currentPath}.vue`;
          // 获取对应的导入函数
          const importFunction = modules[correctPath];
          router.addRoute({
            path: currentPath,
            name: currentPath.slice(1).replace(/\//g, '-') || 'root', // 生成路由名称
            component: () => importFunction(),
            meta: {
              menuId: menu.id,
              title: menu.menuName
            }
          });
        }
      }

      // 递归处理子菜单
      if (menu.children && menu.children.length > 0) {
        generateRoutes(menu.children, currentPath);
      }
    });
  }

  // 获取菜单路由树方法
  const getMenuList = async () => {
    try {
      const response = await request.post('system/menu/list');

      if (response.code === 200) {
        menus.value = response.data;
        generateRoutes(menus.value); // 生成路由
        //这里就是封装的一个ELMessage
        showMessage('success', '菜单加载成功');
        //这是获取新增/编辑对话框中的树方法,下面会给出
        getMenuTreeList(); // 获取菜单树
      } else {
        showMessage('error', response.message);
      }
    } catch (error) {
      showMessage('error', '获取菜单失败');
    }
  };
  

: 这里有个大坑,不知道是不是我配置的问题,尝试了挺多办法,百度搜到的就是let modules = import.meta.glob('@/views/**/*.vue');这样子去获取全部的一个vue组件,然后再去以键值的方式去加载具体的哪个,当时试过好几次就是不行,直到看到一个评论说,把@改成/src就可以了,不识别@,改成/src以后,果然可以了,但是我明明配置了@,其余组件都是正常能导入,但是const correctPath = /src/views${currentPath}.vue;在这里使用@/views${currentPath}.vue就是不可以,很奇怪。

  1. 当前菜单路由树界面
    menusTree.png
  2. 对话框菜单树(很相似,方便顶级父菜单显示,我手动设置了个顶级菜单,不设置也行,不选就是了)
package cn.xy21lin.wms_lin.util;

import cn.xy21lin.wms_lin.entity.Menu;
import cn.xy21lin.wms_lin.vo.MenuTreeVo;

import java.util.List;
import java.util.Map;
import java.util.HashMap;

public class MenuTreeUtil {
    public static MenuTreeVo buildMenuTree(List<Menu> menus) {
        MenuTreeVo rootMenuTree = new MenuTreeVo(0L, "顶级菜单");
        // 先构建菜单的映射,方便查找
        Map<Long, MenuTreeVo> menuMap = new HashMap<>();
        // 初始化菜单映射
        for (Menu menu : menus) {
            menuMap.put(menu.getId(), new MenuTreeVo(menu.getId(), menu.getMenuName()));
        }
        // 构建菜单树
        for (Menu menu : menus) {
            Long parentId = menu.getParentId();
            if (parentId == null || parentId == 0) {
                // 父级菜单
                rootMenuTree.getChildren().add(menuMap.get(menu.getId()));
            } else {
                // 查找父菜单
                MenuTreeVo parent = menuMap.get(parentId);
                if (parent != null) {
                    parent.getChildren().add(menuMap.get(menu.getId()));
                }
            }
        }
        return rootMenuTree;
    }
}
  1. 前端对话框菜单树方法(就是前面的getMenuTreeList方法),并且我才请求菜单路由树的时候就请求了,并且通过pinia持久化存储了,后面menu.vue组件直接在store中获取,不必在请求。
// 持久化store
import { defineStore } from 'pinia'

// 定义一个store,第一个参数是store的唯一标识,建议使用文件名,第二个参数使用setup()函数 或 option 对象,这里使用setup()函数
// 使用export const useXXXStore = defineStore('web',()=>{...}) 定义一个store,并导出一个useXXXStore函数,调用该函数可以获取到该store的实例
// useXXXStore 命名规范,在标识前加use,后加store
export const useMenuStore = defineStore('menu', () => {
    const menus = reactive({
        menuData: [],
    })


    const setMenuData = (data) => {
        menus.menuData = data
    }

    //返回定义的数据
    return {
        menus,
        setMenuData
    }
},
    // 主要就是这一段  注意位置,是在defineStore第三个参数的位置
    {
        persist: true // 开启持久化
    })


  // 这里才是菜单树方法,注释都是我写的时候就写了,也很简单,毕竟后端处理完根据前端组件需求,直接返回了指定格式,直接存就可以
  // 获取菜单树方法
  const getMenuTreeList = async () => {
    try {
      const response = await request.post('system/menu/menuTreeData');

      if (response.code === 200) {
        menuStore.setMenuData(response.data);
      }
    } catch (error) {
      showMessage('error', '获取菜单树失败');
    }
  };
  1. menu.vue组件使用(使用的是TreeSelect 树形选择
<!--...省略其他代码 -->
    <el-form-item label="上级菜单" class="form-item-center" :label-width="formLabelWidth">
    <!-- <el-input autocomplete="off" v-model="dialogForm.parentId" placeholder="请选择父级别菜单" /> -->
    <el-tree-select v-model="dialogForm.parentId" :data="menuData" check-strictly
    :render-after-expand="false" />
    </el-form-item>
<!--...省略其他代码 -->

<script setup>
  import request from '@/utils/request';
  //使用对应store就需要去导入它
  import { useMenuStore } from '@/store/menu';
  // 设置响应式数据,是一个数组
  const menuData = ref([]);
  //... 省略其他代码
  // 修改后的获取菜单方法
  //获取菜单方法,这里是在获取表格中菜单列表时候,就把菜单树数据从store中获取后添加了
  const getMenuList = async () => {
    try {
      const response = await request.post('system/menu/list');
      if (response.code === 200) {
        tableData.value = response.data;
        // 设置菜单树数据
        menuData.value.push(menuStore.menus.menuData);
      }
    } catch (error) {
      showMessage('error', '获取菜单失败');
    } finally {
      isTableLoading.value = false;
    }
  };
  //... 省略其他代码
</script>
  1. 菜单树界面
    dialogMenuTree.png
  2. 菜单路由树返回的格式(忘记给出返回格式了,补上,只用关注data中的内容,下面的菜单树也一样)
// 菜单路由树格式
{
    "code": 200,
    "message": "获取成功",
    "data": [
        {
            "id": 1,
            "menuName": "系统管理",
            "path": "#",
            "parentId": 0,
            "icon": "Setting",
            "orderNum": 0,
            "menuType": 0,
            "permissionName": null,
            "description": "系统管理总菜单",
            "isDeleted": 0,
            "isHidden": 0,
            "children": [
                {
                    "id": 2,
                    "menuName": "用户管理",
                    "path": "/system/user",
                    "parentId": 1,
                    "icon": "User",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "system:user",
                    "description": "用户管理目录",
                    "isDeleted": 0,
                    "isHidden": 0,
                    "children": [
                        {
                            "id": 17,
                            "menuName": "新增用户",
                            "path": "/system/user/add",
                            "parentId": 2,
                            "icon": "",
                            "orderNum": 2,
                            "menuType": 2,
                            "permissionName": "system:user:add",
                            "description": "新增用户按钮",
                            "isDeleted": 0,
                            "isHidden": 0
                        }
                    ]
                },
                {
                    "id": 3,
                    "menuName": "角色管理",
                    "path": "/system/role",
                    "parentId": 1,
                    "icon": "UserFilled",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "system:role",
                    "description": "角色管理目录",
                    "isDeleted": 0,
                    "isHidden": 0
                },
                {
                    "id": 4,
                    "menuName": "菜单管理",
                    "path": "/system/menu",
                    "parentId": 1,
                    "icon": "Menu",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "system:menu",
                    "description": null,
                    "isDeleted": 0,
                    "isHidden": 0
                }
            ]
        },
        {
            "id": 5,
            "menuName": "货物管理",
            "path": "#",
            "parentId": 0,
            "icon": "ShoppingTrolley",
            "orderNum": 0,
            "menuType": 0,
            "permissionName": null,
            "description": null,
            "isDeleted": 0,
            "isHidden": 0,
            "children": [
                {
                    "id": 6,
                    "menuName": "类别管理",
                    "path": "/product/category",
                    "parentId": 5,
                    "icon": "ShoppingBag",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "product:category",
                    "description": null,
                    "isDeleted": 0,
                    "isHidden": 0
                },
                {
                    "id": 7,
                    "menuName": "商品管理",
                    "path": "/product/product",
                    "parentId": 5,
                    "icon": "Goods",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "product:product",
                    "description": null,
                    "isDeleted": 0,
                    "isHidden": 0
                },
                {
                    "id": 8,
                    "menuName": "库存管理",
                    "path": "/product/inventory",
                    "parentId": 5,
                    "icon": "List",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "product:inventory",
                    "description": null,
                    "isDeleted": 0,
                    "isHidden": 0
                }
            ]
        },
        {
            "id": 9,
            "menuName": "出入库管理",
            "path": "#",
            "parentId": 0,
            "icon": "Van",
            "orderNum": 0,
            "menuType": 0,
            "permissionName": null,
            "description": null,
            "isDeleted": 0,
            "isHidden": 0,
            "children": [
                {
                    "id": 10,
                    "menuName": "出入库记录",
                    "path": "/warehouse/inoutRecord",
                    "parentId": 9,
                    "icon": "Ticket",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "warehouse:inoutRecord",
                    "description": null,
                    "isDeleted": 0,
                    "isHidden": 0
                },
                {
                    "id": 11,
                    "menuName": "出入库明细",
                    "path": "/warehouse/inoutDetail",
                    "parentId": 9,
                    "icon": "Tickets",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "warehouse:inoutDetail",
                    "description": null,
                    "isDeleted": 0,
                    "isHidden": 0
                },
                {
                    "id": 18,
                    "menuName": "仓库管理",
                    "path": "/warehouse/warehouse",
                    "parentId": 9,
                    "icon": "House",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "warehouse:warehouse",
                    "description": "仓库管理目录",
                    "isDeleted": 0,
                    "isHidden": 0
                },
                {
                    "id": 25,
                    "menuName": "库存预警管理",
                    "path": "/warehouse/stockWarning",
                    "parentId": 9,
                    "icon": "WarnTriangleFilled",
                    "orderNum": 0,
                    "menuType": 1,
                    "permissionName": "warehouse:stockWarning",
                    "description": null,
                    "isDeleted": 0,
                    "isHidden": 0
                }
            ]
        },
        {
            "id": 12,
            "menuName": "合作方管理",
            "path": "#",
            "parentId": 0,
            "icon": "Briefcase",
            "orderNum": 1,
            "menuType": 0,
            "permissionName": null,
            "description": null,
            "isDeleted": 0,
            "isHidden": 0,
            "children": [
                {
                    "id": 13,
                    "menuName": "供应商管理",
                    "path": "/business/supplier",
                    "parentId": 12,
                    "icon": "WalletFilled",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "business:supplier",
                    "description": null,
                    "isDeleted": 0,
                    "isHidden": 0
                },
                {
                    "id": 14,
                    "menuName": "客户管理",
                    "path": "/business/client",
                    "parentId": 12,
                    "icon": "Avatar",
                    "orderNum": 1,
                    "menuType": 1,
                    "permissionName": "business:client",
                    "description": null,
                    "isDeleted": 0,
                    "isHidden": 0
                }
            ]
        }
    ],
    "success": true
}
  1. 菜单树格式
{
    "code": 200,
    "message": "获取成功",
    "data": {
        "label": "顶级菜单",
        "value": 0,
        "children": [
            {
                "label": "系统管理",
                "value": 1,
                "children": [
                    {
                        "label": "用户管理",
                        "value": 2,
                        "children": [
                            {
                                "label": "新增用户",
                                "value": 17,
                                "children": []
                            },
                            {
                                "label": "123",
                                "value": 27,
                                "children": []
                            }
                        ]
                    },
                    {
                        "label": "角色管理",
                        "value": 3,
                        "children": []
                    },
                    {
                        "label": "菜单管理",
                        "value": 4,
                        "children": []
                    }
                ]
            },
            {
                "label": "货物管理",
                "value": 5,
                "children": [
                    {
                        "label": "类别管理",
                        "value": 6,
                        "children": []
                    },
                    {
                        "label": "商品管理",
                        "value": 7,
                        "children": []
                    },
                    {
                        "label": "库存管理",
                        "value": 8,
                        "children": []
                    }
                ]
            },
            {
                "label": "出入库管理",
                "value": 9,
                "children": [
                    {
                        "label": "出入库记录",
                        "value": 10,
                        "children": []
                    },
                    {
                        "label": "出入库明细",
                        "value": 11,
                        "children": []
                    },
                    {
                        "label": "仓库管理",
                        "value": 18,
                        "children": []
                    },
                    {
                        "label": "库存预警管理",
                        "value": 25,
                        "children": []
                    }
                ]
            },
            {
                "label": "合作方管理",
                "value": 12,
                "children": [
                    {
                        "label": "供应商管理",
                        "value": 13,
                        "children": []
                    },
                    {
                        "label": "客户管理",
                        "value": 14,
                        "children": []
                    }
                ]
            }
        ]
    },
    "success": true
}

2. 数据库触发器

这里主要记录触发器。

2.1 库存预警触发器

-- 修改一下结束符
DELIMITER //

CREATE TRIGGER stock_warning_trigger
BEFORE UPDATE ON biz_inventory
FOR EACH ROW
BEGIN
    -- 定义最小库存
    DECLARE min_stock_temp INT;   
    -- 定义库存预警表id
    DECLARE stock_id_temp INT;   
    DECLARE record_exists INT DEFAULT 0;
    -- 获取产品的最小库存  这里INTO,是将查询到的min_stock 赋值给 定义的变量 min_stock_temp,New是更新后新纪录的引用,对应的OLD就是老记录,也就是修改前
    SELECT min_stock INTO min_stock_temp FROM biz_product WHERE biz_product.id = NEW.product_id;

    -- 检查库存预警表中是否存在记录
    SELECT COUNT(*) INTO record_exists FROM biz_stock_warning WHERE product_id = NEW.product_id and is_flag = 0;
				
    -- 检查库存是否低于最小库存
    IF NEW.quantity < min_stock_temp THEN
        IF record_exists > 0 THEN
        -- 存在预警记录,直接更新预警时间和当前库存
          UPDATE biz_stock_warning SET biz_stock_warning.current_stock = NEW.quantity and biz_stock_warning.warning_time = NOW() WHERE id = stock_id_temp and is_flag = 0;
        ELSE
          -- 插入库存预警信息到库存预警表
            INSERT INTO biz_stock_warning (product_id, current_stock)
            VALUES (NEW.product_id, NEW.quantity);
        END IF;
    END IF;
		
    -- 检查库存是否超过了最小库存
    IF NEW.quantity > min_stock_temp THEN
        -- 检查库存预警表中是否存在记录
        SELECT COUNT(*) INTO record_exists FROM biz_stock_warning WHERE product_id = NEW.product_id and is_flag = 0;
        IF record_exists > 0 THEN
            -- 获取库存预警表中的记录 ID
            SELECT id INTO stock_id_temp FROM biz_stock_warning WHERE product_id = NEW.product_id  and is_flag = 0;            
            -- 更新 is_flag 字段为 1  新设置的字段,标识这个记录的处理状态,不然同产品可以有多个预警记录,更新时候不根据状态,那问题太大了,如果有未处理的记录,直接覆盖上去就可以,我是这样子想的
            UPDATE biz_stock_warning SET is_flag = 1 WHERE id = stock_id_temp;
        END IF;
    END IF;
END //

-- 结束符号改回;
DELIMITER ;

-- 删除触发器
DROP TRIGGER stock_warning_trigger;

2.2 自增菜单触发器

这里是因为,我是用管理员账号进行增加菜单操作时,刷新发现没有,才想起来,我还要手动添加权限才可以,比较烦人,所以写这个触发器,新增后会自动添加权限到管理员角色下,就一句话,比较简单。

DELIMITER //

-- 注意这里,是AFTER INSERT 新增后
CREATE TRIGGER menu_insert_admin
AFTER INSERT ON sys_menu
FOR EACH ROW
BEGIN
    -- 1是管理员角色id,NEW.id也就是新增菜单的自增的主键id了
	INSERT INTO sys_role_menu VALUES(1,NEW.id);
END //

DELIMITER ;

-- 删除触发器
DROP TRIGGER menu_insert_admin

2.3 审核通过触发器

这里是考虑,每次出入库,都需要管理员去审核后,才能对产品进行的操作,本来打算使用代码实现,刚好在弄触发器,想着可以使用触发器试试,还没验证出库,还没写到那里,就验证了新产品入库,嘎嘎。

DELIMITER //

CREATE TRIGGER insertOrUpdate_product
BEFORE UPDATE ON biz_inout_record
FOR EACH ROW
BEGIN
    -- 定义产品id
    DECLARE productId INT;
    -- 定义产品名称
    DECLARE productName VARCHAR(255);
    -- 定义分类id
    DECLARE categoryId INT;
    -- 定义仓库id
    DECLARE warehouseyId INT;
    -- 定义产品最小库存
    DECLARE minStock INT;
    -- 定义产品售价
    DECLARE salePrice DECIMAL(10, 2);
    -- 定义产品进价
    DECLARE purchasePrice DECIMAL(10, 2);
    -- 定义产品单位
    DECLARE productUnit VARCHAR(50);
    -- 定义产品规格
    DECLARE productSpec VARCHAR(255);
    -- 定义产品更新数量
    DECLARE quantityNum INT;

    IF NEW.audit_status = '已通过' THEN
        -- 如果productId 不为空 就是更新,否则新增
        SELECT product_id, product_name, category_id, warehouse_id, min_stock, sale_price, purchase_price, product_unit, product_spec, quantity
        INTO productId, productName, categoryId, warehouseyId, minStock, salePrice, purchasePrice, productUnit, productSpec, quantityNum
        FROM biz_inout_detail
        WHERE record_id = NEW.record_no;

        IF productId IS NOT NULL THEN
            -- 不为空 更新
            -- 判断 入库 就是 +  出库 就是 -  这里还在想,更新产品入库,到底能不能修改其他内容,规格、单位等,如果修改了,应该就算新产品了,所以还是没对其他字段更新。
            IF NEW.operation_type = '入库' THEN
                UPDATE biz_inventory SET quantity = quantity + quantityNum WHERE product_id = productId;
            ELSE
                UPDATE biz_inventory SET quantity = quantity - quantityNum WHERE product_id = productId;
            END IF;
        ELSE
            -- 新增产品
            INSERT INTO biz_product (`name`, category_id, spec, unit, purchase_price, sale_price, min_stock)
            VALUES (productName, categoryId, productSpec, productUnit, purchasePrice, salePrice, minStock);

            -- 获取新增产品的 productId
            SET productId = LAST_INSERT_ID();

            -- 新增库存
            INSERT INTO biz_inventory (warehouse_id, product_id, quantity)
            VALUES (warehouseyId, productId, quantityNum);
        END IF;
    END IF;
END //

DELIMITER ;

-- 删除触发器
DROP TRIGGER insertOrUpdate_product;

: 写的使用就在想,我入库有数量,是对应的库存信息啊,可是我产品库存是有单独的表的,那我这个产品id都不知道,怎么去存库存呢,老样子,百度看看,不写这玩意真不知道,给我推荐了这个方法LAST_INSERT_ID(),解释为:使用 LAST_INSERT_ID() 函数获取新插入记录的 productId。这个时候,获取id方法解决了,我又在考虑,我一条一条加,肯定能获取到最新一次的,要是多个用户同时添加,并发一下不久寄寄了?再搜搜看。解释:在并发操作场景下,直接使用 SET productId = LAST_INSERT_ID(); 通常不会出问题,因为 LAST_INSERT_ID() 是一个会话级别的函数,它返回的是当前会话中最后一次插入操作所生成的自增 ID 值,不同会话之间的 LAST_INSERT_ID() 调用是相互独立的,没理解的话再看看例子。(注意注意,这个是在mysql中使用的,不同数据库是不一样的。)
假设有两个并发的数据库连接(会话 A 和会话 B)同时执行插入操作:

  • 会话 A 执行 INSERT 操作插入一条记录到 biz_product 表,生成自增 ID 为 100。此时会话 A 调用 LAST_INSERT_ID() 会返回 100。
  • 与此同时,会话 B 也执行 INSERT 操作插入一条记录到 biz_product 表,生成自增 ID 为 101。会话 B 调用 LAST_INSERT_ID() 会返回 101,而会话 A 的 LAST_INSERT_ID() 仍然返回 100,不会受到会话 B 插入操作的影响。