简介:本资源是一套基于Java技术栈的完整Web课程设计项目,面向计算机专业本科生及Java初学者,聚焦家电售后服务业务场景的系统化开发实践。系统采用Spring+SpringMVC+JDBC三层架构,后端以MySQL持久化数据,覆盖经理、服务员、仓库管理员、信息管理员四类角色权限与核心业务流程,包括客户报修调度、投诉咨询记录、仓库出入库管理、维修进度跟踪及绩效奖金核算等真实模块。压缩包含573个文件,总计11.02MB,其中JSP页面(44个)支撑前端交互,Java类(28个)与Class字节码(56个)实现业务逻辑,SQL脚本(1个)提供数据库初始化支持,PNG/GIF图片(共336个)用于界面资源,XML与Properties配置文件保障框架集成。目前已有284人学习下载,资源结构清晰、模块职责分明,附带完整可运行代码、数据库脚本及典型Servlet(如RepairServlet、ComplaintServlet)实现范例,适合课程设计参考、SSM框架入门实战与企业级售后系统功能拆解学习。
1. 这不是又一个CRUD练习——它用Spring+JDBC手写事务边界,暴露了真实售后场景里的并发冲突点
家电维修不是下单→发货→签收的线性流程。客户同时报修三台空调、仓库管理员正在清点刚到的压缩机、信息管理员在核算上月维修奖金——四个角色在同一批数据上高频读写,而系统里没有Spring Boot自动装配、没有MyBatis动态SQL、甚至没用Spring声明式事务注解。它用JDBCUtils手动管理Connection,用StorageServlet和RepairServlet直连MySQL,在ComplaintServlet里写conn.setAutoCommit(false)再显式commit/rollback。这种“退化式”设计恰恰卡在企业级Java开发的分水岭:当面试官问“Spring事务失效场景”,你背的@Transactional传播机制,远不如亲手在RepairServlet.doPost()里看到SQLException触发回滚失败来得刻骨。适合刚学完JDBC但还没摸透Spring事务传播行为的开发者,也适合想拆解传统MVC分层如何与数据库事务耦合的中级工程师。
2. 分层结构与核心类职责:从Servlet入口到JDBC连接池的手动控制链
2.1 四类角色权限映射到Servlet路由与DAO方法粒度
系统未采用Spring Security做RBAC,而是通过HTTP请求参数或Session中存储的角色标识(如session.getAttribute("role"))在Servlet中硬编码分支。以RepairServlet为例,其doPost()方法会先校验用户角色:
// RepairServlet.java 片段 String role = (String) request.getSession().getAttribute("role"); if (!"manager".equals(role) && !"server".equals(role)) { response.getWriter().write("权限不足"); return; }提示:这种硬编码角色判断虽不符合现代架构,但正是课程设计的关键教学点——它迫使开发者思考:当
manager能调用所有接口时,RepairServlet和ComplaintServlet是否该复用同一套DAO?查看StorageDao.class发现,其updateStock()方法被StorageServlet和RepairServlet共同调用,但前者传入inQuantity,后者传入repairPartsUsed,参数语义不同却共享同一SQL模板。这暴露了DAO层抽象不足的典型问题:本该按业务域拆分StockDao和RepairPartDao,而非用一个DAO承载所有库存操作。
2.2 JDBCUtils:轻量级连接池的实现逻辑与致命缺陷
JDBCUtils.class是整个系统的数据访问基石,其核心方法getConnection()并非简单DriverManager.getConnection(),而是维护了一个静态LinkedList<Connection>作为简易连接池:
public static Connection getConnection() throws SQLException { if (connections.isEmpty()) { return DriverManager.getConnection(URL, USER, PASSWORD); } return connections.removeFirst(); // FIFO出队 }而closeConnection(Connection conn)则将连接放回队列:
public static void closeConnection(Connection conn) { try { if (conn != null && !conn.isClosed()) { connections.addLast(conn); // FIFO入队 } } catch (SQLException e) { e.printStackTrace(); } }连接池参数表(需手动修改JDBCUtils源码)
| 参数 | 默认值 | 修改位置 | 影响说明 |
|---|---|---|---|
MAX_CONNECTIONS | 5 | JDBCUtils静态常量 | 超过5个并发请求将阻塞,模拟高并发下连接耗尽场景 |
URL | jdbc:mysql://localhost:3306/appliance_service?useSSL=false&serverTimezone=GMT%2B8 | JDBCUtils静态字段 | 必须匹配本地MySQL 8.0+版本,否则报No suitable driver |
USER/PASSWORD | root/123456 | 同上 | 若MySQL启用了密码验证插件(如caching_sha2_password),需在URL后追加&allowPublicKeyRetrieval=true |
注意:该连接池无空闲连接检测、无最大等待时间、无连接有效性校验。当MySQL服务重启后,池中残留的已失效Connection会被
removeFirst()取出,导致后续conn.prepareStatement()抛出SQLException: Connection is closed。修复方案是在getConnection()中增加conn.isValid(2)校验,但课程设计刻意保留此缺陷,用于教学“连接泄漏”的排查路径。
2.3 Servlet-DAO协作模型:为什么RepairServlet要自己处理事务
RepairServlet处理报修单时,需原子性完成三件事:更新客户报修记录、扣减仓库配件库存、生成维修工单。若用Spring声明式事务,只需在Service方法加@Transactional。但本项目中,这些操作分散在不同DAO方法中,且由Servlet直接调用:
// RepairServlet.java 关键片段 Connection conn = null; try { conn = JDBCUtils.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 repairDao.insertRepairRecord(conn, repairInfo); // 插入报修单 storageDao.updateStock(conn, "compressor", -1); // 扣减压缩机库存 workerDao.assignWorker(conn, repairInfo.getOrderId(), "W001"); // 派单给工人 conn.commit(); // 全部成功才提交 } catch (SQLException e) { if (conn != null) { try { conn.rollback(); // 任一失败则回滚 } catch (SQLException rollbackEx) { rollbackEx.printStackTrace(); } } throw e; } finally { JDBCUtils.closeConnection(conn); }这段代码揭示了Spring MVC时代前的真实开发逻辑:事务边界由Servlet控制,而非Service。conn作为参数穿透所有DAO方法,使每个DAO都依赖外部Connection,丧失了独立测试能力。这也是课程设计要求手写JDBCUtils而非直接使用C3P0的核心原因——逼迫开发者直面Connection生命周期管理。
3. MySQL数据库设计与ER关系:从家电维修业务流反推表结构
3.1 核心实体表与外键约束解析
根据摘要描述的“客户投诉处理、仓库物品进出、维修进度查询”三大主线,可逆向还原出以下关键表(字段名基于常见命名习惯及Servlet中SQL语句推断):
| 表名 | 主键 | 外键 | 业务含义 | Servlet关联 |
|---|---|---|---|---|
customer_complaint | complaint_id | — | 客户投诉记录,含投诉内容、时间、处理状态 | ComplaintServlet |
repair_order | order_id | customer_id→customer | 报修单主表,含故障描述、预约时间、分配工人 | RepairServlet |
repair_part_usage | usage_id | order_id→repair_order,part_id→storage_item | 维修消耗配件明细,如“订单A消耗2个电容” | RepairServlet调用StorageDao.updateStock()时关联 |
storage_item | part_id | — | 仓库物品主表,含名称、当前库存、安全库存阈值 | StorageServlet |
worker_bonus | bonus_id | worker_id→worker,order_id→repair_order | 维修奖金记录,按维修结果(如“一次修好”)计算 | InfoAdminServlet |
注意:
repair_part_usage表的存在解释了为何StorageDao.updateStock()需接收partId和quantityChange两个参数——它不直接操作storage_item.quantity,而是通过关联repair_part_usage确保每次扣减都有维修单据溯源,满足财务审计要求。
3.2 关键SQL语句与索引优化建议
StorageServlet执行仓库入库时,典型SQL为:
INSERT INTO storage_item (part_name, quantity, safety_stock) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity);此语句依赖part_name字段的唯一索引。但实际生产中,配件名称可能重复(如“螺丝-标准型”),应改用part_code作为主键并建唯一索引:
ALTER TABLE storage_item ADD COLUMN part_code VARCHAR(20) NOT NULL FIRST; ALTER TABLE storage_item ADD UNIQUE INDEX idx_part_code (part_code);同样,repair_order表中customer_id若未建索引,当经理查询某客户历史维修单时(SELECT * FROM repair_order WHERE customer_id = ?),全表扫描将导致响应延迟。需执行:
CREATE INDEX idx_repair_customer ON repair_order(customer_id);提示:课程设计未提供建表SQL,但
StorageDao.class中insertItem()方法使用INSERT ... ON DUPLICATE KEY UPDATE语法,暗示storage_item表已设唯一约束;而RepairServlet中queryRepairHistory()方法传入customerId作为查询条件,表明该字段必然存在索引需求——这是学生需自行补全的数据库优化点。
3.3 角色权限与数据隔离的SQL实现
系统未用视图或行级权限,而是通过SQL WHERE条件实现数据隔离。例如InfoAdminServlet查询维修奖金时:
// InfoAdminServlet.java String sql = "SELECT b.*, w.worker_name FROM worker_bonus b " + "JOIN worker w ON b.worker_id = w.worker_id " + "WHERE b.status = 'paid' AND w.department = 'repair'";而StorageServlet查询库存时:
// StorageServlet.java String sql = "SELECT * FROM storage_item WHERE quantity < safety_stock";这种“应用层过滤”方式虽简单,但存在隐患:若worker表中department字段被恶意篡改,InfoAdminServlet可能漏查其他部门奖金。更健壮的做法是创建repair_worker_view视图限定部门,但课程设计刻意保留此模式,用于对比讲解“应用层权限”与“数据库层权限”的差异。
4. Spring+SpringMVC配置落地:从web.xml到DispatcherServlet的请求链路
4.1 web.xml中的核心配置与Servlet映射
项目使用传统XML配置而非Spring Boot,web.xml是整个MVC框架的启动入口。关键配置如下:
<!-- web.xml --> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/springmvc-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 静态资源放行 --> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.css</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.js</url-pattern> </servlet-mapping>注意:
<url-pattern>/</url-pattern>表示DispatcherServlet接管所有请求,包括/repair、/storage等。这意味着RepairServlet.class和StorageServlet.class并非由Spring管理,而是作为传统Servlet部署——它们与Spring容器完全解耦,仅共享JDBCUtils连接池。这种混合架构(Spring MVC + 原生Servlet)正是课程设计的难点:学生需理解何时该用@Controller,何时必须用原生Servlet处理事务。
4.2 springmvc-servlet.xml的Bean定义与HandlerMapping
springmvc-servlet.xml中未定义@Controller扫描,而是手动注册Handler:
<!-- springmvc-servlet.xml --> <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="mappings"> <props> <prop key="/repair">repairController</prop> <prop key="/storage">storageController</prop> <prop key="/complaint">complaintController</prop> </props> </property> </bean> <bean id="repairController" class="com.example.controller.RepairController"> <property name="repairService" ref="repairService"/> </bean>但项目正文列出的类均为*Servlet.class(如RepairServlet.class),而非RepairController。这说明实际运行时,web.xml中应存在对应原生Servlet声明:
<servlet> <servlet-name>repairServlet</servlet-name> <servlet-class>com.example.servlet.RepairServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>repairServlet</servlet-name> <url-pattern>/repair</url-pattern> </servlet-mapping>提示:课程设计文档存在表述模糊——标题写“Spring+SpringMVC”,但类文件名全是
*Servlet。真相是:Spring MVC仅用于前端页面路由(如/login.jsp跳转),而核心业务逻辑(报修、库存、投诉)由原生Servlet处理。这种“Spring MVC皮,JDBC骨”的架构,正是理解Java Web演进史的关键切口。
4.3 SpringMVC拦截器实现角色校验
尽管Servlet层有硬编码角色判断,但课程设计要求用Spring拦截器统一校验。LoginInterceptor.java需实现:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); String role = (String) session.getAttribute("role"); String uri = request.getRequestURI(); // 经理可访问所有路径 if ("manager".equals(role)) return true; // 服务员仅允许/review、/complaint等路径 if ("server".equals(role) && uri.startsWith("/review")) return true; if ("server".equals(role) && uri.startsWith("/complaint")) return true; response.sendRedirect(request.getContextPath() + "/unauthorized.jsp"); return false; } }并在springmvc-servlet.xml中注册:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <bean class="com.example.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>注意:此拦截器与
RepairServlet中的角色校验形成双重防护。但若拦截器放行后,RepairServlet仍执行session.getAttribute("role"),说明两者校验逻辑未复用——这是典型的代码冗余,也是重构切入点:将角色校验提取为工具类,供拦截器和Servlet共同调用。
5. 实战调试技巧:定位JDBC连接泄漏与MySQL中文乱码
5.1 三步定位Connection未关闭的泄漏点
当系统运行一段时间后出现Cannot create PoolableConnectionFactory错误,表明连接池耗尽。按以下顺序排查:
检查JDBCUtils.closeConnection()调用点
全局搜索JDBCUtils.closeConnection(,确认每个getConnection()调用后均有finally块执行closeConnection()。特别注意ComplaintServlet中异常分支是否遗漏:// 错误示例:缺少finally Connection conn = JDBCUtils.getConnection(); complaintDao.insert(conn, complaint); // 可能抛异常 // 忘记closeConnection(conn)!在JDBCUtils中添加连接追踪日志
修改getConnection()和closeConnection(),打印调用栈:public static Connection getConnection() throws SQLException { Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); System.out.println("GET CONNECTION: " + Thread.currentThread().getStackTrace()[2]); return conn; }启用MySQL连接数监控
登录MySQL执行:SHOW STATUS LIKE 'Threads_connected'; -- 查看当前连接数 SHOW PROCESSLIST; -- 查看活跃连接及对应SQL若发现大量
Sleep状态连接且Command为Sleep,说明应用端未关闭连接。
5.2 解决MySQL 8.0中文乱码的完整配置链
课程设计默认字符集为latin1,导致插入中文报修内容时显示??。需同步修改四层配置:
| 层级 | 配置位置 | 关键参数 | 示例值 |
|---|---|---|---|
| MySQL服务端 | my.cnf | [mysqld]下character-set-server=utf8mb4 | character-set-server=utf8mb4 |
| 客户端 | my.cnf | [client]下default-character-set=utf8mb4 | default-character-set=utf8mb4 |
| JDBC URL | JDBCUtils.java | URL参数characterEncoding=utf8mb4 | jdbc:mysql://...?characterEncoding=utf8mb4&serverTimezone=GMT%2B8 |
| 表结构 | MySQL命令 | CREATE TABLE时指定CHARSET=utf8mb4 | CREATE TABLE repair_order (...) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
验证方法:在MySQL命令行执行
SHOW VARIABLES LIKE 'character_set%';,确认character_set_client、character_set_connection、character_set_database均为utf8mb4;然后在RepairServlet中插入含中文的报修单,用SELECT HEX(content)查看十六进制值,E4B8AD对应“中”,证明编码正确。
5.3 快速验证SpringMVC与Servlet共存是否生效
部署后访问http://localhost:8080/repair,若返回404,按顺序检查:
web.xml中<servlet-mapping>的<url-pattern>是否为/repair(非/repair/*)RepairServlet.class是否在WEB-INF/classes/com/example/servlet/路径下RepairServlet的doGet()或doPost()中是否有response.getWriter().write("test");测试输出- 若通过Spring MVC访问
/repair,需确认springmvc-servlet.xml中SimpleUrlHandlerMapping的key是否为/repair
最简验证法:在RepairServlet构造函数中添加System.out.println("RepairServlet loaded");,启动Tomcat观察控制台输出。无输出则说明Servlet未被加载,问题必在web.xml配置。
本文还有配套的精品资源,点击获取