基于SSM+Vue的药房药品管理系统实现与避坑指南

发布时间:2026/9/23 23:35:17

基于SSM+Vue的药房药品管理系统实现与避坑指南 简介一套基于Java、SSM与Vue技术栈的药房药品管理系统完整源码面向JavaWeb学习者和毕业设计选题学生也适用于需要快速搭建药品库存、药品信息管理场景的开发者。系统采用前后端分离结构后端以Spring、SpringMVC、MyBatis整合支撑业务逻辑前端用Vue.js构建交互页面并通过SpringBoot简化配置、Maven管理依赖数据存储在MySQL5.7及以上版本中。压缩包整体约22.7MB包含项目源码和配置文件可直接作为同类管理系统的开发蓝本。目前已有83人学习下载适合用来理解SSM整合流程、Vue与后端接口的数据交互方式以及药品管理场景下的表结构设计思路拿到手后可结合自身需求做二次开发快速形成可用于课程设计或项目练习的可用系统。1. 药房管理系统用 SSMVue 写最大的价值不是新而是能跑如果你正在找一套 java-ssmvue 药房药品管理系统实现源码做课程设计或者毕业设计大概率是这么个场景你已经在 CSDN 或 GitHub 上翻过几个项目不是依赖版本老得装不上就是前端 Vue3 和网上教程对不上号。这套 SSM Vue 的组合放在 2026 年看技术栈确实不算新但论照着重现、查资料方便、答辩讲得清楚它依然是课程设计级项目里吃得最开的搭配。这篇文章就顺着这套系统讲清楚三件事药房系统到底在管什么、SSM 后端和 Vue 前端各自的代码怎么落、以及真跑起来会遇到哪些坑。同时带出 MySQL 表设计、库存扣减、效期预警等关键逻辑让你拿到源码后能照着复现而不是卡在配置里。2. 先把系统拆明白药房药品管理的核心业务与数据模型2.1 药房系统的业务边界不只是 CRUD药房药品管理系统表面上就是一套增删改查但细分下去业务边界比普通的后台管理要宽。常见的功能模块包括药品基础信息维护名称、规格、生产厂家、批准文号、药品分类管理处方药与非处方药要分开、供应商管理、采购入库、销售出库、库存盘点、效期预警、过期药品统计以及最基础的用户登录和权限区分管理员、药师、收银员三类角色。这里有个很关键的药房业务特性药品不能只按名称存一个总库存数。同一款药可能有两个批次一批是 2025 年 3 月生产的另一批是 2025 年 9 月生产的如果不把批次拆开效期预警就是一笔糊涂账。这也是药房系统和普通商城系统的本质差异——商城管库存只看数量药房必须在数量之外再看效期。另一个容易被忽视的边界是处方药流程。收银员卖处方药时系统至少要记录开方医生和患者信息这个字段在销售订单里必须有。现实里很多毕设源码只是把处方做成一个备注输入框这在你答辩时会被老师追问所以做表设计时我会单独留一张处方信息表。2.2 数据库设计药品表、库存表、供应商表和它们的边界我见过太多同学拿到源码后先跑起来然后再研究表结构最后答辩被问两句就露馅。正确顺序是先把表拆明白。一套标准的药房系统核心表大概六张左右用户表、药品分类表、药品信息表、药品库存批次表、供应商表、采购订单表、销售订单表。其中药品信息表和库存批次表是核心中的核心。药品信息表不能把库存数量直接写进去。药品基本信息是静态数据库存是动态数据两者混在一张表里会导致每次销售扣减都要 UPDATE 同一行并发高一点就会出现超卖。我建议把库存拆出来按批次管理这是药房系统设计里最值得认真理解的一个点。下面给出核心表的精简建表 SQL你可以直接用来做初始库结构-- 药品信息表 CREATE TABLE drug ( drug_id BIGINT NOT NULL AUTO_INCREMENT, drug_code VARCHAR(50) NOT NULL COMMENT 药品编码, drug_name VARCHAR(100) NOT NULL COMMENT 药品名称, spec VARCHAR(100) COMMENT 规格如 0.25g*24片, category_id BIGINT COMMENT 分类ID关联 drug_category, manufacturer VARCHAR(150) COMMENT 生产厂家, approval_number VARCHAR(100) COMMENT 批准文号, prescription TINYINT DEFAULT 0 COMMENT 0-非处方 1-处方, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (drug_id), UNIQUE KEY uk_drug_code (drug_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 库存批次表 CREATE TABLE drug_stock ( stock_id BIGINT NOT NULL AUTO_INCREMENT, drug_id BIGINT NOT NULL, batch_no VARCHAR(50) NOT NULL COMMENT 批次号, produce_date DATE NOT NULL COMMENT 生产日期, expire_date DATE NOT NULL COMMENT 有效期至, stock_qty INT NOT NULL DEFAULT 0 COMMENT 当前库存数量, PRIMARY KEY (stock_id), KEY idx_drug_id (drug_id), KEY idx_expire (expire_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明drug 表只存药品的静态属性drug_stock 表存批次和动态数量。两表通过 drug_id 关联。这里有两个关键设计一是 drug_stock 里必须存 expire_date效期预警全靠这个字段二是 stock_qty 不能允许负数这个约束不能只在代码里做数据库层面也得有 CHECK 约束或者在扣减 SQL 里加条件。参数说明drug_code 设置了唯一索引这是避免同一药品重复建档的第一道防线category_id 不建强外键只保留逻辑关联这样删除分类时不会被数据库外键拦着实际操作更灵活。表引擎用 InnoDB 是必须的因为库存扣减依赖事务字符集用 utf8mb4 是防止生僻药品名插入时报错。2.3 效期预警与库存扣减药房系统最核心的两条 SQL效期预警的逻辑不复杂但 SQL 写不对就会出问题。业务上药房通常把有效期剩余 6 个月的药品列为近效期剩余 3 个月的列为预警。这条 SQL 需要算当前日期到 expire_date 之间相差多少天然后按天过滤SELECT d.drug_name, d.spec, s.batch_no, s.expire_date, s.stock_qty, DATEDIFF(s.expire_date, CURDATE()) AS remain_days FROM drug_stock s JOIN drug d ON s.drug_id d.drug_id WHERE DATEDIFF(s.expire_date, CURDATE()) BETWEEN 0 AND 180 AND s.stock_qty 0 ORDER BY s.expire_date ASC;逻辑说明用 DATEDIFF 直接算剩余天数然后筛选在 0 到 180 天内的批次。DATEDIFF 的结果为 0 表示当天过期为负数表示已经过期所以 BETWEEN 0 AND 180 会把过期药品排除在外。如果需要把过期药也列出来把这个条件改成BETWEEN -30 AND 180即可。参数说明180 天这个阈值可以根据实际需求调整有的药店用 90 天。ORDER BY expire_date ASC 让最紧急的批次排在最前面页面直接渲染就能用。再看库存扣减这是药房系统里最容易写翻车的操作。销售出库时需要同时完成扣库存和产生销售记录两步必须放到同一个事务里。扣减 SQL 长这样UPDATE drug_stock SET stock_qty stock_qty - #{qty} WHERE stock_id #{stockId} AND stock_qty #{qty};逻辑说明关键在于 UPDATE 语句的 WHERE 条件里带了stock_qty #{qty}这样如果库存不足这条 SQL 影响的行数是 0程序就能感知到并回滚事务避免出现负库存。参数说明这里走的是批次维度扣减即销售时指定扣哪个批次。如果系统只在药品维度扣减那就要按先进先出原则先查最早批次再逐个扣减逻辑会复杂很多。对于毕设源码来说按批次 ID 精确扣减是性价比最高的方案。3. 后端 SSM 落地从 Spring 配置到 MyBatis 的完整链路3.1 Maven 依赖与项目骨架版本号是最大的坑SSM 项目的第一步是把 pom.xml 配对。很多源码跑不起来八成是 jar 包版本互相冲突。这里给出一套亲测稳的组合Spring 5.2.x、MyBatis 3.5.x、mybatis-spring 2.0.x、Druid 1.2.x、MySQL 连接器用 8.0.x、Jackson 2.9.x。properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target spring.version5.2.22.RELEASE/spring.version /properties dependencies !-- Spring 核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis 与 Spring 整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.16/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency !-- 数据库连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.31/version /dependency !-- JSON 序列化 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.12.5/version /dependency /dependencies逻辑说明Spring 的版本统一用变量管理避免 spring-webmvc 和 spring-jdbc 版本不一致导致运行期找不到类。MySQL 驱动要用 8.0.x对应的驱动类名是com.mysql.cj.jdbc.Driver和旧版的com.mysql.jdbc.Driver不同这个细节能卡住很多人。Jackson 必须配因为后端 Controller 返回 JSON 全靠它不配的话前端拿到的是乱码或直接报 406。参数说明JDK 统一用 1.8不要用 17 或更高版本否则 Spring 5.2 在运行期会有模块访问报错。Druid 1.2.x 版本够用就好不要追新新版本对 JDK 版本要求更严格。项目骨架按 Maven 标准结构来src/main/java 里分包为 controller、service、dao或 mapper、entity、configsrc/main/resources 下放 spring 配置文件、mybatis-config.xml、mapper 映射文件。我强烈建议把 mapper XML 文件放在 resources/mapper 目录下而不是和 interface 放一起否则 Maven 打包时默认不会带上 XML 文件。3.2 Spring SpringMVC MyBatis 配置三个文件串起一条链SSM 配置说难不难说容易也容易漏。核心是三个配置spring.xml 管业务层和数据源spring-mvc.xml 管 Controller 和 JSONmybatis-config.xml 管 MyBatis 行为。!-- spring.xml 关键部分 -- context:component-scan base-packagecom.pharmacy.service/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/pharmacy_db?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value123456/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.pharmacy.dao/ /bean!-- mybatis-config.xml -- configuration settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSTDOUT_LOGGING/ /settings /configuration!-- spring-mvc.xml -- mvc:annotation-driven/ mvc:default-servlet-handler/ context:component-scan base-packagecom.pharmacy.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean逻辑说明这三个配置文件的职责边界要分清。spring.xml 里配置数据源和 MyBatis 的 SqlSessionFactory让 DAO 层可以被 Spring 管理mybatis-config.xml 只放 MyBatis 自身的设置其中mapUnderscoreToCamelCase必须设为 true否则数据库的 create_time 字段映射不到实体的 createTime 属性上所有时间字段查出来都是 null。spring-mvc.xml 里mvc:annotation-driven负责注册 JSON 转换器mvc:default-servlet-handler负责放行静态资源否则前端打包后的 js/css 文件全部 404。参数说明Druid 数据源的 URL 里必须带serverTimezoneAsia/Shanghai这是 MySQL 8 的强制要求不带会报时区错误。characterEncodingutf8解决中文乱码。classpath:mapper/*.xml是 Mapper XML 的扫描路径如果你的 XML 放在别的位置这里要对应改。3.3 核心代码实现药品分页、库存扣减与节点登录后端代码不需要每一张表都写一套关键是吃透一个分页查询 一个事务操作其余模块照着抄结构就行。先看药品分页查询Controller RequestMapping(/api/drug) public class DrugController { Autowired private DrugService drugService; ResponseBody GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword) { ListDrug list drugService.queryPage(pageNum, pageSize, keyword); int total drugService.count(keyword); return Result.ok(list, total); } }!-- DrugMapper.xml -- select idqueryPage resultTypecom.pharmacy.entity.Drug SELECT drug_id, drug_code, drug_name, spec, manufacturer, approval_number, prescription, create_time FROM drug where if testkeyword ! null and keyword ! AND (drug_name LIKE CONCAT(%, #{keyword}, %) OR drug_code LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select逻辑说明分页没有引入 PageHelper 插件而是手写 LIMIT原因是减少一个依赖就少一分版本冲突风险。offset的计算放在 Service 层(pageNum - 1) * pageSize。XML 里用where标签配合if实现动态 SQL关键词为空时不拼接条件关键词非空时对药名和编码做模糊匹配。参数说明RequestParam(defaultValue 1)保证前端不传 pageNum 时后端不会报 400。count 查询和 queryPage 是两个独立 SQL这样才能拿到总条数做分页组件。action 层的ResponseBody必须加不加的话 SpringMVC 会把返回值当成视图名解析前端拿到的会是 404。再看库存扣减这个事务方法这是整个系统最需要谨慎的地方Service public class SaleServiceImpl implements SaleService { Autowired private DrugStockDao drugStockDao; Autowired private SaleOrderDao saleOrderDao; Override Transactional(rollbackFor Exception.class) public void createSaleOrder(SaleOrder order, ListSaleOrderDetail details) { for (SaleOrderDetail detail : details) { int rows drugStockDao.deductStock(detail.getStockId(), detail.getQty()); if (rows 0) { throw new RuntimeException(库存不足扣减失败); } saleOrderDao.insertDetail(detail); } saleOrderDao.insertOrder(order); } }逻辑说明方法上加了Transactional任何一个批次扣减失败都会抛异常并回滚全部操作避免出现订单生成了但库存没扣或者反过来库存扣了但订单没记录的不一致状态。deductStock 就是 2.3 节里那条带stock_qty #{qty}条件的 UPDATE返回影响行数 rows为 0 就说明库存不够。参数说明rollbackFor Exception.class指定了回滚条件是所有异常。Spring 默认只在抛出 RuntimeException 时才回滚如果漏配这个参数catch 住了异常但没重新抛出事务就能成功提交造成超卖这是血泪经验。事务粒度上我把整个订单包含多条明细放在一个事务里药房这种低频系统不需要做分布式事务单库事务完全够用。再说登录鉴权。SSM 时代最稳妥的方案是 Session 拦截器不用引入 Shiro 或 JWT避免增加复杂度Controller public class LoginController { Autowired private UserService userService; ResponseBody PostMapping(/login) public Result login(RequestParam String username, RequestParam String password, HttpSession session) { User user userService.findByUsername(username); if (user ! null user.getPassword().equals(password)) { session.setAttribute(loginUser, user); return Result.ok(); } return Result.error(用户名或密码错误); } }mvc:interceptors mvc:interceptor mvc:mapping path/api/**/ mvc:exclude-mapping path/api/login/ mvc:exclude-mapping path/api/logout/ bean classcom.pharmacy.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors逻辑说明登录成功把用户对象塞进 Session拦截器负责拦截除 login 和 logout 外的所有/api/**请求。LoginInterceptor 里做一件事从 Session 拿 loginUser拿不到就返回 JSON{code: 401, msg: 未登录}不重定向到登录页因为前后端分离下页面路由归 Vue 管。参数说明exclude-mapping必须配全放行路径否则前端第一次请求验证码接口都会被拦。毕业答辩时老师常问你权限怎么控制的你就答 Session 拦截器控制粒度到是否登录和角色判断够用且好讲。4. 前端 Vue 落地从脚手架到 Element UI 联调4.1 环境准备Node 版本、vue/cli 安装、项目初始化前端这块实际跑的时候坑比后端还多主要集中在环境版本上。SSM 后端对应的是 Vue 2 Element UIVue 2 已经停止维护但生态稳定课程设计起步用它最省心。先用命令行确认环境node -v npm -v逻辑说明Vue 2 项目建议 Node 14 或 16Node 18 在部分场景下与 node-sass 不兼容容易直接编译失败。如果你用 Vue 3那配套的是 Element Plus但后端接口风格差异不大。这里我默认走 Vue 2 路线。参数说明node-sass 是这个项目里最可能翻车的一个依赖。最新版 Node 装 node-sass 大概率报错建议用npm install sass1.32.13替代同时组件内部langscss照样支持。下面安装项目基础依赖npm install -g vue/cli4.5.15 vue create pharmacy-web # 选择 Manually select features勾选 Router、Vuex cd pharmacy-web npm install element-ui2.15.14 axios0.21.1逻辑说明vue/cli固定用 4.5.x太高版本创建项目时会默认生成 Vue 3 模板。Element UI 2.x 只兼容 Vue 2Element Plus 只兼容 Vue 3这两者混用是典型的装依赖翻车现场。axios 用 0.21.x 是因为新版本 axios 的拦截器写法变化不大但旧教程代码跑在旧版本上更不会出幺蛾子。4.2 axios 封装与跨域代理前后端联调的必踩点前后端分离开发时前端在http://localhost:8080后端在http://localhost:8081直接请求必然跨域。解决办法不是在后端写CrossOrigin那个会让 Session 丢失而是用 vue.config.js 的代理转发。// axios 封装 request.js import axios from axios import { Message } from element-ui import router from ../router const request axios.create({ baseURL: /api, timeout: 10000, withCredentials: true }) request.interceptors.response.use( response { const res response.data if (res.code 401) { router.push(/login) return Promise.reject(new Error(未登录)) } return res }, error { Message.error(error.message) return Promise.reject(error) } ) export default request// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }逻辑说明axios 的 baseURL 直接写/api不要写成http://localhost:8081/api。开发时 devServer 会把路径里以/api开头的请求转发到 target 指向的后端地址从而绕过浏览器同源策略。withCredentials: true是必须的它允许跨域请求携带 CookieSession 登录才不会被断掉。参数说明后端接口路径要统一加/api前缀比如后端的/drug/list写完整是/api/drug/list这样代理规则不用做路径重写。timeout设 10000 毫秒SSM 项目首次请求时 MyBatis 初始化较慢如果超时时间设短了会误报超时。生产环境部署时这个代理配置不生效前端打包后的 js 文件直接由后端同一个 Tomcat 提供请求从 8080 端口发到自身天然同源不用额外处理跨域。4.3 页面与路由药品列表、库存预警、登录页的实现思路页面部分最核心的不是把每个页面写完而是把列表 分页 弹窗和路由守卫登录跳转两件事吃透。看药品列表页的骨架template div el-input v-modelkeyword placeholder搜索药品名称/编码 stylewidth: 300px/ el-button typeprimary clickloadData查询/el-button el-table :datatableData border stripe el-table-column propdrugName label药品名称/ el-table-column propspec label规格/ el-table-column propmanufacturer label生产厂家/ el-table-column propprescription label类型 template slot-scopescope{{ scope.row.prescription 1 ? 处方药 : 非处方药 }}/template /el-table-column el-table-column label操作 template slot-scopescope el-button typetext clickshowStock(scope.row)库存/el-button el-button typetext clickopenEdit(scope.row)编辑/el-button /template /el-table-column /el-table el-pagination :current-pagepageNum :page-sizepageSize :totaltotal layouttotal, prev, pager, next current-changechangePage/ /div /template script import request from /utils/request export default { data() { return { keyword: , tableData: [], pageNum: 1, pageSize: 10, total: 0 } }, created() { this.loadData() }, methods: { async loadData() { const res await request.get(/drug/list, { params: { pageNum: this.pageNum, pageSize: this.pageSize, keyword: this.keyword } }) this.tableData res.data this.total res.total }, changePage(page) { this.pageNum page this.loadData() } } } /script逻辑说明页面模板用 el-table 渲染列表el-table-column 的 prop 对应后端返回字段的驼峰命名。关键字搜索、分页都通过 axios 实例请求后端响应被拦截器处理后直接在函数里取 res.data 和 res.total。created生命周期里调用 loadData保证页面加载即查数据。参数说明el-pagination 的current-change只在页码切换时触发pageNum 变化后要重新请求。注意后端接口返回结构保持统一格式{code, msg, data, total}这样拦截器逻辑只需写一遍每个页面调接口拿数据时才能同步拿到分页总数。路由守卫控制页面访问未登录用户直接访问/dashboard会被弹回登录页。在router/index.js里加前置守卫router.beforeEach((to, from, next) { const loginUser sessionStorage.getItem(loginUser) if (to.path /login) { next() } else if (!loginUser) { next(/login) } else { next() } })逻辑说明前端在登录成功后把用户信息写入 sessionStorage守卫每次路由跳转前检查。这里需要配合后端 Session 过期时间一起设计前端存的是我登录过的标记后端 Session 存的是真正的登录态。两者不一致时靠 axios 响应拦截器里的 401 判断来引导用户重新登录。参数说明sessionStorage 与 localStorage 的区别是前者关闭浏览器页签即失效药房系统选 sessionStorage 更贴合安全性需求。如果后端 Session 设置了 30 分钟超时前端也需要在拦截器收到 401 时清理 sessionStorage避免状态残留。5. 避坑指南SSMVue 药房系统最常见的 5 个翻车点5.1 现象MySQL 8 驱动连接报异常运行后端时报Loading class com.mysql.jdbc.Driver. This is deprecated或者直接ClassNotFoundException: com.mysql.jdbc.Driver。原因MySQL 8 的驱动包把驱动类从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver。你的 pom.xml 里如果导入了 mysql-connector-java 8.x但 spring.xml 里 driverClassName 还写的旧类名就会报错。即便改成新类名URL 连接字符串不带serverTimezone参数依然会在建立连接时报时区错。解决两处配置一并改掉。driverClassName 用com.mysql.cj.jdbc.DriverURL 改成jdbc:mysql://localhost:3306/pharmacy_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。如果你用的 MySQL 5.7则不用改类名但也建议把 URL 写全避免中文字符集出问题。5.2 现象create_time 查询出来全是 null药品列表页面有数据但时间列空白打开浏览器控制台发现接口返回的createTime字段为 null。数据明明在数据库里。原因数据库字段名是下划线风格create_timeJava 实体属性是驼峰风格createTimeMyBatis 没有启用驼峰映射时两者对不上值自然映射不进去。解决在 mybatis-config.xml 里加setting namemapUnderscoreToCamelCase valuetrue/。加完后重启后端问题消失。如果个别查询用到了 resultMap 手写映射resultMap 里也要把 column 和 property 写对。顺手检查一下 SQL 里是否用了SELECT *用了SELECT *则字段映射靠全局配置生效问题好解决如果你手写了字段列表却漏了 create_time那加配置也救不了。5.3 现象前端登录后请求列表接口仍然 401登录接口成功跳转首页后列表数据加载失败后端拦截器报未登录。原因这是前后端分离项目的经典跨域掉 Session 问题。前端地址是 localhost:8080后端是 localhost:8081你加了CrossOrigin支持跨域但浏览器默认在跨域请求下不会携带 Cookie后端 Session 里根本找不到loginUser。请求看起来是通的但每次都是新 Session。解决分两步。第一步去掉后端CrossOrigin改用前端 vue.config.js devServer 代理掉跨域第二步在 axios 请求配置里加withCredentials: true让浏览器愿意把 Cookie 带上去。注意代理路径要统一/api开头的请求全部转发到 8081后端 Controller 的 RequestMapping 按/api/...设计。5.4 现象部署到 Tomcat 后页面 404只有登录页能访问本地开发一切正常npm run build后把 dist 目录丢到 Tomcat webapps 下访问根路径能出登录页但刷新/嵌套路由时 404或者直接访问首页空白。原因Vue Router 默认的模式是 hash但有些人为了提高美观度会改成 history 模式。history 模式是浏览器真实路径Tomcat 在对应物理路径下没有这个文件自然返回 404。登录页能访问是因为直接落在 index.html 上路由没触发刷新。解决除非你有把握配置 Tomcat 的 rewrite否则把 Vue Router 改回 hash 模式mode不写或者显式写成hash。URL 会多一个#/前缀不影响功能切换页面不再请求后端。如果你确实要 history 模式需要在 web.xml 里配置 forward 到 index.html 的 servlet这属于进阶操作毕设不建议给自己加戏。5.5 现象接口返回的时间是一串数字或格式不对前端展示时间列显示为类似/Date(1700000000000)/或者2024-11-15T10:30:00和页面想要的2024-11-15 10:30:00对不上。原因后端返回 Java 的 Date 对象时Jackson 默认序列化成时间戳一串数字。而实体字段如果用的是 LocalDateTime序列化格式又是一套 ISO 标准格式。两种原因症状不一样操作上要分别处理。解决统一在实体时间字段上使用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)注解或者更省事的方式是在 spring-mvc.xml 里配置一个全局的 ObjectMappermvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper bean classcom.fasterxml.jackson.databind.ObjectMapper property namedateFormat bean classjava.text.SimpleDateFormat constructor-arg valueyyyy-MM-dd HH:mm:ss/ /bean /property /bean /property /bean /mvc:message-converters /mvc:annotation-driven逻辑说明每个实体加JsonFormat太啰嗦全局配置一次解决所有 Date 类型字段。timezone 指定GMT8是为了避免服务器时区和本地时区不一致导致前端看到的时间差 8 小时这个坑在部署到云服务器时特别容易出现。参数说明如果你的实体用的是 LocalDateTimeObjectMapper的 dateFormat 属性对它无效需要额外注册JavaTimeModule对应的依赖是 jackson-datatype-jsr310。最简单的选择是实体全部用 java.util.DateSSM 课程设计场景完全够用。6. 从源码到上线本地跑通 打包部署的验证清单6.1 用 Maven 打包并部署到 Tomcat后端在 IDEA 里开发调试没问题后下一步是打包部署。SSM 项目用 Maven 打 war 包mvn clean package -DskipTests执行完会在 target 目录生成xxx.war。把这个 war 文件复制到 Tomcat 的webapps目录下启动 Tomcatwar 会被自动解压成一个同名文件夹。访问路径就是http://localhost:8080/xxx/其中xxx是 war 包名这个路径要记住因为前端请求的/api实际对应http://localhost:8080/xxx/api是一个相对路径关系。前端部分执行npm run build把生成的dist目录里的文件复制到刚才那个webapps/xxx目录里这样前端页面和后端接口就在同一个服务下运行了。访问http://localhost:8080/xxx/就能同时拿到 Vue 页面和后端数据不再有跨域问题。一个常见的路径坑如果 war 包名是pharmacy.war访问根路径不会自动跳到pharmacy地址栏要手动输入完整路径。或者直接把 war 包名改成ROOT.war部署后可以直接用根路径访问。6.2 验证功能清单与数据观察点部署完成后按这份清单过一遍20 分钟确认系统可用。每一项都对应一个业务场景建议拿着预置的测试账号逐条操作功能点操作步骤预期结果登录输入管理员账号密码跳转首页无效账号提示错误药品分页打开药品管理页列表显示 10 条翻页正常关键词搜索输入一个药品名关键字列表刷新仅显示匹配项效期预警打开预警页面剩余 180 天内的批次按效期升序显示销售出库选择药品和批次填数量提交列表库存减少订单记录生成库存不足单个批次数量填大于库存的值提示库存不足订单不生成未登录访问清理浏览器 Cookie 后直接访问首页跳转登录页最后检查一件事效期预警的日期计算是否落在交易时段内。数据库里的CURDATE()取的是数据库服务器时间如果数据库部署在国外服务器上和北京时间有 8 小时时差预警结果可能早晚偏一天。这不是代码 bug而是环境时区问题校验时用本机 MySQL 基本不会有问题。这套链路打通之后你再遇到 SSMVue 的项目从解压源码到上线部署的完整流程已经走了一遍。我自己早期做这类系统时最喜欢把效期预警的逻辑写在 Java 代码里循环判断数据一多性能立刻崩后来才意识到数据库 SQL 做的事就不要搬到应用层记住这个原则药房系统这类重查询、重报表的项目才不会写到后面自己都改不动。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/23 23:35:17

FAT32文件系统源码解析:从磁盘布局到Linux镜像验证

简介:FAT32文件系统源代码压缩包是面向驱动开发、嵌入式设计及底层编程学习者的完整参考实现。资源围绕FAT32核心机制展开,涵盖FAT表读写、启动扇区BPB解析、簇链分配与释放、目录项管理以及长文件名处理等关键模块,配套中文使用手册和工程配…

2026/9/23 23:30:17

多用途QQ群机器人长期稳定运行:nonebot2协议选型与插件管理实战

简介:这是一份面向Python开发者与聊天机器人爱好者的QQ群机器人完整源码项目,基于nonebot2框架构建,适合希望学习事件驱动插件开发、异步编程与社交平台机器人落地的中初级开发者。项目以多用途群聊管理为核心,涵盖自动回复、群管…

2026/9/24 0:40:22

Java服务端发丝级抠图:ONNX Runtime部署matting模型实战

简介:该资源是一套基于ONNX模型的发丝级人像抠图与背景替换Java实现源码,面向希望将深度学习模型集成到Java应用中的开发者,以及研究图像分割与高精度抠图的技术人员。项目以Java为核心语言,借助ONNX实现跨框架模型加载与推理&…

2026/9/24 0:35:22

细粒度用户评论情感分析实战:BiGRU、RCNN与Capsule模型对比

简介:基于Python的细粒度用户评论情感分析设计与实现,是一套面向NLP开发者和数据分析师的完整工程资源。其围绕用户评论情感识别场景,系统覆盖文本清洗、分词、情感词典构建、特征工程以及规则/机器学习/深度学习等建模方法,适合用…

2026/9/23 12:07:00

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/23 12:06:55

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/24 0:00:21

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:21

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:21

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/22 16:34:32

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/22 20:01:30

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/22 13:25:41

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码