ARTICLE DETAIL

资讯详情

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

SSM药房管理系统毕设全攻略:从数据库脚本到避坑部署

SSM药房管理系统毕设全攻略:从数据库脚本到避坑部署 简介一套基于SSM框架的药房管理系统毕业设计项目源码与数据库脚本面向计算机相关专业毕业生、正在准备毕设答辩的学生以及需要完整项目练手的中级Java学习者。项目经导师指导并获九十八分评审源码全部经过本地编译与严格调试确保可以运行且内容经过助教老师审定。资源包共1222个文件约19.56MB包含48个Java核心类、70个JSP页面、大量前端样式脚本与图片资源并提供SQL数据库脚本与XML配置可覆盖药品分类管理、库存预警、供应商与客户信息管理、销售统计等典型药房业务模块。前端采用EasyUI、Bootstrap等组件库界面整洁便于阅读与二次开发。目前已有174人学习下载适合作为课程设计、毕业设计及项目实战参考导入开发工具即可运行快速搭建一套完整可演示的药房管理系统也可作为SSM框架整合学习的进阶案例。1. Java毕设选题里的SSM药房系统为什么说它是后台管理最稳的模板每年毕设季都会有一批Java方向的同学拿着“基于SSM框架的药房管理系统源码数据库脚本”来问我这个题目能不能做、好不好做。我的答案通常是好好做这是后台管理系统里最稳的一类高分毕设。它没有推荐算法的玄学成分也没有分布式项目的部署负担却把SSM框架的知识覆盖得很完整——Spring管对象、Spring MVC管请求、MyBatis管数据库再配上一份像样的数据库脚本和几个核心业务模块就是一个标准的高分项目模板。适合基础一般但想稳拿成绩的人也适合已经在工作、想补一套SSM完整链路的人。下面按我做项目的顺序把框架选型、数据库设计、源码实现、排错和答辩经验一次讲透。2. SSM框架组合与工程结构三件套怎么分工源码包怎么搭2.1 Spring、Spring MVC、MyBatis的分工边界背下这张职责表SSM三个字母拆开是Spring、Spring MVC、MyBatis。很多同学上来就问“现在工作都写Spring Boot了毕设为什么还折腾SSM”我的理解是Spring Boot替你做掉了大量自动配置而SSM把底层的IoC容器、AOP切面、MVC分发链路全部暴露给你手写。老师要看的恰恰是这个——你能不能说清楚一次HTTP请求怎么从Controller走到Mapper事务在哪个环节生效Bean是怎么被管理起来的。把三个框架放到药房系统里职责边界其实非常清楚层核心组件在药房管理系统里的具体职责表现层Spring MVC接收登录、药品增删改查、销售录入的HTTP请求交给Controller再渲染JSP业务层Spring管理Service实现类声明式事务处理销售单和库存扣减库存预警的业务判断持久层MyBatis把Java对象映射成SQL参数把查询结果映射成Drug、Sales等实体这一层关系搞不清楚后面写代码一定会乱。最常见的问题是新手在Controller里直接注入Mapper看起来跑通了但事务、参数校验、日志切面全都绕过Spring管理答辩时一问事务隔离级别就露馅。2.2 版本选型与Maven依赖JDK 8、Spring 5.x、MyBatis 3.5.xSSM组合最好锁JDK 8。别追JDK 17很多老依赖在JDK 17上会因为模块化或者反射访问限制直接翻车。毕设要的不是最新是最稳。我常用的版本组合是Spring 5.2.x Spring MVC 5.2.x MyBatis 3.5.x mybatis-spring 2.0.x数据库用MySQL 8.0或5.7都行。pom.xml里需要引入一组相互兼容的坐标重点看注释里的坑。properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding spring.version5.2.25.RELEASE/spring.version mybatis.version3.5.10/mybatis.version /properties dependencies !-- Spring 核心、MVC、JDBC 三个模块缺一不可 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency 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 与 mybatis-spring 必须配套mybatis-spring 2.0.x 对应 mybatis 3.5.x -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency !-- MySQL 8 必须用 com.mysql.cj.jdbc.Driver5.7 建议用 5.1.49 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency !-- 连接池用 Druid后面排查连接问题时能拿到更多监控信息 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency /dependencies依赖版本千万别全部都写“最新版”。Spring 6.x和MyBatis 3.5.x混用、mybatis-spring 3.0.0配合Spring 5.2都会出现启动时Bean创建异常。锁版本锁JDK 8这是SSM项目跑起来的第一原则。2.3 工程目录与三层代码结构Controller-Service-Mapper怎么分单模块Maven项目足够。目录结构直接决定答辩老师的第一印象我看到太多人把几百行代码堆在Servlet里那种项目连看第二眼的欲望都没有。标准做法是严格分包。pharmacy-ssm/ ├── pom.xml └── src/main/ ├── java/com/pharmacy/ │ ├── controller/ # 只做参数接收和视图跳转 │ │ ├── LoginController.java │ │ └── DrugController.java │ ├── service/ # 业务接口 实现事务注解写这里 │ │ ├── DrugService.java │ │ └── impl/DrugServiceImpl.java │ ├── mapper/ # MyBatis的Mapper接口一个接口对应一个业务实体 │ │ ├── DrugMapper.java │ │ └── DrugMapper.xml │ ├── entity/ # 数据库表对应的JavaBean │ │ ├── Drug.java │ │ ├── User.java │ │ └── Sales.java │ ├── interceptor/ # 登录拦截器、操作日志拦截器 │ │ └── LoginInterceptor.java │ └── config/ ├── resources/ │ ├── jdbc.properties │ ├── mybatis-config.xml │ ├── spring/applicationContext.xml │ └── spring/spring-mvc.xml └── webapp/ ├── WEB-INF/views/ # JSP页面 └── static/ # CSS、JS、图片Spring容器和Spring MVC容器要分开。主流做法是applicationContext.xml扫描service、mapper、数据源spring-mvc.xml只扫描controller。我在applicationContext里配置数据源、SqlSessionFactory、事务管理器再把Mapper接口扫描出来!-- 数据库连接参数 -- context:property-placeholder locationclasspath:jdbc.properties/ !-- Druid 数据源 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ /bean !-- SqlSessionFactorymapper.xml 放在 classpath:mapper/ 下 -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.pharmacy.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean !-- 自动扫描 Mapper 接口 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.pharmacy.mapper/ /bean !-- 事务管理器注解驱动 -- bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/这里最容易被忽略的是MapperScannerConfigurer。没有它每个Mapper接口都得手写bean写十个药品相关的方法就得配十次完全没必要。扫描包路径必须和接口包一致否则启动时直接报找不到SqlSessionFactory。事务管理器DataSourceTransactionManager的dataSource必须和SqlSessionFactory用的dataSource是同一个这是新手最容易踩的事务失效原因。3. 药房数据库脚本建库建表、初始化数据与外键索引的设计细节3.1 核心表怎么拆用户、药品、供应商、销售单缺一不可数据库脚本是一份毕设的隐形评分点。药房管理系统最少要覆盖六张表用户表sys_user、药品分类表drug_category、药品信息表drug、供应商表supplier、销售主表sales、销售明细表sales_item。药品和供应商是多对一一张销售单对应多行明细明细里既要关联drug_id又要冗余药品名称和规格作为快照。为什么明细表要冗余快照因为药品价格和名称是会变的半年后再翻一张三个月前的销售单如果上面只有drug_id显示的可能是调价后的价格这笔账就对不上。把drug_name、specification、sale_price冗余进明细表虽然违反第三范式但这是零售行业的标准做法答辩时主动讲出来能加分。3.2 建库建表与初始化数据的完整SQL脚本-- 建库字符集必须用 utf8mb4utf8 在 MySQL 8 里存生僻字和图标会报错 CREATE DATABASE IF NOT EXISTS pharmacy_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE pharmacy_db; -- 系统用户表 DROP TABLE IF EXISTS sys_user; CREATE TABLE sys_user ( user_id INT AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码演示项目建议存MD5, real_name VARCHAR(50) NOT NULL COMMENT 姓名, role VARCHAR(20) NOT NULL DEFAULT ADMIN COMMENT 角色ADMIN-管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1启用 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT系统用户表; -- 药品分类表 DROP TABLE IF EXISTS drug_category; CREATE TABLE drug_category ( cate_id INT AUTO_INCREMENT PRIMARY KEY, cate_name VARCHAR(50) NOT NULL UNIQUE COMMENT 分类名称 ) ENGINEInnoDB COMMENT药品分类表; -- 药品信息表把库存数量和预警阈值放同一张表查询预警直接比较两个字段 DROP TABLE IF EXISTS drug; CREATE TABLE drug ( drug_id INT AUTO_INCREMENT PRIMARY KEY, drug_code VARCHAR(30) NOT NULL UNIQUE COMMENT 药品编码, drug_name VARCHAR(100) NOT NULL COMMENT 药品名称, cate_id INT NOT NULL COMMENT 分类ID, specification VARCHAR(100) NOT NULL COMMENT 规格如0.25g*36粒, dosage_form VARCHAR(30) NOT NULL COMMENT 剂型片剂/胶囊/注射液, manufacturer VARCHAR(100) COMMENT 生产厂家, produce_date DATE NOT NULL COMMENT 生产日期, expire_date DATE NOT NULL COMMENT 有效期至, purchase_price DECIMAL(10,2) NOT NULL COMMENT 进价, sale_price DECIMAL(10,2) NOT NULL COMMENT 零售价, stock_quantity INT NOT NULL DEFAULT 0 COMMENT 当前库存, warning_threshold INT NOT NULL DEFAULT 20 COMMENT 库存预警阈值, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在售 0停售, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_drug_name (drug_name), CONSTRAINT fk_drug_category FOREIGN KEY (cate_id) REFERENCES drug_category (cate_id) ) ENGINEInnoDB COMMENT药品信息表; -- 供应商表 DROP TABLE IF EXISTS supplier; CREATE TABLE supplier ( supplier_id INT AUTO_INCREMENT PRIMARY KEY, supplier_name VARCHAR(100) NOT NULL COMMENT 供应商名称, contact_person VARCHAR(50) COMMENT 联系人, phone VARCHAR(20) COMMENT 联系电话, address VARCHAR(200) COMMENT 地址, status TINYINT NOT NULL DEFAULT 1 ) ENGINEInnoDB COMMENT供应商表; -- 销售主表 DROP TABLE IF EXISTS sales; CREATE TABLE sales ( sales_id INT AUTO_INCREMENT PRIMARY KEY, sales_no VARCHAR(30) NOT NULL UNIQUE COMMENT 单号如XS20250101001, user_id INT NOT NULL COMMENT 操作员ID, customer_name VARCHAR(50) COMMENT 顾客姓名, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 应收金额, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_create_time (create_time), CONSTRAINT fk_sales_user FOREIGN KEY (user_id) REFERENCES sys_user (user_id) ) ENGINEInnoDB COMMENT销售主表; -- 销售明细表冗余药品名称、规格、单价快照 DROP TABLE IF EXISTS sales_item; CREATE TABLE sales_item ( item_id INT AUTO_INCREMENT PRIMARY KEY, sales_id INT NOT NULL, drug_id INT NOT NULL, drug_name VARCHAR(100) NOT NULL COMMENT 药品名称快照, spec VARCHAR(100) COMMENT 规格快照, unit_price DECIMAL(10,2) NOT NULL COMMENT 成交单价快照, quantity INT NOT NULL COMMENT 数量, amount DECIMAL(10,2) NOT NULL COMMENT 小计金额, CONSTRAINT fk_item_sales FOREIGN KEY (sales_id) REFERENCES sales (sales_id), CONSTRAINT fk_item_drug FOREIGN KEY (drug_id) REFERENCES drug (drug_id) ) ENGINEInnoDB COMMENT销售明细表;初始化数据一定要有而且要精心设计。管理员账号默认admin/123456密码存MD5。药品数据里故意放一批库存低于预警阈值的比如库存只剩10盒、阈值20的布洛芬这样你上讲台打开首页就能直接演示库存预警效果不用现场改数据库。INSERT INTO sys_user (username, password, real_name, role) VALUES (admin, MD5(123456), 系统管理员, ADMIN), (pharmacist, MD5(123456), 值班药师, PHARMACIST); INSERT INTO drug_category (cate_id, cate_name) VALUES (1, 抗生素类), (2, 感冒用药), (3, 心脑血管类); INSERT INTO drug (drug_code, drug_name, cate_id, specification, dosage_form, manufacturer, produce_date, expire_date, purchase_price, sale_price, stock_quantity, warning_threshold) VALUES (DR001, 阿莫西林胶囊, 1, 0.25g*36粒, 胶囊剂, 华北制药, 2024-01-15, 2026-01-14, 8.50, 12.00, 150, 30), (DR002, 布洛芬缓释胶囊, 2, 0.3g*20粒, 胶囊剂, 中美史克, 2024-02-01, 2026-01-31, 15.00, 19.80, 10, 20);注意一点外键约束会拖慢插入速度。但毕设数据量在几千条以内外键带来的完整性收益远大于性能损失所以销售明细表我保留了两条外键。实际生产系统里这类大表一般会去掉数据库外键把完整性校验放到Service层这是答辩时别人问“你有没有考虑过性能”时你能答上来的内容。3.3 外键、索引和字符集数据库脚本里的三个细节字符集和排序规则是脚本里最容易被忽视的地方。MySQL 8.0默认字符集已经是utf8mb4但如果你的脚本在5.7上跑建库时不指定表就会用系统默认的latin1中文全变问号。所以我每次建库都显式写DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci不给环境发挥的空间。索引不用建太多。药品表的drug_name加普通索引销售表的create_time加索引销售单号本身有UNIQUE约束这三处就是SSM项目的够用配置。每个数据表都建五六个索引只会让优化器犯难答辩时反而说不清楚每个索引解决什么问题。我一般会在sale_time这种范围查询字段上建索引因为销售统计的SQL必然会按日期范围过滤。数据库脚本最好再带一个batch_data.sql放批量数据方便扩展演示。比如Excel导入药品库存这种事有数据支撑演示效果完全不同。4. SSM核心源码实现从登录拦截到库存预警的完整代码路径4.1 登录与权限拦截的源码实现登录模块是整个系统演示的第一步也是老师最可能打断你问“权限是怎么控制的”的地方。我在登录上做两层Controller负责收参数、开Session拦截器负责所有后续请求的准入判断。Controller public class LoginController { Autowired private UserService userService; RequestMapping(/login) public String login(String username, String password, HttpSession session) { User user userService.login(username, password); if (user null) { // 登录失败回到登录页并带一个error标记 return redirect:/toLogin?error1; } session.setAttribute(loginUser, user); // 登录成功直接进首页首页会展示药品列表和库存预警 return redirect:/index; } RequestMapping(/logout) public String logout(HttpSession session) { session.invalidate(); return redirect:/toLogin; } }Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override public User login(String username, String password) { // 库里存的是MD5登录时把明文加密后再比对 String encodedPwd DigestUtils.md5DigestAsHex( password.getBytes(StandardCharsets.UTF_8)); return userMapper.selectByUsernameAndPassword(username, encodedPwd); } }密码字段在数据库里存的是MD5后的密文这是毕设级项目的及格线。如果你想让分数再高一点可以在答辩时说“生产环境我不会直接用MD5我会用BCrypt加盐MD5在彩虹表攻击面前就是裸奔。”这一句话就能让老师知道你不只是把代码敲了一遍。拦截器的核心逻辑在preHandle里检查session里有没有用户没有就重定向到登录页。特别注意要排除登录接口和静态资源否则CSS和JS全部被拦死页面只剩一堆无样式的HTML。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); if (session.getAttribute(loginUser) null) { // 未登录的用户一律弹回登录页 response.sendRedirect(request.getContextPath() /toLogin); return false; } return true; } }在spring-mvc.xml里注册拦截器时用mvc:exclude-mapping把/login和/static/**排除掉剩下的/**全部走拦截器。4.2 药品CRUD与MyBatis分页查询的实现药品管理是药房的主线业务增删改查加搜索分页是标配。我在Controller里调ServiceService里调Mapper严格三层。分页插件用PageHelper这是SSM项目最常用的分页方式没有之一。它靠MyBatis拦截器在SQL执行前自动拼接LIMIT你只需要在查询前调用一句PageHelper.startPage。Controller RequestMapping(/drug) public class DrugController { Autowired private DrugService drugService; RequestMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, String keyword, Model model) { PageInfoDrug pageInfo drugService.findPage(pageNum, pageSize, keyword); model.addAttribute(page, pageInfo); model.addAttribute(keyword, keyword); return drug/list; } RequestMapping(/save) ResponseBody public Result save(RequestBody Drug drug) { drugService.save(drug); return Result.success(保存成功); } RequestMapping(/delete) ResponseBody public Result delete(Integer id) { drugService.delete(id); return Result.success(删除成功); } }Service Transactional public class DrugServiceImpl implements DrugService { Autowired private DrugMapper drugMapper; Override public PageInfoDrug findPage(int pageNum, int pageSize, String keyword) { // startPage 必须紧跟Mapper查询中间不能插入任何业务代码 PageHelper.startPage(pageNum, pageSize); ListDrug list drugMapper.selectByKeyword(keyword); return new PageInfo(list); } Override public void save(Drug drug) { if (drug.getDrugId() null) { drugMapper.insert(drug); } else { drugMapper.update(drug); } } Override public void delete(Integer id) { Drug drug drugMapper.selectById(id); if (drug null) { throw new RuntimeException(药品不存在); } // 有销售明细引用的药品不允许直接删除 int refCount drugMapper.countSalesItemByDrugId(id); if (refCount 0) { throw new RuntimeException(该药品存在销售记录不允许删除); } drugMapper.deleteById(id); } }注意delete方法的第二层保护先查引用计数有销售记录的药品直接删会违反外键约束在用户界面上表现为白屏异常。用这个业务判断拦住输出一个友好提示这是操作经验不是书本上教的。对应的Mapper XML里模糊搜索的LIKE拼接不要用“%”keyword%这种Java字符串拼接会有SQL注入风险。MyBatis里推荐用CONCAT函数配合#{}预编译参数select idselectByKeyword parameterTypestring resultTypecom.pharmacy.entity.Drug SELECT drug_id, drug_name, specification, dosage_form, manufacturer, sale_price, stock_quantity, warning_threshold, status FROM drug where if testkeyword ! null and keyword ! drug_name LIKE CONCAT(%, #{keyword}, %) OR drug_code LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY drug_id DESC /select4.3 库存预警和销售统计两个容易被答辩老师追问的点库存预警的本质是两条查询出库存数量小于等于阈值的药品再决定前端怎么展示。查询很直白但这里有个关键参数设计——预警阈值不是全局写死的而是每盒药自己带一个warning_threshold字段同一种药在不同门店可以设置不同阈值。我一般把阈值默认值设为20管理员在药品编辑页面随时调整。select idselectWarningDrugs resultTypecom.pharmacy.entity.Drug SELECT drug_id, drug_name, specification, stock_quantity, warning_threshold FROM drug WHERE status 1 AND stock_quantity lt; warning_threshold ORDER BY stock_quantity ASC /select注意XML里的小于等于号必须写成lt;直接写会导致XML解析报错这是MyBatis新手必踩的坑。销售统计用SQL聚合就够了不需要写存储过程。按天统计销售额遇到日期格式问题时用DATE_FORMAT。这里有个隐藏坑直接用BETWEEN且endDate传当天日期会漏掉当天的数据。正确做法是把endDate加一天或者用date_format(create_time, %Y-%m-%d) endDate。select idselectStatByDay parameterTypemap resultTypecom.pharmacy.entity.SalesStatVO SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS statDate, COUNT(*) AS saleCount, SUM(total_amount) AS totalAmount FROM sales WHERE create_time gt; #{startDate} AND create_time lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY) GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY statDate /select日期范围查询用大于等于开始日期、小于结束日期加一天是日期区间查询的标准左闭右开写法能彻底避免BETWEEN的边界遗漏问题。5. 避坑排查SSM药房系统最常见的6个翻车现场与解决方法5.1 数据库连接失败驱动类、URL和密码三个坑现象Tomcat启动时控制台报错日志里出现“Communications link failure”或“Access denied for user rootlocalhost”。原因MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver很多人还在用老版本的com.mysql.jdbc.DriverURL里没带serverTimezone参数会报时区错误密码里有、#这类特殊字符时properties文件解析会截断。解决驱动类换成cj包URL加上useSSLfalseserverTimezoneAsia/Shanghai密码里的特殊字符用反斜杠转义。顺手把allowPublicKeyRetrievaltrue加上否则MySQL 8默认的caching_sha2_password认证会报Public Key Retrieval错误。5.2 Invalid bound statementMapper XML没打进classpath现象日志报“Invalid bound statement (not found): com.pharmacy.mapper.DrugMapper.selectByKeyword”但接口、XML文件名都对。原因Maven默认不会把src/main/java下的XML文件打进classpath。你把DrugMapper.xml放在和接口同一个包等于编译后运行目录里只有.class没有.xmlMyBatis自然找不到SQL。解决在pom.xml的build节点里把src/main/java下的xml和properties文件加进resourcesspring-mvc的mapperLocations配成classpath:mapper/.xml如果XML按包分散则配classpath:com/pharmacy/mapper/*.xml。5.3 中文乱码连接参数、表字符集和页面编码要一致现象插入数据库的中文变成“???”或者JSP页面上显示一堆乱码。原因三层编码不一致。JDBC连接URL没加characterEncodingutf8建表时用了latin1字符集JSP文件没设置pageEncoding也不声明contentType。解决URL加characterEncodingutf8建库建表统一utf8mb4JSP头部加pageEncodingUTF-8。同时确认Tomcat的server.xml的Connector加了URIEncodingUTF-8这个容易漏GET请求中文参数乱码多半是它。5.4 PageHelper分页错乱startPage必须紧跟Mapper查询现象第一页显示20条数据点第二页还是那20条或者PageInfo.getTotal()返回-1。原因PageHelper.startPage()和Mapper查询语句之间插了其他查询或拦截器代码导致分页线程变量被下一次查询消费多个数据源混用也会让分页服务判错方言。解决startPage()之后立即调Mapper查询中间不允许有任何其他数据库操作。拿到PageInfo后再做数据加工。多数据源场景要把PageHelper的helperDialect在初始化参数里显式指定为mysql否则自动检测可能拿错。5.5 事务不生效该检查的不是方法名而是容器配置现象销售单插入主表成功明细表插入失败但主表数据还是落库了没有回滚。原因Transactional注解写在Controller方法上或者Service内部调用上Spring的AOP代理根本拦不到这一步更隐蔽的是数据源没配DataSourceTransactionManager或者配置里用的dataSource和SqlSessionFactory用的不是同一个对象。解决注解放在Service实现类的public方法上事务管理器dataSource一定ref指向同一个数据源。Spring容器扫描时避免把Controller也扫进applicationContext否则两个容器各管一份Bean事务代理生成混乱。5.6 静态资源404DispatcherServlet拦截范围没放开现象登录页HTML能打开但所有CSS、JS加载失败浏览器F12全是404。原因spring-mvc.xml里把DispatcherServlet映射到“/”所有请求都进了Spring MVC而Spring MVC默认不会处理静态资源默认的DefaultServlet被覆盖掉了。解决spring-mvc.xml里加mvc:resources mapping/static/** location/static//或者改用 mvc:default-servlet-handler/ 交给Tomcat的DefaultServlet处理。前端页面里的资源引用路径建议用${pageContext.request.contextPath}/static/xxx避免部署路径不同导致引用全错。6. 部署验证与答辩加分最后一天还能做的三件事别把部署留到答辩前夜。我第一次带项目的血泪经验是换了一台电脑JDK版本从8变成11Tomcat从8.5换到9项目启动花了整整两个晚上。从那以后我给每个SSM项目都固定一套环境清单。先建一个干净的MySQL实例按顺序执行建库脚本和初始化数据脚本再改jdbc.properties里的账号密码。数据库能连通后用mvn clean package打包把war丢进Tomcat的webapps启动后访问http://localhost:8080/pharmacy/。能出现登录页说明环境链路是通的。答辩演示有一条路线建议先登录打开首页第一眼停在库存预警列表上说清楚阈值逻辑然后演示药品搜索用模糊查询找到“布洛芬”编辑它的库存数让它从预警变成安全库存最后切到销售统计页面展示按日期的柱状图。这三步覆盖了SSM的请求分发、MyBatis动态SQL、事务和聚合统计比从头到尾把每个CRUD都点一遍高效得多。最后两个加分习惯值得临时补上一是Controller统一返回Result对象给前端返回code、msg、data三段式结构而不是裸字段二是加一个全局异常处理器用ControllerAdvice把RuntimeException统一转成友好提示避免答辩现场一点就白屏。这两处改动代码量不大却能让老师看到工程化意识。把日常工作习惯带进毕设项目是我一贯的做法。哪怕只是一个药房管理系统也要让每个方法有日志、每条SQL有参数说明、每次异常有响应。这套SSM项目做完你以后写Spring Boot会发现框架变了但分层思想没变。希望帮到你。本文还有配套的精品资源点击获取
返回列表