SpringBoot+Vue医药管理系统:从零搭建到部署的完整毕设指南

SpringBoot+Vue医药管理系统:从零搭建到部署的完整毕设指南 做毕设选管理系统方向的人很多但能把“医药管理系统”做完整、做得像一个能交付的软件而不是“为了答辩凑功能的Demo”其实不多。这个SpringBootVue的医药管理系统平台恰好是市面上比较标准的一套Java Web毕设案例后端用SpringBoot前端用Vue数据库脚本和接口文档都齐了拿到之后能快速跑起来也能根据自己理解改造成答辩加分项。这个项目的核心价值在于它不只是“能增删改查”而是围绕医药行业真实的业务流程来设计的药品基础资料维护、分类管理、供应商管理、采购入库、销售开单、库存预警、临期药品提醒、会员管理再加上后端的用户登录和权限控制。它适合正在准备Java Web相关的毕业设计、课程设计或者想快速上手SpringBootVue前后端分离项目的同学参考。如果你只想找一份能直接抄的代码这篇内容能帮你少走弯路如果你想把项目吃透在答辩时能讲清楚每一个表、每一个接口为什么这么设计这篇更值得看完。我接下来就按完整的项目拆解顺序从功能设计、技术选型、数据库脚本、后端接口、前端联调、部署运行到问题排查把整套项目的思路和实操细节完整过一遍。1. 项目整体设计与功能拆解1.1 医药管理系统到底要管什么很多同学一听到“医药管理系统”第一反应就是药品的增删改查其实这是把业务想简单了。药店或者医院药房的信息化管理核心不只是“记录药品”而是围绕药品的“进销存”三个字展开的——进就是采购入库销就是销售出库存就是库存管理。再加上药品这个特殊商品有批次、有效期、批准文号这些属性系统要比普通的商品管理系统多考虑一层安全合规因素。这套项目里的功能设计基本上是按照真实药房业务流程来的。系统面向两类用户管理员和收银员。管理员负责药品维护、供应商管理、采购入库、用户管理这些比较高权限的操作收银员主要负责销售开单、会员录入、查询库存这类日常操作。权限不一样前端菜单不一样后端接口也要做对应的拦截这种设计在毕业答辩里很加分因为老师问到“你怎么考虑用户权限的”至少能拿出实际的实现方案。整个系统的业务闭环是这样的管理员先维护好药品分类和药品信息录入供应商然后做采购入库库存增加收银员在销售页面根据药品名称或编号检索药品加购、结算、生成销售单库存自动扣减库存低于阈值时系统预警药品临近有效期时也要能提醒。这个闭环跑通了这个项目才叫“管理系统”而不是“管理页面”。1.2 模块划分与角色权限设计从代码层面拆开看这个项目包含以下功能模块系统管理用户登录、退出登录、用户管理、修改密码、角色权限控制。药品管理药品分类管理、药品信息管理商品名、通用名、规格、剂型、生产厂家、批准文号等。库存管理库存查询、库存预警、临期药品列表、库存盘点辅助逻辑。采购管理供应商管理、采购入库单录入、采购记录查询。销售管理销售开单购物车式、销售记录查询、销售明细查看。会员管理会员信息维护、会员价设置、购药记录查看。角色权限这块比较简洁的做法是基于JWT 拦截器或Spring Security来实现。其实毕设项目用Spring Security有时候反而会增加学习成本如果只是为了实现“不同角色看到不同菜单、访问不同接口”用拦截器校验token和角色也完全够用。我见过不少项目的代码里其实没有真正的权限表只是在Controller层写死了“管理员角色才能调用”这种实现虽然简单但答辩时容易暴露问题。如果时间充裕建议把用户表、角色表、用户角色关系表三张表都建上哪怕不用前端动态菜单也能体现设计思路是完整的。2. 技术选型背后的逻辑2.1 为什么是SpringBoot Vue这套组合先直接说结论SpringBoot Vue已经是目前Java Web毕设的事实标准你拿这套组合去答辩老师基本不会在技术选型上挑毛病。原因是这套组合的开发效率和演示效果都比较好。SpringBoot这边内置Tomcat不用单独部署容器就能跑起来自动配置机制把SpringMVC、MyBatis、数据源这些集成工作简化了一大截再加上它和Spring生态天然配合写Service、Controller时不用写一堆XML配置。对毕设来说最重要的是“能跑起来”和“不太折腾”SpringBoot在这方面确实比传统的SSH、SSM框架省心不少。Vue这边核心价值在于前后端分离的开发模式清晰前端只管渲染页面和调用接口后端只管提供数据和业务逻辑。用Vue CLI或Vite创建工程配合Element UI/Element Plus组件库开发后台管理系统的效率非常高——表格、表单、弹窗、分页这些常见的后台组件都是现成的。你不需要自己手写一大堆CSS去拼页面又能做出比JSP页面好看很多的界面。还有一个现实原因这套组合的资料多。无论是你代码里碰到一个bug还是答辩时老师问到一个概念网上随便一搜就是大把的解决方案和讲解对于毕设周期很短的场景来说这个“后援力量”很重要。2.2 后端核心依赖与版本选择后端项目的pom.xml里核心依赖大概有这些dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency这里有一个比较关键的版本坑如果你的SpringBoot版本是2.6.x以上配Swagger的时候要注意springfox的兼容性有点问题启动时会报Failed to start bean documentationPluginsBootstrapper。解决办法有两个一是把SpringBoot版本降到2.5.x二是换成springdoc-openapi这套方案。从实际体验来说springdoc-openapi的配置更省心依赖也更简洁dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-ui/artifactId version1.7.0/version /dependencyMyBatis-Plus选型也是为了省事。单表CRUD不需要手写SQLBaseMapper里已经提供了selectById、selectPage、insert、updateById等方法分页查询用PaginationInterceptor配置一下就能直接拿Page对象条件查询用LambdaQueryWrapper比如根据药品名模糊查询代码就一行不用在XML里写whereif这种标签。对于自己做毕设的人来说这个选择能省下至少一天的编码时间。2.3 前端环境和依赖选择前端工程一般用Vue CLI创建如果你熟悉Vite用Vite也可以但毕设资料大多数还是Vue CLI的写法建议跟主流保持一致。需要装的依赖包括vue-router前端路由管理。axios发送HTTP请求。element-uiVue 2项目或 element-plusVue 3项目。sass如果你需要自定义样式可以一起装上。如果项目是基于Vue 3 Element Plus那么main.js里大概是这样import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(router) app.use(ElementPlus) app.mount(#app)这里有个小提醒Vue 2和Vue 3的API差别很大如果你拿到的是Vue 2写的项目又用Element Plus去适配那基本改不动。建议先看清package.json里的vue版本再做对应调整。这个项目如果用Vue 2对应的UI组件库就是Element UI配的是Vue 2的Options API写法用Vue 3的话配的则是组合式API或者还是选项式API都能兼容。3. SQL脚本设计与数据库初始化3.1 表结构设计这关怎么过数据库是一个管理系统最核心的部分。医药管理系统的表结构不是只建一张“药品表”就能支撑起来的。合理的设计至少包含八到十张表我在这个项目里见到的整理如下表名用途关键字段sys_user系统用户id, username, password, real_name, role, statusdrug_category药品分类id, category_name, parent_id, sort_orderdrug_info药品信息id, drug_code, generic_name, trade_name, spec, unit, manufacturer, approval_number, retail_price, stock_quantity, stock_low_limit, expire_datesupplier供应商id, supplier_name, contact_person, phone, address, statuspurchase_order采购入库单id, order_no, supplier_id, total_amount, create_time, create_bypurchase_order_detail采购入库明细id, order_id, drug_id, purchase_price, quantity, sub_totalsale_order销售单id, order_no, member_id, total_amount, pay_type, create_time, create_bysale_order_detail销售明细id, order_id, drug_id, sale_price, quantity, sub_totalmember会员信息id, member_no, name, phone, level, discountsys_role_permission角色权限关联可选id, role, permission_key药品表是核心表设计时要特别注意几个字段stock_low_limit是库存预警阈值低于这个值就触发低库存预警expire_date是有效期取当前日期在30天内的记录就是临期药品。这两个字段在前端界面上会有对应的“库存预警”和“临期提醒”代码里是查询条件所以数据库设计时一定要留出来。还有一点金额字段建议用DECIMAL(10,2)不要用DOUBLE或者FLOAT因为浮点数在计算金额时存在精度问题比如0.10.2不等于0.3这在财务场景里是不可接受的。销售单和采购单的关联方式是用“主表明细表”的结构主表记录单据总体信息明细表记录每一条药品的进货或销售数据这样既能查出一张订单买了什么也能反查某个药品被哪些订单采购或销售过。3.2 SQL脚本执行与编码避坑指南拿到SQL脚本后第一件事是看数据库版本。MySQL 5.7和8.0在语法上大部分兼容但如果是8.0驱动连接用的是com.mysql.cj.jdbc.DriverURL里要加时区参数如果是5.7用旧的驱动也可以。执行脚本时最容易碰到两个问题中文乱码和脚本执行报错。中文乱码的原因通常是SQL文件本身是UTF-8编码但连接数据库时客户端字符集不对。解决办法是在连接URL上加参数jdbc:mysql://localhost:3306/medicine?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai有人喜欢在URL里加useSSLfalse这是因为本地开发环境没有配置SSL证书连接时MySQL会给出警告。虽然不影响运行但加了这个参数后控制台更干净也能避免在特定MySQL版本下连接失败。执行SQL脚本有几种方式。如果你用的是Navicat直接右键数据库选择“运行SQL文件”选好脚本文件点开始就能执行如果你用命令行则要这样操作mysql -u root -p medicine medicine.sql这条命令的前提是数据库medicine已经创建好。如果没有可以先登录MySQLCREATE DATABASE medicine DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE medicine; SOURCE /path/to/medicine.sql;这里我建议用utf8mb4而不是utf8原因很简单utf8mb4是utf8的超集可以存储emoji和一些生僻字。虽然系统里不一定用得到emoji但从规范角度来说建库用utf8mb4是一个更好的习惯。另外一个常见坑是脚本重复执行。如果脚本里已经包含DROP TABLE语句重复执行没问题但很多脚本没有加DROP第二次执行时会报“Table already exists”。这不算报错但是会打断执行流程。我的习惯是在脚本最前面加上DROP TABLE IF EXISTS这样能保证脚本可以重复执行开发和调试的时候省心很多。4. 后端核心实现与接口设计4.1 后端目录结构与启动流程SpringBoot项目的包结构建议按“controller / service / mapper / entity / common / config”来分包。这个项目也是这么做的src/main/java/com/medicine/ ├── common/ │ ├── Result.java统一返回结果类 │ └── ResultCode.java返回状态码枚举 ├── config/ │ ├── MybatisPlusConfig.java │ ├── WebConfig.java跨域配置 │ ├── SwaggerConfig.java │ └── JwtInterceptor.java ├── controller/ │ ├── AuthController.java │ ├── DrugController.java │ ├── CategoryController.java │ ├── SupplierController.java │ ├── PurchaseController.java │ ├── SaleController.java │ └── MemberController.java ├── entity/ │ ├── DrugInfo.java │ ├── SaleOrder.java │ ├── ... ├── mapper/ │ ├── DrugInfoMapper.java │ └── ... ├── service/ │ ├── DrugService.java │ └── ... └── MedicineApplication.java统一返回结果类Result很重要它保证了前端拿到的数据格式是一致的。一般结构是public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }前端axios拦截器里会根据code字段判断请求是否成功是200就取data否则弹出错误提示。这种统一封装的好处是接口定义清晰前端处理逻辑统一不会出现一个接口返回{success:true}另一个返回{status:1}这种混乱情况。启动项目的方式很简单在MedicineApplication.java里右键Run启动成功后SpringBoot会默认在8080端口启动Tomcat。如果你本地的8080端口被占用了可以在application.yml里改server: port: 8080然后访问http://localhost:8080/如果项目配置了Swagger地址是http://localhost:8080/swagger-ui/index.htmlspringdoc方案或http://localhost:8080/swagger-ui.htmlspringfox方案。4.2 核心接口设计与Swagger接口文档这个医药管理系统的接口设计我按模块罗列一下核心接口登录认证模块POST /api/auth/login参数为username和password返回token。POST /api/auth/logout退出登录清除token。药品管理模块GET /api/drug/page分页查询支持按药品名称、分类、生产厂家筛选。GET /api/drug/{id}药品详情。POST /api/drug新增药品。PUT /api/drug修改药品。DELETE /api/drug/{id}删除药品。GET /api/drug/expiring查询临期药品。GET /api/drug/lowStock查询低库存药品。采购管理模块GET /api/supplier/page供应商分页查询。POST /api/purchase创建采购入库单同时写入主表和明细表。GET /api/purchase/page采购单分页查询。销售管理模块POST /api/sale创建销售单购物车数据一次性提交。GET /api/sale/page销售单分页查询。GET /api/sale/detail/{orderId}销售单详情。参与前后端分离项目接口文档是必须有的。这个项目里采用Swagger自动生成接口文档你不必为每个接口单独写Word文档只要在Controller方法上加上注解Swagger会自动扫描并生成可在线调试的文档页面。示例Api(tags 药品管理接口) RestController RequestMapping(/api/drug) public class DrugController { ApiOperation(value 分页查询药品列表) GetMapping(/page) public ResultPageDrugInfo page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String drugName) { // 业务代码 } }配置Swagger后前端同学或者答辩老师可以直接在页面上调试接口不需要额外导出文档。如果学校要求提交接口文档文档也可以从Swagger的/v3/api-docsspringdoc或/v2/api-docsspringfox导出JSON或转换成Markdown/HTML格式。这个“导出接口文档”的操作在毕设材料整理时很实用能省掉不少手写文档的功夫。4.3 登录鉴权与权限控制怎么做登录鉴权直接决定了一个管理系统是否“真正完整”。这个项目用的是JWT方案流程是这样的用户输入用户名密码后端校验用户名是否存在、密码是否正确。校验通过后生成一个JWTS tokentoken里包含用户ID、用户名和角色。把token返回给前端前端存到localStorage或sessionStorage。前端axios请求拦截器在每次请求的header里自动加上Authorization: Bearer token。后端配置拦截器拦截除/api/auth/login以外的所有请求校验token是否有效、是否过期。JWT工具类核心代码很简单public class JwtUtil { private static final String SECRET medicine-secret-key; private static final long EXPIRE_TIME 2 * 60 * 60 * 1000; // 2小时 public static String generateToken(String username, String role) { return Jwts.builder() .setSubject(username) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }拦截器注册Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals(OPTIONS)) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { JwtUtil.parseToken(token.substring(7)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }注册拦截器时注意要让静态资源和登录接口放行。这个细节如果忘记配置会出现“前端页面能打开但所有数据接口都401”的问题而且排查要花不少时间。权限控制如果要做得再细一点可以在拦截器里解析token中的role字段判断当前角色是否允许访问某个接口。比如if (ADMIN.equals(role)) { return true; } if (SALE.equals(role) allowedForSaleUrls.contains(request.getRequestURI())) { return true; }毕业答辩时这个点是最值得深入讲的。因为“管理系统”和“普通的CRUD页面”之间最大的区别就是有没有真正的权限控制。很多项目虽然页面上根据角色隐藏了菜单但后端接口不拦截别人知道接口地址一样能调用这在安全设计上是说不过去的。所以你哪怕只是做一个简单的拦截判断也要让代码体现出“后端也有鉴权”这个意识。5. 前端Vue实现细节与前后端联调5.1 前端目录与页面结构规划前端工程结构大概是这样的src/ ├── api/ │ ├── login.js │ ├── drug.js │ ├── sale.js │ ├── supplier.js │ └── request.jsaxios封装 ├── router/ │ └── index.js ├── layout/ │ └── Layout.vue主布局包含侧边栏和顶部导航 ├── views/ │ ├── login/Login.vue │ ├── drug/DrugList.vue │ ├── drug/CategoryList.vue │ ├── purchase/PurchaseList.vue │ ├── purchase/PurchaseAdd.vue │ ├── sale/SaleList.vue │ ├── sale/SaleAdd.vue │ ├── supplier/SupplierList.vue │ └── member/MemberList.vue └── main.js页面级别上登录页是一个独立的页面登录成功后进入Layout布局左侧菜单显示模块列表右侧是内容区点击菜单切换对应的Vue组件。这个布局是所有后台管理系统的通用模式在Vue项目里用路由嵌套来实现Layout是父路由各功能页面是子路由。路由配置示例const routes [ { path: /login, component: Login }, { path: /, component: Layout, redirect: /drug, children: [ { path: drug, component: DrugList }, { path: purchase, component: PurchaseList }, { path: sale, component: SaleList }, ] } ]路由守卫也要配一下未登录的用户访问首页时自动跳转到登录页router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })这个小配置很实用是“前端权限控制”的一部分也值得在答辩时提一下。5.2 axios封装与请求拦截器配置前端和后端联调最关键的是把axios封装做好。封装好了后续每个接口都只需要写一行调用代码。在request.js里统一配置axios实例import axios from axios import { Message } from element-ui import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器 request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) // 响应拦截器 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { Message.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } else { Message.error(网络请求失败) } return Promise.reject(error) } ) export default request然后每个API模块对应一个文件比如api/drug.jsimport request from ./request export function getDrugPage(params) { return request({ url: /drug/page, method: get, params }) } export function addDrug(data) { return request({ url: /drug, method: post, data }) }页面里只需要这样调用import { getDrugPage } from /api/drug const res await getDrugPage({ pageNum: 1, pageSize: 10, drugName: 阿莫西林 }) this.tableData res.data.records这里有一个开发阶段非常实用的配置在项目根目录的vue.config.js里配置开发代理避免跨域问题module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样前端在3000端口访问/api/drug/page时会被代理到后端的8080端口浏览器层面不触发跨域开发体验顺畅很多。如果不用代理你可能要在后端写CORS配置或者在前端用完整的路径请求然后挨个处理跨域报错体验会差很多。5.3 销售开单页面的核心逻辑整个项目里技术含量最高的页面我个人认为是销售开单页面。它不是一个简单的提交表单而是带购物车逻辑的交互页面。页面上需要实现左侧是药品搜索列表支持按名称或编号搜索结果显示库存数量和零售价点击“加入购物车”按钮把药品加入右侧的购物车区域购物车里可以修改数量、删除单条记录购物车实时计算总金额点击“结算”按钮把购物车数据一次性提交到后端。前端购物车的核心数据结构就是一个数组cart: [ { drugId: 1, drugName: 阿莫西林胶囊, spec: 0.25g*24粒, price: 15.00, quantity: 2, subTotal: 30.00 }, { drugId: 2, drugName: 布洛芬缓释胶囊, spec: 0.3g*20粒, price: 18.50, quantity: 1, subTotal: 18.50 } ]提交时把主表字段总金额、支付方式、收银员、会员ID和明细数组一起发给后端。后端事务性地做两件事插入销售单、扣减库存。这里一定要加事务否则可能出现销售单录入了但库存没扣减的情况。SpringBoot里加事务很简单Service方法上加上Transactional注解即可。这个页面的代码量不小但非常能体现一个开发者对业务的理解。答辩时你可以主动说“这个页面处理了一个订单事务库存扣减和销售单写入在同一个事务里保证数据一致性”老师的印象分会明显提升。6. 部署运行与完整流程6.1 本地运行完整步骤拿到整套源码后要按这个顺序跑起来第一步准备基础环境JDK 8或11Maven 3.6Node.js 14MySQL 5.7或8.0Navicat或命令行工具。这些缺一不可先检查一下环境。第二步初始化数据库用Navicat或命令行创建medicine数据库执行项目提供SQL脚本确保所有表创建成功。第三步修改数据库配置打开后端项目的application.yml改数据库地址、用户名、密码。spring: datasource: url: jdbc:mysql://localhost:3306/medicine?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver第四步启动后端在项目根目录执行mvn spring-boot:run或者在IDEA里右键MedicineApplication运行。启动日志出现“Started MedicineApplication”就说明成功了。第五步启动前端在前端目录执行npm install npm run serve如果npm install很慢可以切换淘宝镜像npm config set registry https://registry.npmmirror.com启动之后控制台会提示访问地址一般是http://localhost:3000。第六步打开浏览器访问前端地址使用管理员账号登录开始操作。6.2 本地运行常见问题速查表我梳理一下实际操作中遇到频率比较高的问题分成几类方便你排查现象原因解决办法后端启动报错提示端口8080被占用本机其他程序占用端口改server.port或关闭占用进程数据库连接失败Access denied数据库用户名或密码错误检查application.yml里的连接配置数据库连接失败Communications link failureMySQL没启动或端口不是3306启动MySQL服务或改URL里的端口中文乱码数据库表字符集不是utf8mb4或连接URL没有编码参数确认建库字符集URL加characterEncodingutf8前端npm install报错node-sass安装失败Node版本和node-sass不兼容换用dart-sass或调整Node版本前端页面能打开但接口401登录拦截器把请求拦截了或token没带检查axios拦截器检查后端放行的URL配置接口报CORS错误前后端端口不同没用代理用vue.config.js配置代理或后端加跨域配置Swagger页面无法访问springfox和SpringBoot版本不兼容换springdoc或降SpringBoot版本启动时提示Table doesnt existSQL脚本没执行成功重新执行脚本确认所有表创建成功还有一个容易被忽略的问题如果你在后端新加了接口前端调用时发现404先排查Controller的RequestMapping路径和前端API文件里请求的URL是否完全一致。大小写、多一个斜杠都不行这类问题如果不仔细看能找半天。7. 从毕设到项目的进阶建议如果你的目标不只是“让系统跑起来”而是想把项目作为毕业设计的亮点或者在面试时能拿出来讲有几个方向值得投入时间。第一把库存预警和临期提醒做成真正的提醒维度不只是列表查询。例如在导航栏右上角显示未处理预警数量点击进入预警详情。这里需要思考“什么是预警”——库存低于阈值算一条药品距离有效期不足30天算一条接口可以用一个聚合统计接口实现前端登录后定时拉取。这种“主动推送”的体验比用户手动去查询列表要更像一个真正的系统。第二考虑多门店或者多仓库场景。现在的表结构只有单一库存字段如果未来要支持多门店就需要在药品表之外设一个“门店库存表”每一条记录代表某个仓库中某个药品的库存数量。这个设计能体现出你对业务扩展性的思考。第三补充一个数据可视化页面。例如统计每月销售额、销售趋势、药品销售排行榜用ECharts画柱状图和饼图。这种页面视觉效果好又能在答辩演示时讲出“我用ECharts做了数据可视化分析”是很划算的加分项。第四尝试完善系统日志模块。每次登录、新增、修改、删除操作都记录到一张日志表里前端提供一个日志查询页面。其实很多系统都会有日志需求能做出来会显得工程意识很到位。我个人在实际操作中的体验是这类毕设项目一定要亲自把整个流程跑通至少两遍第一遍按文档走第二遍尝试不看文档重新部署一次。在这个过程中会暴露大量问题比如SQL脚本的编码、依赖版本冲突、前端跨域配置。等这些问题全部处理完之后你对项目的理解、对SpringBoot和Vue这套技术栈的掌握会有一个明显的提升。答辩时不管老师问什么你都能基于自己实际踩坑的经验给出有底气的回答。最后再分享一个小技巧拿到一个项目源码后第一件事不要急着启动先花半小时把所有代码文件过一遍看清目录结构、数据表、接口路径。这个习惯能帮你节省至少两小时的调试时间也会让你对项目的掌控力完全不同。