ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Web的仓库管理系统架构:从Spring Boot后端到Vue前端部署实践

基于Web的仓库管理系统架构:从Spring Boot后端到Vue前端部署实践 简介基于WEB的仓库管理系统完整设计与实现资料面向计算机相关专业毕业生、课程设计学生以及需要快速搭建类似系统的开发者。系统采用经典的B/S架构围绕入库、出库、存储、盘点等核心业务完整展示从需求分析、数据库设计到前后端功能实现的整个过程可作为毕业设计、课程设计或项目实战的参考模板。压缩包共12个文件约60.02MB包含MySQL数据库脚本、项目源码包、毕业设计论文、答辩PPT、任务书文档以及项目部署和核心功能操作录屏视频。其中mp4视频覆盖数据库创建、项目启动和入库、出库、商品查看、用户注册、个人信息管理等模块方便对照源码理解每个环节。已有54人学习下载适合用于毕业设计选题参考、仓库管理原型开发或相关技术的综合练习。1. 基于WEB的仓库管理系统到底在解决什么账实不符、多人协作与数据黑匣子想象一下凌晨一点的仓库管理员拿着一沓纸质单据和Excel表格对账发现账面库存比实物少了47件——这不是某个人的工作失误而是工具在根子上就撑不住多人协作。基于WEB的仓库管理系统核心不是把Excel搬到网页上而是让每一次入库、出库、调拨都直接作用于同一份实时数据让仓管、采购、财务看到的是同一个数字。这篇笔记会从技术选型讲到部署上线覆盖后端并发控制、前端页面实现、以及上线之后最容易被忽略的账实核对适合正在做毕业设计的Web方向学生也适合承接中小企业内部工具的一线工程师。它解决的问题很朴素让仓库不再是一个数据黑匣子。2. 技术选型与数据库设计为什么我选Spring Boot Vue三张核心表怎么建2.1 企业级Web开发选型对比仓库系统的技术栈不是越新越好仓库管理系统的业务特征很明确并发量不会特别高一个中型仓库同时在线操作的人也就几十个但数据一致性要求极高库存记录一旦错了补账成本远超代码成本报表和权限是刚需登录后不同角色看到不同菜单部署环境往往是中小企业的一台普通云服务器。带着这些约束去做技术选型而不是先定一个“热门框架再往里套”才不容易翻车。我的常规选择是Spring Boot Vue而不是PHPLaravel或Node.jsExpress。原因有三点第一Spring Boot的生态在后台管理系统里最成熟——MyBatis做数据访问、Spring Security做权限、事务注解一行搞定Java类型安全对库存这种敏感数据更可靠开发速度并不比脚本语言慢多少第二Vue Element Plus这类组件库把表格、表单、弹窗、分页全包了前端开发量比原生JS减少一半以上第三招人或接手都容易后续维护成本低给客户交接时不用解释“为什么用冷门框架”。技术栈类型安全前后端协作部署复杂度典型场景Spring Boot Vue强清晰后端出接口文档中企业级Web开发、中小型管理系统PHPLaravel弱前后端可混写早期快低外包项目、快速原型Node.js Express中前端可全栈低前端团队主导的小工具Django React较强清晰中Python团队、数据类系统如果做的是业务复杂的进销存我会坚持这个组合。如果只是几十个表单页面、没有库存强一致需求、团队全是前端那Node.js会更顺手。选型的核心判断不是“哪个语言高级”而是“出问题后谁能最快定位并修好”。2.2 数据库设计商品、库存、流水三张核心表的DDL仓库系统表数量不多但每一张都要想清楚“谁来写、谁来读、数据多久留”。最基础的一张商品表保存SKU、名称、规格等静态信息一张库存表保存当前实时数量一张流水表记录每一次数量变化。很多初学方案把“当前库存”直接存在商品表里还省了流水表看起来简单实际一上线就废——因为你永远无法回答“这批货从哪来、账为什么对不上”。以下是我常用的基础DDL三张表是底线CREATE TABLE product ( id BIGINT AUTO_INCREMENT PRIMARY KEY, sku_code VARCHAR(64) NOT NULL UNIQUE COMMENT SKU编码, name VARCHAR(128) NOT NULL COMMENT 商品名称, spec VARCHAR(64) DEFAULT COMMENT 规格型号, unit VARCHAR(16) DEFAULT 件 COMMENT 计量单位, category_id BIGINT DEFAULT 0 COMMENT 分类ID, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; CREATE TABLE stock ( id BIGINT AUTO_INCREMENT PRIMARY KEY, product_id BIGINT NOT NULL COMMENT 商品ID, warehouse_id BIGINT NOT NULL COMMENT 仓库ID, quantity INT NOT NULL DEFAULT 0 COMMENT 当前库存数量, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号防并发覆盖, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_product_warehouse (product_id, warehouse_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存表; CREATE TABLE stock_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, product_id BIGINT NOT NULL COMMENT 商品ID, warehouse_id BIGINT NOT NULL COMMENT 仓库ID, change_type TINYINT NOT NULL COMMENT 1入库 2出库 3盘点调整, change_quantity INT NOT NULL COMMENT 正数入库负数出库, before_quantity INT NOT NULL COMMENT 变更前数量, after_quantity INT NOT NULL COMMENT 变更后数量, operator_id BIGINT NOT NULL COMMENT 操作人ID, remark VARCHAR(255) DEFAULT COMMENT 备注, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_product_time (product_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存流水表;逻辑说明stock表用UNIQUE KEY uk_product_warehouse保证同一商品在同一个仓库只有一条库存记录这是避免脏数据的第一道闸。quantity用INT而不是DECIMAL因为按“件”管理的仓库用整数最安全避免浮点精度问题如果你的仓库按重量管理改成DECIMAL(12,3)即可但流水表里的口径必须一致。stock_record里的before_quantity和after_quantity是关键——这条流水就是日后对账的所有依据没有它库存表只是一个被反复覆盖的临时变量。2.3 权限模型手写一个够用的RBAC而不是引入重框架权限这块很多毕业设计和外包项目都走了极端要么完全不设登录进去所有人能删库存要么引入Shiro、Spring Security全套配置还没写业务先被过滤器链搞晕。仓库系统的权限实际只需要回答三个问题谁能登录、能看到哪些菜单、能操作哪些按钮。我一般手写一套轻量RBAC五张表足够。表名作用关键字段user用户表id, username, password_hash, status, created_atrole角色表id, role_code, role_name, remarkmenu菜单/权限表id, parent_id, title, path, perm_codeuser_role用户与角色关联id, user_id, role_idrole_menu角色与菜单关联id, role_id, menu_id实现逻辑是用户登录成功后一次查询user - user_role - role_menu - menu拿到该用户可见的菜单树存进Redis或内存缓存后端接口上用AOP做一个RequirePermission(stock:edit)注解在方法执行前查一下当前用户的perm_code集合。权限模型不需要做到Spring Security那种过滤器链级别但两点基础不能省密码存BCrypt哈希而不是明文登出后token要立刻失效。注意不要给所有角色配同一个菜单树至少把“管理员”和“仓管员”分开——前者能看账本、调库存后者只能做入库出库操作。这一步做晚了后面每个接口都要改权限验证非常痛苦。3. 后端落地库存扣减怎么做到不超卖事务与乐观锁的代码方案3.1 先复现翻车场景两条并发请求怎么把库存扣成负数做库存扣减最典型的错误是“读出数量、内存计算、写回结果”。下面的代码就是翻车现场// 有问题的扣减逻辑先查再减再更新 Stock stock stockMapper.selectByProductAndWarehouse(productId, warehouseId); if (stock.getQuantity() deductQuantity) { throw new BusinessException(库存不足); } int newQty stock.getQuantity() - deductQuantity; stock.setQuantity(newQty); stockMapper.updateById(stock); // 第二步到来时这里的更新会覆盖第一步的结果逻辑说明两个线程同时查询同一商品库存都是10都判断库存充足然后各自updateById把库存改成8。实际扣了两次库存却只少了2件账面和实物从此对不上。更危险的是如果扣减数量是12两个线程同时读到10都判定“不足”而报错好像没问题但一旦有人用到了setQuantity这类直接覆盖字段的方法负库存随时可能出现。参数说明这里的deductQuantity是调用方传进来的扣减数量永远应该转成负数记入流水。真正把并发问题挡在门外靠的不是Java代码里的if判断而是数据库层面的原子更新——下一节给出正确写法。3.2 乐观锁扣减一条UPDATE把并发问题挡在SQL层正确的扣减思路是把“判断库存够不够”和“扣减数量”放进同一条UPDATE语句里让数据库保证原子性。同时用version字段做乐观锁防止ABA问题。Service层代码如下Transactional(rollbackFor Exception.class) public void deductStock(Long productId, Long warehouseId, int deductQuantity, Long operatorId, String remark) { Stock stock stockMapper.selectByProductAndWarehouse(productId, warehouseId); if (stock null) { throw new BusinessException(库存记录不存在请先初始化库存); } int affected stockMapper.deductWithVersion(stock.getId(), productId, warehouseId, deductQuantity, stock.getVersion()); if (affected 0) { throw new BusinessException(库存不足或数据已被其他操作修改请刷新后重试); } Stock latest stockMapper.selectById(stock.getId()); stockRecordMapper.insert(buildRecord(productId, warehouseId, 2, deductQuantity, stock.getQuantity(), latest.getQuantity(), operatorId, remark)); }配套的Mapper XMLupdate iddeductWithVersion UPDATE stock SET quantity quantity - #{deductQuantity}, version version 1 WHERE id #{id} AND quantity #{deductQuantity} AND version #{version} /update逻辑说明deductWithVersion这条UPDATE把“库存判断”与“数量扣减”合并成一个原子操作数据库的行锁保证同一时刻只有一条事务能执行成功。quantity #{deductQuantity}让库存不足时更新0行version #{version}让版本不匹配时更新0行两次并发只有一个能拿到影响行数1另一个抛业务异常回滚事务。流水记录在同一个事务里写入保证主数据与流水永远不会不一致。参数说明version字段非常重要每次成功更新都会加1下一次操作必须用最新版本号否则就说明有别人改过数据这时不能让后写者覆盖先写者的结果。这种乐观锁方案特别适合仓库系统这种“冲突不频繁但一旦冲突必须明示”的场景如果要处理超高频秒杀才需要考虑Redis分布式锁或悲观锁仓库管理系统用乐观锁扛几十并发已经足够。3.3 入库、盘点与流水所有库存变更都走同一条路径出入库和盘点调整不应该各自写一套逻辑而是收敛到一个统一的changeStock方法。入库就是正数变更出库就是负数变更盘点就是把实际数量强制校正成某个值。统一路径的好处是流水表的写入规则只有一处后续做日报、对账、审计都只查一张表。public void changeStock(Long productId, Long warehouseId, int changeQuantity, int changeType, Long operatorId, String remark) { Stock stock stockMapper.selectByProductAndWarehouse(productId, warehouseId); if (stock null) { // 盘点或入库时如果还没有库存记录先初始化一条 stockMapper.initStock(productId, warehouseId, 0); stock stockMapper.selectByProductAndWarehouse(productId, warehouseId); } int affected stockMapper.changeWithVersion(stock.getId(), changeQuantity, stock.getVersion()); if (affected 0) { throw new BusinessException(操作冲突请刷新后重试); } Stock latest stockMapper.selectById(stock.getId()); stockRecordMapper.insert(buildRecord(productId, warehouseId, changeType, changeQuantity, stock.getQuantity(), latest.getQuantity(), operatorId, remark)); }逻辑说明changeType把语义和数量符号解耦——入库时传入1changeQuantity是正数出库时传入2changeQuantity是负数盘点调整是3数量可正可负。这样流水表里只需要按change_type聚合就能知道入库总量和出库总量不需要从正负数反推类型。这里有一个很多人踩过的坑盘点调整不是“按盘点数与账面数的差额去改”而是直接把quantity改成实际盘点数。差值的计算逻辑放在业务层流水表记的是“从100改成86调整-14”这样审计时能看到每一次调整的来龙去脉而不只是看到最终数字。3.4 报表统计用SQL聚合代替循环累加仓库系统后面必然要接昨天晚上入库多少、今天出库多少、本周哪些商品动销最快。新手常见的做法是把流水全部查出来在Java里for循环累加数据量一到几万条就明显变卡。正确做法是把聚合下推到数据库。SELECT DATE(created_at) AS stat_day, SUM(CASE WHEN change_type 1 THEN change_quantity ELSE 0 END) AS inbound_total, SUM(CASE WHEN change_type 2 THEN ABS(change_quantity) ELSE 0 END) AS outbound_total FROM stock_record WHERE product_id #{productId} AND created_at #{startDate} AND created_at #{endDate} GROUP BY DATE(created_at) ORDER BY stat_day;逻辑说明CASE WHEN把入库和出库的数值分开累加ABS把出库的负数转成正数便于图表展示。created_at #{endDate}用半开区间避免把2025-06-30 00:00:00之后的数据漏掉或重复计入。参数说明startDate和endDate在前端传过来时一般是2025-06-01和2025-06-30这种字符串后端务必要解析成日期时间再传给SQL不要直接拼字符串否则会触发隐式转换导致索引失效。如果报表响应超过500毫秒优先给stock_record加一个(product_id, created_at)联合索引比在数据库里调什么参数都管用。4. 前端落地从Vue登录到仪表盘Web页面怎么真正点起来4.1 项目骨架与路由设计Vue3 Vite初始化Web前端我用Vue3 Vite Element Plus起步不用Vue CLI因为Vite冷启动快太多而且对组件热更新的支持更稳。仓库管理系统的前端页面不复杂通常是登录页、仪表盘、库存列表、入库单、出库单、流水记录、系统设置这几块路由设计要一次性把“登录页和主布局分离”这件事做对// src/router/index.js import { createRouter, createWebHistory } from vue-router; const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: () import(/views/Layout.vue), redirect: /dashboard, children: [ { path: dashboard, component: () import(/views/Dashboard.vue) }, { path: stock, component: () import(/views/Stock.vue) }, { path: stock-record, component: () import(/views/StockRecord.vue) } ] } ]; const router createRouter({ history: createWebHistory(), routes }); // 全局守卫未登录跳转登录页 router.beforeEach((to) { const token localStorage.getItem(token); if (to.path ! /login !token) { return /login; } }); export default router;逻辑说明Layout.vue是登录后的主框架放侧边菜单和顶栏子页面通过children嵌套进去。createWebHistory使用HTML5的History模式URL里没有#看起来更专业但后面需要Nginx配try_files支持前端路由刷新否则会404。参数说明全局守卫里我只判断了token是否存在实际项目还要校验token是否过期。更稳的做法是在axios拦截器里遇到401时清掉本地token并跳转登录页下一节会讲到。4.2 axios封装统一处理登录失效与后端错误码如果每个页面各自写fetch后端返回错码、token过期、网络超时这些逻辑就会散落得到处都是。我把axios实例抽成一个独立模块所有页面共用同一套拦截逻辑// src/api/request.js import axios from axios; import { ElMessage } from element-plus; const service axios.create({ baseURL: /api, timeout: 10000 }); // 响应拦截器统一处理业务码 service.interceptors.response.use( (response) { const data response.data; if (data.code ! 0) { if (data.code 401) { localStorage.removeItem(token); window.location.href /login; } else { ElMessage.error(data.message || 请求失败); } return Promise.reject(new Error(data.message || Business Error)); } return data.data; }, (error) { // 网络错误、超时 ElMessage.error(error.message || 网络异常); return Promise.reject(error); } ); export default service;逻辑说明后端统一返回{ code, message, data }结构code0表示成功非0则弹错误提示。401单独处理清掉本地token并跳转登录避免用户手动去删localStorage。参数说明baseURL: /api是前后端联调的约定开发环境通过Vite的proxy把/api代理到后端端口生产环境由Nginx反向代理到后端服务这样前端代码里不会写死后端地址部署时也少一件需要改的事。timeout: 10000对普通查询够用如果报表接口要跑大查询建议对特定接口单独放宽超时时间不要全局一刀切。4.3 库存查询页面Element Plus表格与API模块的完整对接库存列表页面是仓库系统最常用的页面它必须支持分页、关键字搜索、按仓库筛选。页面组件不要直接写axios调用而是先把API按模块抽出去// src/api/stock.js import request from ./request; export function getStockPage(params) { return request({ url: /stock/page, method: get, params }); } export function deductStock(data) { return request({ url: /stock/deduct, method: post, data }); }页面组件里这样绑定script setup import { ref, onMounted } from vue; import { getStockPage } from /api/stock; const list ref([]); const total ref(0); const page ref(1); const pageSize ref(20); const keyword ref(); const loading ref(false); const loadData async () { loading.value true; try { const data await getStockPage({ page: page.value, pageSize: pageSize.value, keyword: keyword.value }); list.value data.records; total.value data.total; } finally { loading.value false; } }; onMounted(loadData); /script template div el-input v-modelkeyword placeholder输入SKU或名称搜索 clearable stylewidth: 260px keyup.enterpage 1; loadData() / el-table :datalist v-loadingloading border stripe el-table-column propskuCode labelSKU width140 / el-table-column propname label商品名称 min-width180 / el-table-column propquantity label当前库存 width100 / el-table-column propwarehouseName label仓库 width120 / el-table-column propupdatedAt label最后更新 width180 / /el-table el-pagination v-model:current-pagepage v-model:page-sizepageSize :totaltotal layouttotal, prev, pager, next current-changeloadData / /div /template逻辑说明这个页面只依赖getStockPage这一个API模块任何后端接口调整只改src/api/stock.js不用每个页面去翻。finally里关loading保证即使接口报错loading也会结束防止按钮一直转。参数说明分页参数page和pageSize后端需要做默认值处理。前端点击搜索时把page重置为1否则用户在第5页搜索会得到空列表这是一个很常见但又很难察觉的交互问题。表格里显示的数量字段一定要设置宽度或使用min-width不然列宽会被长单词挤乱。4.4 仪表盘图表用ECharts展示出入库趋势仪表盘用来回答“本周哪些商品动销最快”“最近几天入库出库趋势如何”。ECharts在后台管理系统里仍是首选封装一个带resize监听的组件比每次裸写init和destroy要靠谱// src/utils/chart.js import * as echarts from echarts; export function initTrendChart(el, days, inboundData, outboundData) { const chart echarts.init(el); chart.setOption({ tooltip: { trigger: axis }, legend: { data: [入库, 出库] }, xAxis: { type: category, data: days }, yAxis: { type: value }, series: [ { name: 入库, type: line, smooth: true, data: inboundData }, { name: 出库, type: line, smooth: true, data: outboundData } ] }); return chart; }逻辑说明setOption是增量更新不是全量覆盖。如果周期内切换日期范围直接拿新的数据再调用一次setOption即可不需要销毁重绘。组件卸载时记得调用chart.dispose()否则多页签反复切换会造成内存泄漏页面越用越卡。参数说明days是日期数组inboundData和outboundData是数量数组三个数组的长度必须一致否则ECharts会默认补空位产生看起来像“缺一天数据”的错误折线。后端返回的报表结构建议直接设计成{ days: [], inbound: [], outbound: [] }前端少做一步转换也就少一个出错的地方。5. 避坑指南从并发超卖到中文乱码5条真实的踩坑记录这一章写的都是我在类似项目里真实遇到过的坑很多不是搜文档能搜出来的靠的是上线后一遍遍对账和对日志的经验。每条按“现象 → 原因 → 解决”来梳理方便你照着排查。5.1 并发扣减导致库存变负数现象系统上线后第一次促销仓库同时接到多笔出库单压测时发现库存数量变成了负数。数据库里没有唯一约束拦住这个结果业务上却已经发了货。原因代码里“判断库存足够”和“扣减库存”分成两步执行两条并发事务同时读到旧数量都在Java层判定为“够”然后先后写回后写的覆盖了先写的。解决按3.2节的方式改成单条UPDATE让数据库用行锁完成“检查扣减”。同时给stock.quantity加一个CHECK (quantity 0)约束设置默认值是0。这个约束是最后一道保险即使某个接口写漏了条件也会直接报SQL异常而不是静默写入负库存。5.2 前端时间显示和数据库差了8小时现象库存更新后在页面上看时间比本地时间少了8个小时。凌晨做的操作列表里显示的是前一天下午的时间排查日志时非常容易看错顺序。原因后端Jackson序列化Date类型时默认使用GMT时区而数据库连接串里的serverTimezone有时写的是UTC或没写导致多套时区叠加。解决在application.yml中统一时区和日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai datasource: url: jdbc:mysql://localhost:3306/warehouse?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8这样从数据库到接口再到前端页面时间口径全部一致。前端拿到字符串后不要再做new Date(value)去转否则可能再次引入本地时区偏移。5.3 CSV或Excel导出中文乱码现象导出库存报表用记事本打开正常用Excel打开全是乱码反过来用Excel编辑后导入系统中文全部变成了问号。原因CSV文件按UTF-8编码保存但Windows版Excel默认按ANSIGBK去解析所以中文乱码。导入时系统按UTF-8读取Excel保存的GBK文件自然也是乱码。解决导出CSV时在文件开头写入BOM标记Excel会根据BOM自动切成UTF-8解析byte[] BOM new byte[] { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF }; outputStream.write(BOM);导入侧则固定要求模板文件必须是UTF-8编码并在上传后校验文件头。系统内部数据库、应用、接口统一UTF-8这个基调不要破坏。5.4 系统跑几天后突然变慢像假死现象系统刚上线时响应很快跑了三四天后页面开始转圈重启一下又好了再跑几天又复发。日志里出现数据库连接超时或获取连接池等待超时。原因数据库连接池默认最大连接数太小某些慢查询占着连接不释放同时Tomcat临时目录和日志文件越积越多磁盘空间被占满后应用连写日志都卡住。解决把连接池调大并加慢查询日志spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000同时给MySQL打开慢查询日志找出耗时超过200毫秒的SQL。最容易藏雷的是报表查询没有覆盖索引和一个事务里执行了多次外部调用。部署目录里的日志文件要配logback按天滚动并设置保留天数不然半年后磁盘满了你自己都找不到原因。5.5 Web接口被扫描出SQL注入现象给客户做安全测试时扫描器在库存查询接口上报了SQL注入漏洞定位到MyBatis XML里某个条件用了${}拼接字符串。原因${}是直接字符串替换不会预编译。很多人只在动态排序字段上用它觉得“排序字段没什么风险”但扫描器会尝试把order by后的内容换成子查询一旦后端没做限制就会被拖出数据。解决能用#{}就绝不用${}。动态排序字段用白名单映射而不是直接拼接用户传参String sortColumn switch (sortField) { case quantity - quantity; case updatedAt - updated_at; default - id; };把用户传入的sortField映射成固定的数据库字段名再拼进SQL这样即使用户传quantity; DROP TABLE也只会落到default分支。这条纪律在项目初期就要立下来等接口多了再回头改就是大工程。6. 部署与进阶jar包Nginx上线加一个库存验证和打印技巧6.1 Linux云服务器部署打包、反向代理与静态资源缓存本地开发通过Vite代理转发生产环境还是老老实实 jar 包加 Nginx。后端打成一个可执行jar前端构建后的静态文件交给Nginx托管cd warehouse-server mvn clean package -DskipTests scp target/warehouse-server.jar rootyour-server:/opt/app/ ssh rootyour-server cd /opt/app nohup java -jar warehouse-server.jar --spring.profiles.activeprod app.log 21 前端在本地构建后把dist目录上传到服务器Nginx配置核心是“静态资源交给文件系统API转发到后端”server { listen 80; server_name your-domain.com; location / { alias /opt/web/dist/; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }前端用的createWebHistory配上try_files $uri $uri/ /index.html刷新深层路由才不会404。修改前端静态文件后如果页面没变多半是Nginx缓存了旧资源给静态location配置合理的expires或者部署时在文件后面加版本参数就能避免用户看到旧页面。注意nohup启动的进程在服务器重启后不会自动拉起。生产环境建议用systemd管理服务配置Restartalways否则一次宕机后系统就再也起不来了。6.2 库存验证与PDF打印上线后每天要对账打印不能靠截图系统上线不等于交付完成真正见功夫的是数据验证和日常使用细节。我最推荐的对账方式是每天跑一遍“账实核对”SQL把stock表里的当前数量与stock_record流水推算出来的数量做差值这个差值应该永远为0。SELECT s.product_id, s.quantity AS stock_qty, s.quantity - ( SELECT IFNULL(SUM(change_quantity), 0) FROM stock_record r WHERE r.product_id s.product_id ) AS diff_qty FROM stock s HAVING diff_qty ! 0;这个SQL把随时可能发生的账实不一致暴露在每天一上班就能看到的位置。如果diff_qty不为0优先查当天所有类型为3的盘点记录看是不是人为调了库存却没写备注。打印这块很多项目做成“截图留档”打印出来模糊且没有签字栏。Web页面PDF打印最常见也最稳的做法是浏览器打印给打印区域加上编号和签字栏function printStockOrder(orderId) { const node document.getElementById(print-area); document.title 入库单_${orderId}; node.classList.add(printing); window.print(); node.classList.remove(printing); document.title 仓库管理系统; }配合CSS里media print隐藏按钮和侧边栏只保留单据主体打印质量远高于截图。用户要PDF时直接在浏览器打印对话框里选择“另存为PDF”不需要后端再去生成PDF文件省掉一套依赖。我早期自己做仓库系统最深的教训是只把CRUD跑通就上了线结果第一次并发发货就把库存扣成负数半夜爬起来手工改库。后来涉及库存的方法都按“事务版本号流水”三条纪律来写上线前跑一遍对账脚本再没翻过车。这套方案从选型到部署都是低成本、可复现的做法你照着做一遍至少能避开我走过的那些弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表