资讯动态

Struts销售管理系统实战:从订单到进销存后台的完整实现

发布时间:2026/10/20 0:14:19 来源:尧图企业网站定制
简介这是一套基于Struts框架构建的企业级销售管理系统源码面向Java Web开发者与课程设计学习者用于掌握MVC模式下的业务系统开发。系统围绕商品进出货管理与客户管理两大核心模块展开涵盖入库出库记录、库存查询与盘点、客户信息维护、客户分类评级及售后服务等典型销售流程功能并延伸至销售报表与促销管理。压缩包共298个文件约2.05MB以94个JSP页面、48个Java源文件与48个class文件为主体辅以35个JS脚本、18个CSS与17个PNG等前端资源另有少量XML配置与jar依赖结构完整、层次清晰。已有259人学习下载。读者可借此理解Action处理业务逻辑、JSP展示数据、控制器协调模型与视图的完整链路并参考Struts标签库、国际化与本地化等实现方式快速搭建或扩展自己的销售管理项目。1. 销售管理系统从一堆散落的订单到能跑起来的进销存后台很多开发者第一次接触 Struts 框架都是被一个叫「销售管理系统」的练手项目带进来的。它不像电商前台那样花哨核心就干一件事把客户、商品、订单、库存这几张表串成一条能走通的业务线。客户下单系统扣库存、生成订单、记录回款管理员在后台看报表。听起来简单但真要把增删改查、分页、权限过滤、事务一致性都塞进一个 Struts 的 Action 里坑比想象中多。这份资源适合两类人一是刚学完 Servlet/JSP 想找个完整项目练手的二是需要一套传统 SSH 架构参考实现来改造老系统的。它用的是经典的 Struts 数据库直连模式没有过度封装反而让每一层调用都看得见摸得着。2. 技术选型与目录结构为什么这套老架构还值得拆2.1 技术栈拆解Struts 负责什么不负责什么这个项目的技术栈非常典型属于「SSH 简化版」——保留了 Struts 的 MVC 调度去掉了 Spring 的依赖注入和 Hibernate 的 ORM 映射直接用 JDBC 或轻量 DAO 操作数据库。这么选的原因很实际练手项目如果一上来就堆框架光配置就劝退一半人。Struts 在这里承担的是前端控制器角色所有.do或.action请求先过ActionServlet再按struts-config.xml里的映射分发到具体 Action。Action 里调 ServiceService 调 DAODAO 拿Connection执行 SQL结果封装成ActionForm或DTO回传到 JSP。常见做法是struts-config.xml里配action-mappings每个业务动作对应一个action节点type指向 Action 全限定名name绑定 ActionFormscope决定表单数据存 request 还是 session。参数校验用validate属性开Validator框架或者直接在 Action 的execute里手写 if-else。我一般会建议新手先跑通手写校验的版本再去看 Validator 配置不然报错信息都找不到在哪。数据库层没有用连接池的话就是每次DriverManager.getConnection()这在并发稍高的场景下会直接崩。所以项目里如果带了db.properties和简单的连接池工具类优先用那个。事务控制基本靠Connection.setAutoCommit(false)手动提交回滚Service 方法里一个 try-catch 包住多条 DAO 调用。2.2 目录结构每个包放什么改代码先找哪里一个标准的 Struts 销售管理系统目录大致长这样src/ com/ demo/ action/ # 所有 Action 类按模块分子包 form/ # ActionForm字段与页面表单对应 service/ # 业务逻辑事务边界在这层 dao/ # 数据库操作一个表一个 DAO util/ # 连接池、日期工具、分页工具 vo/ # 值对象对应数据库表字段 WebRoot/ WEB-INF/ web.xml # Struts 入口配置 struts-config.xml jsp/ # 页面文件按模块分目录 css/ js/改代码的路径很清晰页面表单字段变了先改form包里的 ActionForm再改struts-config.xml的name绑定然后看 Action 里怎么取。要加一个查询条件从 JSP 的html:text开始到 ActionForm 加属性到 Action 里组装查询参数再到 DAO 的 SQL 拼 where 子句。这条链路走一遍Struts 的「配置驱动」就理解透了。注意struts-config.xml修改后必须重启 Tomcat热部署不一定生效尤其是 action 映射变更。2.3 核心配置文件web.xml 和 struts-config.xml 怎么读web.xml里最关键的是ActionServlet的servlet和servlet-mapping通常映射*.do。还有struts-config的init-param指向配置文件路径。如果项目用了 Struts 的标签库还要配taglib指向struts-bean.tld、struts-html.tld等。!-- web.xml 片段Struts 入口 -- servlet servlet-nameaction/servlet-name servlet-classorg.apache.struts.action.ActionServlet/servlet-class init-param param-nameconfig/param-name param-value/WEB-INF/struts-config.xml/param-value /init-param load-on-startup2/load-on-startup /servlet servlet-mapping servlet-nameaction/servlet-name url-pattern*.do/url-pattern /servlet-mappingstruts-config.xml里>!-- struts-config.xml 片段一个订单查询动作 -- action-mappings action path/orderList typecom.demo.action.OrderAction nameorderForm scoperequest validatefalse forward namelist path/WEB-INF/jsp/order/list.jsp/ forward nameerror path/WEB-INF/jsp/common/error.jsp/ /action /action-mappingspath是浏览器访问的路径type是处理类name对应 ActionForm 在 request 里的 keyforward定义执行完跳哪。validatefalse表示不走 Validator 框架校验逻辑在 Action 里手写。新手常犯的错是name写错导致 ActionForm 取不到页面报NullPointerException。3. 核心模块拆解一个订单从提交到入库的完整链路3.1 ActionForm表单数据的第一个落脚点ActionForm 本质是一个 JavaBean字段名和 JSP 表单里的name属性一一对应。Struts 在调用 Action 之前会自动把请求参数 set 到 ActionForm 实例里。如果字段类型是int或Date而页面传了空字符串就会抛NumberFormatException或ParseException。所以常见做法是ActionForm 里所有字段先用String接在 Action 里再手动转。// OrderForm.java订单表单字段与页面输入框对应 public class OrderForm extends ActionForm { private String orderId; // 订单编号页面隐藏域 private String customerId; // 客户ID下拉框 private String productId; // 商品ID private String quantity; // 数量页面文本框 private String orderDate; // 下单日期格式 yyyy-MM-dd // getter/setter 省略 }这里把quantity定义成String而不是int就是为了避免空值转换异常。在 Action 里用Integer.parseInt()并包 try-catch出错就返回错误提示。orderDate同理用SimpleDateFormat解析格式不匹配就捕获异常。这种「宽进严出」的写法在 Struts 项目里很常见因为 Struts 的类型转换器配置起来比较繁琐不如手动控制。3.2 Action业务调度的中枢也是事务的起点Action 的execute方法是整个请求的核心。它拿到 ActionForm做参数校验调 Service根据结果返回ActionForward。事务一般在这一层开因为一个业务动作可能涉及多张表的写操作。// OrderAction.java处理订单提交 public class OrderAction extends Action { private OrderService orderService new OrderService(); public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { OrderForm orderForm (OrderForm) form; Connection conn null; try { // 1. 参数校验 int quantity Integer.parseInt(orderForm.getQuantity()); if (quantity 0) { request.setAttribute(msg, 数量必须大于0); return mapping.findForward(error); } // 2. 获取连接并开启事务 conn DBUtil.getConnection(); conn.setAutoCommit(false); // 3. 调 Service 完成下单 orderService.createOrder(conn, orderForm.getCustomerId(), orderForm.getProductId(), quantity); conn.commit(); return mapping.findForward(list); } catch (Exception e) { // 4. 回滚并记录日志 if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute(msg, 下单失败 e.getMessage()); return mapping.findForward(error); } finally { DBUtil.close(conn); } } }这段代码有几个关键点conn.setAutoCommit(false)开启事务后Service 里所有 DAO 操作必须用同一个conn否则事务不生效。orderService.createOrder内部通常做两件事插入订单主表、更新商品库存。如果库存不足抛自定义异常Action 捕获后回滚。DBUtil.close(conn)要确保连接归还不然连接池很快耗尽。参数说明mapping.findForward(list)对应struts-config.xml里配的forward namelist跳转到订单列表页。request.setAttribute(msg, ...)在 JSP 里用bean:write namemsg/或 EL 表达式显示。3.3 Service 与 DAO事务边界和 SQL 拼装Service 层是事务的边界一个方法对应一个完整业务动作。DAO 层只负责单表操作不控制事务。// OrderService.java创建订单包含库存扣减 public class OrderService { private OrderDao orderDao new OrderDao(); private ProductDao productDao new ProductDao(); public void createOrder(Connection conn, String customerId, String productId, int quantity) throws Exception { // 1. 检查库存 int stock productDao.getStock(conn, productId); if (stock quantity) { throw new Exception(库存不足当前库存 stock); } // 2. 扣减库存 productDao.reduceStock(conn, productId, quantity); // 3. 插入订单 orderDao.insert(conn, customerId, productId, quantity, new Date()); } }createOrder方法接收外部传入的conn而不是自己创建这样事务才能跨 DAO 生效。productDao.getStock和reduceStock用SELECT ... FOR UPDATE锁行防止并发超卖。SQL 写法-- 查询库存并加行锁 SELECT stock FROM product WHERE id ? FOR UPDATE; -- 扣减库存 UPDATE product SET stock stock - ? WHERE id ?; -- 插入订单 INSERT INTO orders (customer_id, product_id, quantity, order_date) VALUES (?, ?, ?, ?);FOR UPDATE在 MySQL InnoDB 下生效但必须在事务里才有意义。如果项目用的是其他数据库语法可能不同。DAO 里用PreparedStatement预编译避免 SQL 注入。orderDao.insert里如果用了自增主键可以用Statement.RETURN_GENERATED_KEYS拿回订单 ID。3.4 JSP 页面数据展示与 Struts 标签库JSP 层用 Struts 标签库渲染数据logic:iterate遍历列表bean:write输出字段。分页通常用logic:present判断页码拼?pageNo参数。!-- order/list.jsp订单列表 -- logic:iterate idorder nameorderList tr tdbean:write nameorder propertyorderId//td tdbean:write nameorder propertycustomerName//td tdbean:write nameorder propertyproductName//td tdbean:write nameorder propertyquantity//td tdbean:write nameorder propertyorderDate formatyyyy-MM-dd//td /tr /logic:iteratenameorderList对应 request 里的属性名property对应 VO 的字段。format属性可以直接格式化日期省去在 Action 里转字符串。如果列表为空用logic:empty显示「暂无数据」。分页链接用html:link page/orderList.do?pageNo${pageNo1}注意 Struts 的html:link会自动拼 session id如果不需要可以配rewrite属性。4. 跑起来环境搭建、数据库初始化与启动验证4.1 环境准备JDK、Tomcat、数据库版本匹配这个项目对版本比较敏感。Struts 1.x 需要 JDK 1.6 或 1.7Tomcat 6 或 7 最稳。JDK 8 也能跑但有些老版本的commons-*包会报UnsupportedClassVersionError。数据库用 MySQL 5.5 或 5.7字符集设utf8mb4不然中文商品名会乱码。常见做法是先确认java -version和tomcat的JAVA_HOME指向一致。如果 Tomcat 启动时报ClassNotFoundException: org.apache.struts.action.ActionServlet说明struts.jar没放进WEB-INF/lib。依赖包一般包括struts.jar、commons-beanutils.jar、commons-digester.jar、commons-logging.jar、commons-validator.jar、antlr.jar、oro.jar。少一个都可能启动失败。4.2 数据库初始化建表 SQL 和初始数据项目一般会带一个db.sql文件里面是建表语句和几条测试数据。执行顺序先建库CREATE DATABASE sales_db DEFAULT CHARSET utf8mb4;再USE sales_db;然后跑建表。-- 客户表 CREATE TABLE customer ( id VARCHAR(32) PRIMARY KEY, name VARCHAR(64) NOT NULL, phone VARCHAR(20), address VARCHAR(128) ); -- 商品表 CREATE TABLE product ( id VARCHAR(32) PRIMARY KEY, name VARCHAR(64) NOT NULL, price DECIMAL(10,2), stock INT DEFAULT 0 ); -- 订单表 CREATE TABLE orders ( id VARCHAR(32) PRIMARY KEY, customer_id VARCHAR(32), product_id VARCHAR(32), quantity INT, order_date DATETIME, FOREIGN KEY (customer_id) REFERENCES customer(id), FOREIGN KEY (product_id) REFERENCES product(id) );初始数据至少插两条客户、两条商品库存给 100 左右方便测试下单。如果外键约束导致插入失败先插主表再插从表。orders表名用复数是因为order是 SQL 关键字直接写order会报语法错误。4.3 配置修改与启动db.properties 和 Tomcat 部署src下一般有db.properties配数据库连接信息jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/sales_db?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.password123456改完编译把WebRoot整个目录复制到 Tomcat 的webapps下或者用 Eclipse/IDEA 配 Tomcat 服务器。启动后访问http://localhost:8080/sales/如果看到登录页或订单列表说明 Struts 配置生效。如果 404检查web.xml的url-pattern和访问路径是否匹配。如果 500看 Tomcat 日志里struts-config.xml的解析错误通常是action的type类名写错或form的name没定义。提示第一次跑先别改代码用项目自带的 SQL 和配置跑通再动。不然出了问题分不清是环境还是代码。5. 避坑与排查那些让新手卡半天的报错5.1 中文乱码从页面到数据库的三层编码现象页面输入中文商品名保存后数据库里是???或者列表显示乱码。原因JSP 页面pageEncoding、请求体编码、数据库连接编码三者不一致。解决JSP 头部加% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %在web.xml里配CharacterEncodingFilter或者自己写一个 Filter 设request.setCharacterEncoding(UTF-8)db.properties的 URL 加useUnicodetruecharacterEncodingutf8数据库和表都设utf8mb4。四层都对齐乱码基本消失。5.2 ActionForm 取不到值name 属性与 scope 不匹配现象Action 里(OrderForm) form拿到的是空对象所有字段都是 null。原因struts-config.xml里action的name和 JSP 里html:form action/orderSave的name不一致或者scoperequest但表单提交后跳转到了另一个请求。解决确保action nameorderForm和html:form nameorderForm完全一致如果用了forward而不是redirectrequest 里的 form 还在但如果是redirectform 就丢了。下单成功后一般用forward到列表页列表页重新查数据不依赖 form。5.3 事务不回滚Connection 不是同一个现象库存扣了订单没插进去或者反过来数据不一致。原因Service 里两个 DAO 各自从连接池拿了不同的Connection事务只在一个连接上生效。解决Action 里拿一个conn通过参数传给 ServiceService 再传给 DAO。所有 DAO 方法签名都带Connection conn。如果用了ThreadLocal绑定连接要确保DBUtil.getConnection()返回的是同一个。排查时在conn.commit()前打印conn的 hashcode看两个 DAO 里是不是同一个。5.4 分页查询 count 和 list 不一致现象列表显示 10 条但分页总页数算出来是 0 或负数。原因SELECT COUNT(*)的 where 条件和SELECT *的 where 条件不一致或者pageSize传了 0。解决把 where 条件抽成一个String whereSqlcount 和 list 共用。pageSize在 Action 里做默认值如果pageNo小于 1 就设 1大于总页数就设总页数。常见做法是写一个PageBean类封装pageNo、pageSize、totalCount、totalPage、listAction 里只负责填数据。5.5 连接池耗尽Connection 没关现象跑一段时间后报Cannot get a connection, pool exhausted。原因DAO 里Connection在异常分支没关或者ResultSet、PreparedStatement没关。解决用finally块统一关顺序是ResultSet→PreparedStatement→Connection。如果用了连接池close()是归还而不是真关。排查时在DBUtil.close里加日志看每次请求后活跃连接数有没有降下来。6. 二次开发从能跑到能改几个实用的改造方向6.1 加一个「客户回款」模块照着订单模块抄一遍二次开发最稳的路径是复制现有模块。比如要加客户回款记录先建表payment字段包括id、customer_id、amount、pay_date。然后复制OrderForm改成PaymentForm复制OrderAction改成PaymentAction复制OrderDao改成PaymentDao。在struts-config.xml里加action path/paymentList ...和action path/paymentSave ...。JSP 页面复制order/list.jsp和order/edit.jsp改字段名。这样一套下来Struts 的配置驱动就彻底摸清了。改造时注意新 Action 的name不能和已有的重复path也不能冲突。如果两个模块共用同一个 ActionForm字段会互相干扰。我一般会一个模块一个 Form哪怕字段有重复也保持独立后期改起来不牵连。6.2 把 JDBC 换成连接池DBCP 或 C3P0 的最小改动项目如果现在是DriverManager直连并发一高就崩。最小改动是引入 DBCP加commons-dbcp.jar和commons-pool.jar在db.properties里加连接池参数改DBUtil用BasicDataSource。// DBUtil.java用 DBCP 替换直连 private static BasicDataSource dataSource new BasicDataSource(); static { dataSource.setDriverClassName(com.mysql.jdbc.Driver); dataSource.setUrl(jdbc:mysql://localhost:3306/sales_db?useUnicodetruecharacterEncodingutf8); dataSource.setUsername(root); dataSource.setPassword(123456); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxActive(20); // 最大活跃连接 dataSource.setMaxWait(3000); // 获取连接超时毫秒 } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }setMaxActive别设太大数据库端max_connections默认 151设 20 到 50 比较稳。setMaxWait是拿不到连接时的等待时间超时抛异常避免线程一直挂起。改完后压测一下看连接数是否稳定。6.3 验证改造是否成功三个必查点改完代码别急着上线先跑三个检查。第一下单流程走一遍看订单表和库存表数据是否一致事务有没有生效。第二并发开 10 个线程同时下单同一商品看库存有没有超卖FOR UPDATE有没有起作用。第三看 Tomcat 日志有没有SQLException或NullPointerException尤其是连接池相关。我一般会在DBUtil.close里加一行System.out.println(active: dataSource.getNumActive())跑完压测看活跃连接是否归零。从那以后我每次改完 DAO 层都强制走一遍「下单-查库存-看日志」三步不跳过。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑