毕业设计季总有那么一批题目反复出现,“基于SpringBoot+Vue的管理系统”就是其中的绝对主力。看到这个标题——社区防疫物资申报系统管理系统设计与实现,可能不少人觉得又是老套路:一套前端、一套后端、几张表、几个页面。但真上手做过的朋友都清楚,这类项目的价值恰恰在于把它做“闭环”:物资怎么入库出库?申报单怎么从提交一路走到发放?库存怎么保证不错乱?三个角色怎么在系统里各司其职?下面我就按我当时做这个模拟项目X的实际思路,从零到一拆一遍,把设计理由、核心代码逻辑、踩过的坑一并讲清楚。这篇东西适合正在做类似选题的同学,也适合想用SpringBoot+Vue全家桶快速搭一套真实管理系统的开发者。
1. 项目到底在解决什么问题
很多人在动手之前容易犯一个毛病:一上来就建表、写接口,做了两星期回头发现流程对不上,业务逻辑支离破碎。所以我强烈建议,第一步先别碰代码,把业务痛点写出来。
1.1 业务痛点与系统定位
社区物资管理如果靠手工,一般会出现这么几个典型问题:
- 居民想申领物资,不知道还剩多少、该找谁、填什么表,居委会线下登记效率极低。
- 物资出入库全靠一张Excel,今天进了多少、发了多少、库存余量是多少,几个人填出几个版本。
- 审批进度不透明,居民交完申请就石沉大海,只能反复打电话问。
- 发放时缺少记录,事后想对账,连谁领了什么、什么时候领的都说不清。
这套系统的定位就是给“居民申报、管理员审核、系统化管理物资”提供一个线上闭环。系统不需要做得很炫,但核心要解决三件事:物资台账清晰、申报流程可控、角色权限分明。站在技术角度,它就是一个标准的全栈CRUD项目,但核心难点集中在“申报单状态流转”和“库存并发扣减”上,这也是实际开发中最值得花时间的部分。
1.2 功能模块的整体划分
我画功能模块的时候没有按传统“用户管理、角色管理、菜单管理”那种大而全的思路走,而是先列业务主链路,再补齐底座功能。
主链路是:
- 管理员维护物资信息(名称、分类、库存总量、预警阈值)。
- 居民登录后浏览物资并提交申报单,填入申领数量。
- 管理员进入审核列表,通过或驳回申报单。
- 通过后进入发放流程,实际发放并扣除库存。
- 居民在我的申报中随时查看进度。
在这个链路之外,再补上系统管理模块:用户管理、角色权限、公告通知、操作日志。整个项目我最终拆成了三个端:
- 居民端:注册登录、物资查询、申报提交、进度查看、确认领取、公告查看。
- 管理员端:物资管理、分类管理、申报审核、发放登记、库存预警、统计看板、公告管理。
- 系统管理员:用户管理、角色分配、菜单权限。
这样的划分有助于后面设计数据库和接口时保持边界清晰。居民只看到自己的数据,管理员看到所有申报单,系统管理员只管权限底座的维护。
2. 技术选型:为什么是SpringBoot+Vue+MySQL+MyBatis
现在的技术选型五花八门,有的人上来就套微服务,有的人非要搞Redis缓存。但对于这类管理系统,技术栈越经典越可控,也越容易讲清楚设计理由。
2.1 前后端分离的选型理由
后端用SpringBoot 2.7 + MyBatis + MySQL 5.7,前端用Vue 2 + Element UI + Axios + Vue Router。选这套组合的原因很现实:
- SpringBoot简化了Spring的配置复杂度,内嵌Tomcat,打一个jar包就能跑,适合快速交付。
- MyBatis相比JPA/Hibernate,SQL可控性强,尤其是库存扣减这种对SQL敏感的操作,手写SQL心里踏实。
- MySQL 5.7在锁机制、事务支持上成熟稳定,对单机应用足够。
- Vue 2 + Element UI是国内管理系统的事实标准,组件齐全,文档多,后期好维护。
有人可能会问为什么不用Vue 3?坦白讲,如果你是从零开始且参考资料有限,Vue 2的Element UI生态更成熟,坑少。Vue 3组合式API固然好,但配套组件库和社区样例参差不齐,做毕设或快速交付反而容易卡壳。
前端工程结构我按标准Vue CLI脚手架划分:src/api统一放接口调用,src/router放路由配置,src/store存用户登录态,src/views按角色目录组织页面。后端则按经典分层:controller接收请求、service处理业务、mapper操作数据库,entity映射表结构,common放统一返回结果、异常处理、工具类。
2.2 权限模型:RBAC三张基础表
权限如果不做设计,最典型的做法是给用户表加一个role字段,比如0表示居民、1表示管理员,然后前端根据这个值判断显示哪些按钮。这种方案在单一管理员角色的演示项目里勉强能跑,但一旦出现多个管理员各管一摊、菜单权限要动态变化的情况,就会改得很难看。
我采用的是经典RBAC模型:用户表、角色表、菜单表,外加用户角色关联表和角色菜单关联表。核心表结构简化如下:
-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, real_name VARCHAR(30), phone VARCHAR(20), status TINYINT DEFAULT 1 ); -- 角色表 CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(50), role_code VARCHAR(50), remark VARCHAR(255) ); -- 菜单表 CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT, menu_name VARCHAR(50), path VARCHAR(100), component VARCHAR(100), icon VARCHAR(50), sort INT DEFAULT 0, visible TINYINT DEFAULT 1 ); -- 用户-角色关联 CREATE TABLE sys_user_role ( user_id BIGINT, role_id BIGINT ); -- 角色-菜单关联 CREATE TABLE sys_role_menu ( role_id BIGINT, menu_id BIGINT );这种设计最大的好处是:以后想加一个“仓库管理员”角色,只需要在角色表插入一条数据,再绑定菜单权限,完全不用改动现有代码逻辑。菜单遍历时,后端根据当前用户角色查询可访问菜单树,返回给前端动态渲染。这就是“基于角色的访问控制”在管理系统里的落地方式。
3. 数据库设计:一切围绕着申报单的状态流转
数据库设计是整个项目的地基。我见过太多人把设计重点放在字段够不够多,结果表的关联关系、状态流转、并发控制反而考虑得很草率。这套系统的核心表我最终定了七张:用户角色相关表、物资表、物资分类表、申报单主表、申报单明细表、出入库记录表、公告表。
3.1 核心表结构与字段说明
物资表重点关注库存字段的设计:
CREATE TABLE material ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT, name VARCHAR(100) NOT NULL, spec VARCHAR(100) COMMENT '规格', total_stock INT DEFAULT 0 COMMENT '总库存', unit VARCHAR(20) COMMENT '单位,如个/箱/瓶', warning_line INT DEFAULT 10 COMMENT '库存预警线', version INT DEFAULT 0 COMMENT '乐观锁版本号', status TINYINT DEFAULT 1, create_time DATETIME );total_stock直接存当前可用库存即可,不必拆“入库量”和“出库量”两个字段,因为每次出入库还要单独记流水。真正需要单独建表记录的是出入库流水,这既是审计依据,也是日常对账的数据源:
CREATE TABLE stock_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, material_id BIGINT, change_type TINYINT COMMENT '1入库 2出库 3盘点调整', change_num INT, before_stock INT, after_stock INT, relation_no VARCHAR(40) COMMENT '关联单号', create_by BIGINT, create_time DATETIME );申报单主表是这套系统里最值得设计的表:
CREATE TABLE apply_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(40) UNIQUE NOT NULL, user_id BIGINT COMMENT '申报人', status TINYINT DEFAULT 0 COMMENT '0待审核 1已通过 2已发放 3已驳回 4已取消', audit_user_id BIGINT COMMENT '审核人', audit_time DATETIME, audit_remark VARCHAR(255), receive_time DATETIME COMMENT '发放时间', total_num INT COMMENT '申报总数量', remark VARCHAR(255), create_time DATETIME ); CREATE TABLE apply_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT, material_id BIGINT, apply_num INT COMMENT '申报数量', real_num INT COMMENT '实际发放数量' );这里有两个设计细节值得说:
第一,order_no为什么要单独做一个业务单号?因为主键id是自增的,直接暴露给用户既容易猜测业务量,也不利于线下单据对照。我用年月日时分秒加上随机数生成单号,形如SQ20250322153012345,查询打印都方便。
第二,状态字段为什么用TINYINT而不是直接存字符串?存字符串“待审核”虽然直观,但后续判断条件都要靠字符串匹配,容易写错。用数字枚举配合代码里的常量类,判断时语义明确,扩展状态也方便。
3.2 库存扣减与并发控制
这个模块是整套系统最容易出Bug的地方,也是我在开发时花时间最多的地方。想象一个场景:两类物资分别剩8件和10件,居民A提交申请要5件,居民B同时提交申请要6件。如果代码先查库存、判断够不够、再执行更新,当两个请求同时走到的时候,数据库里到底扣了多少?极容易出现超发。
解决思路其实很简单:不要把“判断库存”和“扣减库存”分成两步,直接在一条SQL里完成“库存足够才更新”的原子操作:
UPDATE material SET total_stock = total_stock - #{num}, version = version + 1 WHERE id = #{materialId} AND total_stock >= #{num}这条SQL利用数据库行锁保证同一时间只有一个事务能更新同一行库存,total_stock >= #{num}则保证库存不足时更新影响行数为0。业务代码里只要判断这个返回值,如果返回0,说明库存不足或数据已被修改,直接抛异常回滚事务。
在此基础上,我还在物资表里加了version字段做乐观锁。虽然上面的扣减SQL已经能防超发,但有些场景比如管理员手动调整库存、盘点修正时,两个操作同时修改同一条物资记录,依旧要靠version字段避免覆盖更新:
UPDATE material SET total_stock = #{newStock}, version = version + 1 WHERE id = #{id} AND version = #{oldVersion}实际开发时,我把扣库存的动作放在事务里,和生成出入库流水、更新申报单状态放在同一个事务方法内。这里补充一个经验:事务不要加在Controller层,而是加在Service层,同时注意事务方法内不要捕获异常后吞掉,否则事务不会回滚。
4. 后端实现:登录鉴权、审批流与库存原子操作
后端部分我挑三个核心环节详细讲:登录鉴权怎么做、申报单状态怎么流转、库存怎么安全扣减。这三块搞定,系统基本就站得住脚了。
4.1 登录鉴权:拦截器加Token
系统管理类的项目,用Shiro或Spring Security做权限虽然专业,但对这个小系统而言引入成本偏高。我用了一种更轻量的实现:登录成功后生成一个Token字符串存到Redis(演示环境也可以用内存Map),前端每次请求把Token放到Header里,后端写一个拦截器统一校验。
核心思路是:
- 用户登录时,校验用户名密码(密码用BCrypt加密存储),成功后生成UUID作为Token。
- Token作为key存进Redis,value存用户ID和角色信息,并设置过期时间(我设的是12小时)。
- 后端写预检请求方法:从请求头取Token,取不到或校验失败直接返回401。
- 放行前,把当前用户信息存入ThreadLocal,方便Service层获取操作人。
代码结构大致如下:
@Component public class AuthInterceptor implements HandlerInterceptor { private StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行登录、注册等无需鉴权的接口 if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; PassToken passToken = hm.getMethodAnnotation(PassToken.class); if (passToken != null) { return true; } } String token = request.getHeader("token"); String userId = redisTemplate.opsForValue().get("token:" + token); if (StringUtils.isEmpty(userId)) { // 将错误响应写入outputstream返回401 return false; } // 把用户信息放入ThreadLocal UserContext.setUserId(Long.valueOf(userId)); return true; } }这里有个很容易踩的坑:拦截器会把静态资源请求也拦下来,如果前端部署时直接把dist目录放在后端资源路径下,就会出现登录页面打不开的情况。我的解决方案是前端完全独立部署,通过Nginx访问后端API,拦截器只处理/api/**路径。
4.2 申报单状态流转实现
申报单从提交到发放,状态不是随意跳转的,必须受流程约束。我在设计阶段就把状态流转图画清楚了:
- 待审核:用户提交申报后进入的状态。
- 已通过 / 已驳回:管理员审核后的结果。
- 已发放:审核通过后,管理员登记发放完成。
- 已取消:用户在待审核状态下主动取消申报。
对应的Service方法我拆得非常明确:submitOrder、auditOrder、issueOrder、cancelOrder。每个方法第一件事就是校验当前状态是否允许执行操作,不允许直接抛业务异常。这样做的好处是,后期无论前端怎么调用,后端永远能保证状态的合法性。
审核通过这个动作尤其要注意细节。管理员可能在审核时修改发放数量,比如居民申报5个口罩,但当前库存只有3个,管理员要把实际发放数改成3。因此在明细表里,apply_num和real_num两个字段分开存,发放时以real_num为准来扣库存。
发放时的事务逻辑是这样的:
@Transactional(rollbackFor = Exception.class) public void issueOrder(Long orderId, Long auditUserId, List<OrderItemRealNum> realNumList) { ApplyOrder order = applyOrderMapper.selectById(orderId); if (order == null || order.getStatus() != 1) { throw new BusinessException("当前状态不可发放"); } // 逐条扣减库存 for (OrderItemRealNum item : realNumList) { int rows = materialMapper.reduceStock(item.getMaterialId(), item.getRealNum()); if (rows == 0) { throw new BusinessException("物资库存不足:" + item.getMaterialId()); } // 写出入库流水 stockRecordMapper.insert(...); // 更新明细表的实际发放数量 } // 更新申报单状态为“已发放” applyOrderMapper.updateStatus(orderId, 2, auditUserId, new Date()); }注意事项:这个issueOrder方法一定要加@Transactional(rollbackFor = Exception.class)。默认情况下,Spring事务只对RuntimeException回滚,如果业务代码里抛的是Exception子类而没加rollbackFor,库存扣了但申报单状态没更新,前面扣的库存不会自动恢复,排查起来非常痛苦。
4.3 库存扣减的原子性保障
前面在数据库设计部分讲了扣减SQL的思路,这里把完整实践再梳理一遍。库存扣减其实有两种做法:
派发式扣减:审核通过时就扣库存。好处是库存占用及时,不会出现审核通过但实际发放时没货的尴尬。坏处是如果居民最终没来领取,需要重新回补库存,逻辑上多一步。
领取式扣减:发放时才扣库存。好处是库存变动和实际发放一致,坏处是审核通过了很多单,实际来领的时候发现库存早就被别人领完了。
我做的是领取式扣减,并在审核时做一次“预占检查”,即管理员审核通过时先校验库存充足,但要留出实际发放的变更空间。这样设计综合考虑了实物发放的灵活性:管理员在发放现场可以根据剩余情况调整实发数量。
还有一个容易忽略的点:mybatis的update语句默认返回值是受影响的行数,但XML文件里如果配置了useGeneratedKeys,某些场景下会影响返回值判断。我在Mapper接口里统一返回int来接收更新行数,不依赖MyBatis自动提交,这样在Service层控制事务才是最稳妥的。
5. 前端实现:Vue加Element UI的页面组织
前端部分我按“基础设施”和“业务页面”两块来建设。基础设施包括路由、登录态、请求封装;业务页面则围绕三条主链路展开。
5.1 路由、菜单与角色
前端页面文件按模块划分:登录页、居民端页面(物资列表、申报提交、我的申报)、管理员页面(物资管理、审核列表、发放登记、库存看板)、系统管理页面(用户管理、角色管理、公告管理)。
路由设计上分了两种方式:静态路由和动态路由。登录页、注册页是静态路由,系统内所有页面由后端根据登录用户的菜单权限动态返回菜单树,前端再根据这条菜单树动态添加到路由表中。
// 动态添加路由 const addDynamicRoutes = (menus) => { const routes = menus.map(menu => { const componentMap = { 'material/list': () => import('@/views/admin/MaterialList.vue'), 'apply/audit': () => import('@/views/admin/ApplyAudit.vue') }; return { path: menu.path, component: componentMap[menu.component], meta: { title: menu.menuName, icon: menu.icon } }; }); router.addRoutes(routes); };这里需要特别提醒一个坑:由于动态路由是异步加载的,用户在刷新页面时路由表还没建立,直接访问/apply/audit会白屏。解决方法是刷新时先调用/user/info接口获取菜单,再addRoutes,之后调用next({ ...to, replace: true })让路由重新匹配一次。
路由守卫的写法我放在permission.js文件里:未登录跳登录页;已登录但不能访问当前路径时提示无权限,跳回首页。
5.2 核心页面:申报表单、审核列表、库存看板
申报表单页是居民操作最频繁的页面。页面加载时先调物资列表接口,只展示库存大于0且状态正常的物资,每行带一个数字输入框,底部动态汇总申报总数量。提交前做两次校验:总数量必须大于0,单项数量不能超过物资库存。
审核列表页用Tab切分状态:待审核、已通过、已驳回、已发放。每个Tab下各自调对应接口,避免一次性拉全量数据。审核弹窗里把申报明细列成表格,每行允许管理员修改“实际发放数量”,修改时前端做一次乐观提示:如果实发数量大于申报数量会有红色警告。
库存看板页是我觉得最能体现“系统价值”的一个页面。物资卡片按分类展示,库存充足显示正常颜色,库存低于预警线高亮显示“库存不足”,同时配合ECharts做一个库存分类占比的饼图。数据量不大,直接调统计接口一次返回,前端再分组渲染,不需要额外设计复杂的后端接口。
5.3 axios封装与错误处理
axios如果不封装,每个页面写一遍请求逻辑,后期接口路径一改就要全局搜。我统一在src/api/request.js里创建实例:
const request = axios.create({ baseURL: '/api', timeout: 10000 }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.token = token; } return config; }); request.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { this.$message.error(res.msg); return Promise.reject(new Error(res.msg)); } return res; }, error => { if (error.response && error.response.status === 401) { localStorage.clear(); window.location.href = '/login'; } return Promise.reject(error); } );统一封装最大的好处是:后端返回结构永远是{ code, msg, data },前端只关心data,错误处理全部收敛到拦截器里。后期如果接微信小程序,这套结构也能直接复用。
6. 部署与打包:本地能跑只是第一步
很多人在本地idea里跑得风生水起,打包部署却翻车,特别是前后端分离的项目。这一节把部署的完整链路说清楚。
6.1 前后端分离部署
后端打包前先确认几个配置:数据库连接地址改为生产环境地址,端口确定好(我用的8080),然后在项目根目录执行:
mvn clean package -DskipTests打完包后在target目录下生成xxx.jar。启动命令我不建议直接用java -jar后就撒手不管,至少用nohup保持后台运行:
nohup java -jar xxx.jar --spring.profiles.active=prod > app.log 2>&1 &前端打包:
npm run build生成dist目录后,用Nginx统一托管。Nginx配置如下:
server { listen 80; server_name localhost; root /data/community/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 解决history路由刷新404 location / { try_files $uri $uri/ /index.html; } }注意proxy_pass http://127.0.0.1:8080;后面不要加路径。如果加了/,会把/api前缀剥掉;不加则原样转发,后端Controller里定义的就是/api/login之类的完整路径,两边能对上。我当初因为这里少写一个斜杠,折腾了半天接口404。
6.2 实际部署的几个坑
时区问题。MySQL连接串建议写成jdbc:mysql://localhost:3306/community?serverTimezone=Asia/Shanghai&characterEncoding=utf8,否则日期字段差8小时,前端显示时间总是对不上,检查起来很迷惑。
跨域问题。本地联调时前端跑在8080端口或9527端口,后端在8080,必然跨域。我在后端加了一个全局CORS配置类,允许所有来源、所有请求头、所有方法。生产环境走Nginx代理后不存在跨域,但如果直接前后端分开部署到不同域名,这个配置就很有必要了。
刷新404问题。前端路由用的history模式,直接访问http://server/apply/audit时,如果Nginx没有配置try_files那一条,就会返回404。这个前面已提过,配置里务必加上。
文件上传路径问题。如果系统里做了公告图片或头像上传,建议把文件保存在独立的目录,比如/data/upload,而不是项目根路径下。重启时项目路径可能被临时文件覆盖,而且后期用Nginx代理静态文件也更灵活。
7. 实战中遇到的问题与排查记录
开发过程中踩过的坑,往往比顺利跑通的经验更值钱。把几个高频问题整理成速查表,遇到类似情况可以少走弯路。
7.1 高频问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 登录后调用接口返回401 | Token校验失败或已过期 | 检查Token是否放入请求头,确认Redis过期时间配置 |
| 提交申报一直提示库存不足 | SQL条件里库存判断未生效 | 检查扣减SQL是否带上total_stock >= #{num},Mapper返回值是否正确判断 |
| 发放成功后库存没变 | 事务未生效或异常被吞掉 | Service方法加@Transactional(rollbackFor = Exception.class),别在try-catch里吞异常 |
| 审核通过后看不到申报单状态变化 | 更新状态的条件写错,或状态枚举值不一致 | 检查前端传的状态码和后端常量是否对应,数据库里实际值是什么 |
| 前端菜单为空 | 当前用户角色未绑定菜单 | 查看user_role和role_menu表数据,确认角色关联正确 |
| 打包后连不上数据库 | 生产配置未切换 | 检查application-prod.yml里的数据库地址、账号密码是否正确 |
| 上传图片后页面无法显示 | 静态资源映射未配置 | 配置Nginx的静态资源配置或后端addResourceHandlers指向上传目录 |
| 本地运行正常,部署后接口404 | Nginx代理路径前缀不对 | 确认proxy_pass是否带/,后端server.servlet.context-path是否设置 |
排查的时候我习惯遵循一条链路:先看浏览器Network里请求的状态码,再看后端日志中这个请求有没有进来,最后看SQL执行结果。很多问题都是请求根本没到后端,那你从Service层开始排查就是在浪费时间。
7.2 让项目更好用的几点改造思路
如果时间和精力允许,下面几个方向能让这个项目从“能用”变成“好用”:
导出功能。申报单和物资出入库流水都支持导出Excel,管理后台非常看重这个能力。用Apache POI或者EasyExcel都行,后台直接把数据封装成Workbook对象返回前端下载。居民端的个人申报记录也可以导出,方便线下留档。
统计看板。我做了一个简单的统计接口,返回物资分类数量、申报单各状态数量、本周申报趋势。前端用echarts画几张图放在仪表盘页,答辩或项目演示时视觉上比一堆表格直观得多。
日志记录。写一个切面,拦截所有Controller请求,打印请求路径、参数、耗时和操作用户。这个不仅为了排查问题,也能在后期做操作审计时说清楚谁在什么时间改了什么。
消息通知。如果系统里加了公告表,还可以考虑在用户登录后弹窗显示未读公告。这里注意公告要区分“全员可见”和“仅管理员可见”,专员发布公告时可以选择接收对象。
换成MyBatis-Plus。如果受不了写大量重复的Mapper XML,可以把MyBatis替换成MyBatis-Plus,基础CRUD直接继承BaseMapper,只保留自定义SQL写在XML里。代码量能肉眼可见地减少三分之一。
最后的几点体会
这套系统从表和设计到前后端完全跑通,我大概花了两个星期。回头复盘,最大的感悟是:这类管理系统项目,真正比拼的不是代码量,而是业务闭环的完整性。你把“申报-审核-发放-库存”这条链路想得越清楚,表设计就越顺,写代码就越快。相反,如果上来就急着写接口,回头改表和重构逻辑的成本才最耗时间。
另外,做这种系统时请务必养成好习惯:所有Mapper的更新方法都用int接收返回值、所有事务方法都指定rollbackFor、所有后端返回格式统一成code/msg/data、所有页面接口路径都走同一份封装。这些看似不起眼的小事,在项目后期联调和部署时能帮你省下大量精力。
如果你正好在写类似的SpringBoot+Vue管理系统,建议先花一晚上把状态流转、库存扣减、角色权限这三个核心设计想明白再动手。想明白了,剩下的无非是大量重复的增删改查,那反而是最简单的工作。