基于JSP+Servlet的商品销售统计与管理系统 - 源码深度解析

JavaJavaScriptHTMLCSSMySQLJSP+Servlet
2026-03-174 浏览

文章摘要

本项目是一款基于JSP与Servlet技术栈构建的商品销售统计与管理系统,旨在为中小型零售企业或个体商户提供一体化的商品信息管理与销售数据分析解决方案。其核心业务价值在于解决了传统手工记录或Excel表格管理方式下,商品信息更新不及时、销售数据汇总困难、无法快速生成可视化统计报表等核心痛点。系统通过...

在传统零售业务运营中,商品信息与销售数据的管理长期依赖于手工台账或分散的电子表格。这种方式不仅效率低下,且极易因人为操作导致数据不一致、统计滞后,难以支撑快速的市场决策。面对这一普遍痛点,一套集成了商品管理、库存控制与销售分析功能的自动化系统成为中小型商户的迫切需求。本系统正是基于JSP与Servlet这一经典Java Web技术组合,构建的一个轻量级、高内聚的解决方案,旨在将日常进销存业务全面数字化,并通过数据可视化帮助管理者洞察经营状况。

系统采用标准的Model-View-Controller架构模式,实现了业务逻辑、数据与表现层的清晰分离。View层由JSP页面构成,负责渲染用户界面并展示数据,其内部通过JSTL标签库与EL表达式极大地简化了Java代码的嵌入,提升了页面的可维护性。Controller层的核心是Servlet,它作为请求的统一入口,负责解析用户输入、调用相应的业务逻辑处理,并最终转发至合适的JSP视图。Model层则以JavaBean实体类和数据访问对象为核心,封装了业务数据模型与持久化操作。后端通过JDBC直接连接MySQL数据库,并利用DBUtils工具类对数据库连接与结果集处理进行了有效封装,确保了数据操作的效率与可靠性。

数据库架构设计与核心表分析

系统的数据模型围绕商品、库存、销售单据等核心业务实体构建,共设计有六张核心数据表。其设计遵循了第三范式,以减少数据冗余,并建立了恰当的外键关联以保证数据的完整性与一致性。

1. 商品信息表 product

商品表是整个系统的数据基石,其设计需要兼顾信息的完备性与查询效率。

CREATE TABLE `product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL COMMENT '商品名称',
  `specs` varchar(100) DEFAULT NULL COMMENT '规格型号',
  `supplier_id` int(11) NOT NULL COMMENT '供应商ID',
  `purchase_price` decimal(10,2) NOT NULL COMMENT '采购价',
  `retail_price` decimal(10,2) NOT NULL COMMENT '零售价',
  `stock_quantity` int(11) NOT NULL DEFAULT '0' COMMENT '当前库存',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_supplier_id` (`supplier_id`),
  KEY `idx_name` (`name`),
  CONSTRAINT `fk_product_supplier` FOREIGN KEY (`supplier_id`) REFERENCES `supplier` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品信息表';

设计亮点分析:

  • 价格字段精度purchase_priceretail_price字段采用DECIMAL(10,2)类型,精确到分,完全符合金融计算要求,避免了浮点数精度丢失问题。
  • 库存字段冗余设计stock_quantity字段作为当前库存的实时快照。这是一种典型的反范式设计,通过空间换时间,使得查询当前库存无需关联计算所有出入库记录,极大提升了高频查询操作的性能。
  • 自动化时间戳create_timeupdate_time字段利用MySQL的特性自动管理,确保了数据生命周期的可追溯性,无需在业务代码中手动设置。
  • 索引策略:除了主键索引,还为supplier_idname字段建立了索引。前者加速了按供应商筛选商品的操作,后者则优化了基于商品名称的模糊查询性能。

2. 销售出库单表 outbound_order

销售单表记录了每一笔销售交易的核心信息,是进行销售统计分析的直接数据来源。

CREATE TABLE `outbound_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_number` varchar(50) NOT NULL COMMENT '出库单号',
  `product_id` int(11) NOT NULL COMMENT '商品ID',
  `quantity` int(11) NOT NULL COMMENT '销售数量',
  `sale_price` decimal(10,2) NOT NULL COMMENT '销售单价',
  `total_amount` decimal(10,2) NOT NULL COMMENT '总金额',
  `operator_id` int(11) NOT NULL COMMENT '操作员(用户ID)',
  `sale_time` datetime NOT NULL COMMENT '销售时间',
  `notes` varchar(200) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_number` (`order_number`),
  KEY `idx_product_id` (`product_id`),
  KEY `idx_sale_time` (`sale_time`),
  CONSTRAINT `fk_outbound_product` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`),
  CONSTRAINT `fk_outbound_user` FOREIGN KEY (`operator_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='销售出库单表';

设计亮点分析:

  • 单号唯一性约束order_number字段设置了唯一索引uk_order_number,确保了每一张销售单的唯一性,防止重复录入。
  • 金额反范式设计:尽管total_amount可以通过quantity * sale_price计算得出,但将其作为独立字段存储。这同样是出于性能考虑,在生成销售报表进行汇总时,直接对total_amount进行SUM操作比实时计算要高效得多,也避免了计算逻辑变更带来的历史数据不一致风险。
  • 时间戳索引sale_time字段的索引对于按日、按月或按任意时间区间进行销售统计至关重要,可以快速定位到特定时间段内的所有销售记录。

核心功能模块深度解析

1. 商品管理与库存更新机制

商品管理模块提供了对商品信息的全生命周期管理,包括新增、编辑、查询和删除。其核心在于如何保证库存数据的准确性。

后台Servlet控制器 (ProductManageServlet)

@WebServlet("/admin/productManage")
public class ProductManageServlet extends HttpServlet {
    private ProductService productService = new ProductServiceImpl();

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String action = request.getParameter("action");
        String message = "";
        try {
            if ("add".equals(action)) {
                Product product = this.createProductFromRequest(request);
                boolean success = productService.addProduct(product);
                message = success ? "商品添加成功!" : "商品添加失败!";
            } else if ("update".equals(action)) {
                Product product = this.createProductFromRequest(request);
                product.setId(Integer.parseInt(request.getParameter("id")));
                boolean success = productService.updateProduct(product);
                message = success ? "商品信息更新成功!" : "商品信息更新失败!";
            } else if ("delete".equals(action)) {
                int productId = Integer.parseInt(request.getParameter("id"));
                boolean success = productService.deleteProductById(productId);
                message = success ? "商品删除成功!" : "商品删除失败!";
            }
            request.getSession().setAttribute("message", message);
        } catch (Exception e) {
            e.printStackTrace();
            request.getSession().setAttribute("message", "操作失败:" + e.getMessage());
        }
        response.sendRedirect("productManage.jsp");
    }

    private Product createProductFromRequest(HttpServletRequest request) {
        Product product = new Product();
        product.setName(request.getParameter("name"));
        product.setSpecs(request.getParameter("specs"));
        product.setSupplierId(Integer.parseInt(request.getParameter("supplierId")));
        product.setPurchasePrice(new BigDecimal(request.getParameter("purchasePrice")));
        product.setRetailPrice(new BigDecimal(request.getParameter("retailPrice")));
        // stock_quantity 通常在入库时更新,此处不设置
        return product;
    }
}

库存更新服务 (ProductServiceImpl)

库存的更新并非在商品管理界面直接修改,而是通过独立的入库(InboundOrder)和出库(OutboundOrder)操作,由系统自动计算并更新product表的stock_quantity字段。这是一种事务性更强的设计。

public class ProductServiceImpl implements ProductService {
    private ProductDao productDao = new ProductDaoImpl();

    @Override
    public boolean updateProductStock(int productId, int changeQuantity) throws SQLException {
        Connection conn = null;
        try {
            conn = DBUtil.getConnection();
            conn.setAutoCommit(false); // 开启事务

            // 1. 查询当前库存
            Product product = productDao.findById(conn, productId);
            if (product == null) {
                throw new SQLException("商品不存在");
            }
            int newStock = product.getStockQuantity() + changeQuantity;
            if (newStock < 0) {
                throw new SQLException("库存不足,当前库存:" + product.getStockQuantity());
            }

            // 2. 更新库存
            boolean success = productDao.updateStock(conn, productId, newStock);
            if (!success) {
                conn.rollback();
                return false;
            }
            conn.commit(); // 提交事务
            return true;
        } catch (SQLException e) {
            if (conn != null) conn.rollback();
            throw e;
        } finally {
            DBUtil.closeConnection(conn);
        }
    }
}

商品管理界面 商品管理界面,支持对商品信息的增删改查和按条件筛选。

2. 销售出库与事务一致性处理

销售出库是系统的核心业务流程,它涉及销售记录的创建和商品库存的扣减,必须在一个数据库事务中完成,以确保数据一致性。

销售出库Servlet (SaleOutboundServlet)

@WebServlet("/user/saleOutbound")
public class SaleOutboundServlet extends HttpServlet {
    private OutboundService outboundService = new OutboundServiceImpl();

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String productIdStr = request.getParameter("productId");
        String quantityStr = request.getParameter("quantity");
        String salePriceStr = request.getParameter("salePrice");
        HttpSession session = request.getSession();
        User operator = (User) session.getAttribute("user");

        try {
            int productId = Integer.parseInt(productIdStr);
            int quantity = Integer.parseInt(quantityStr);
            BigDecimal salePrice = new BigDecimal(salePriceStr);

            // 生成唯一的出库单号
            String orderNumber = "SO" + System.currentTimeMillis();

            // 创建出库单对象
            OutboundOrder order = new OutboundOrder();
            order.setOrderNumber(orderNumber);
            order.setProductId(productId);
            order.setQuantity(quantity);
            order.setSalePrice(salePrice);
            order.setTotalAmount(salePrice.multiply(new BigDecimal(quantity)));
            order.setOperatorId(operator.getId());
            order.setSaleTime(new Date());

            // 调用服务层完成出库(包含事务)
            boolean success = outboundService.executeSaleOutbound(order);
            if (success) {
                session.setAttribute("message", "销售出库成功!单号:" + orderNumber);
            } else {
                session.setAttribute("message", "销售出库失败!");
            }
        } catch (Exception e) {
            e.printStackTrace();
            session.setAttribute("message", "系统错误:" + e.getMessage());
        }
        response.sendRedirect("outbound.jsp");
    }
}

服务层事务控制 (OutboundServiceImpl)

服务层的方法封装了完整的业务逻辑,并管理数据库事务的边界。

public class OutboundServiceImpl implements OutboundService {
    private OutboundDao outboundDao = new OutboundDaoImpl();
    private ProductDao productDao = new ProductDaoImpl();

    @Override
    public boolean executeSaleOutbound(OutboundOrder order) throws SQLException {
        Connection conn = null;
        try {
            conn = DBUtil.getConnection();
            conn.setAutoCommit(false); // 开启事务

            // 1. 插入销售出库记录
            boolean addSuccess = outboundDao.addOutboundOrder(conn, order);
            if (!addSuccess) {
                conn.rollback();
                return false;
            }

            // 2. 扣减商品库存 (changeQuantity为负数,例如 -5)
            boolean updateSuccess = productDao.updateStock(conn, order.getProductId(), -order.getQuantity());
            if (!updateSuccess) {
                conn.rollback();
                return false;
            }

            conn.commit(); // 提交事务
            return true;
        } catch (SQLException e) {
            if (conn != null) conn.rollback();
            throw e;
        } finally {
            DBUtil.closeConnection(conn);
        }
    }
}

销售出库管理 销售出库界面,用户选择商品、输入数量后,系统自动计算金额并完成出库。

3. 销售统计与数据可视化

销售统计功能通过对outbound_order表进行聚合查询,生成各类报表,并以图表形式直观展示。

统计查询Servlet (SalesStatisticsServlet)

@WebServlet("/admin/salesStatistics")
public class SalesStatisticsServlet extends HttpServlet {
    private StatisticsService statsService = new StatisticsServiceImpl();

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String type = request.getParameter("type"); // daily, monthly, byProduct
        String startDate = request.getParameter("startDate");
        String endDate = request.getParameter("endDate");

        try {
            List<SalesStats> statsList = null;
            if ("daily".equals(type)) {
                statsList = statsService.getDailySalesStats(startDate, endDate);
            } else if ("byProduct".equals(type)) {
                statsList = statsService.getSalesStatsByProduct(startDate, endDate);
            }

            // 将数据转换为JSON格式,供前端图表库(如ECharts)使用
            Gson gson = new Gson();
            String jsonData = gson.toJson(statsList);
            request.setAttribute("chartData", jsonData);
            request.setAttribute("statsList", statsList);
        } catch (SQLException e) {
            e.printStackTrace();
            request.setAttribute("message", "获取统计数据失败");
        }
        request.getRequestDispatcher("/admin/sales_statistics.jsp").forward(request, response);
    }
}

数据访问层统计查询 (StatisticsDaoImpl)

public class StatisticsDaoImpl implements StatisticsDao {
    @Override
    public List<SalesStats> getDailySalesStats(Connection conn, String startDate, String endDate) throws SQLException {
        String sql = "SELECT DATE(sale_time) as stat_date, " +
                     "SUM(quantity) as total_quantity, " +
                     "SUM(total_amount) as total_amount " +
                     "FROM outbound_order " +
                     "WHERE sale_time BETWEEN ? AND ? " +
                     "GROUP BY DATE(sale_time) " +
                     "ORDER BY stat_date ASC";
        return DBUtil.query(conn, sql, new BeanListHandler<>(SalesStats.class), startDate, endDate);
    }

    @Override
    public List<SalesStats> getSalesStatsByProduct(Connection conn, String startDate, String endDate) throws SQLException {
        String sql = "SELECT p.name as product_name, " +
                     "SUM(o.quantity) as total_quantity, " +
                     "SUM(o.total_amount) as total_amount " +
                     "FROM outbound_order o " +
                     "INNER JOIN product p ON o.product_id = p.id " +
                     "WHERE o.sale_time BETWEEN ? AND ? " +
                     "GROUP BY o.product_id, p.name " +
                     "ORDER BY total_amount DESC";
        return DBUtil.query(conn, sql, new BeanListHandler<>(SalesStats.class), startDate, endDate);
    }
}

管理员仪表盘 管理员仪表盘,集中展示关键指标和销售趋势图表。

实体模型与数据访问层封装

系统使用JavaBean作为实体模型,与数据库表结构一一对应。数据访问层通过DAO模式封装了所有数据库操作。

商品实体类 (Product.java)

public class Product {
    private Integer id;
    private String name;
    private String specs;
    private Integer supplierId;
    private BigDecimal purchasePrice;
    private BigDecimal retailPrice;
    private Integer stockQuantity;
    private Date createTime;
    private Date updateTime;

    // 标准的Getter和Setter方法
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    // ... 其他Getter/Setter
}

商品DAO接口实现 (ProductDaoImpl.java)

数据访问层使用Apache Commons DbUtils库来简化JDBC编程。

public class ProductDaoImpl implements ProductDao {
    private QueryRunner queryRunner = new QueryRunner();

    @Override
    public Product findById(Connection conn, Integer id) throws SQLException {
        String sql = "SELECT * FROM product WHERE id = ?";
        return queryRunner.query(conn, sql, new BeanHandler<>(Product.class), id);
    }

    @Override
    public List<Product> findAll(Connection conn) throws SQLException {
        String sql = "SELECT * FROM product ORDER BY update_time DESC";
        return queryRunner.query(conn, sql, new BeanListHandler<>(Product.class));
    }

    @Override
    public boolean updateStock(Connection conn, Integer productId, Integer newStock) throws SQLException {
        String sql = "UPDATE product SET stock_quantity = ?, update_time = NOW() WHERE id = ?";
        int affectedRows = queryRunner.update(conn, sql, newStock, productId);
        return affectedRows > 0;
    }
}

功能展望与系统优化方向

  1. 引入Redis缓存层:对于商品列表、供应商信息等不常变化但高频访问的数据,可以引入Redis作为缓存。将查询结果缓存起来,设置合理的过期时间,可以显著减轻数据库压力,提升系统响应速度。实现上,可在DAO层加入缓存逻辑,先查缓存,缓存未命中再查数据库并回填缓存。

  2. 集成高级图表分析与预测功能:当前的数据可视化侧重于历史数据展示。未来可以集成更强大的分析引擎,利用时间序列分析算法(如ARIMA)对销售趋势进行预测,或使用关联规则挖掘(如Apriori算法)分析商品之间的关联销售情况,为精准营销提供支持。

  3. 架构升级至Spring Boot微服务:随着业务复杂

本文关键词
JSPServlet商品销售统计管理系统源码解析

上下篇

上一篇
没有更多文章
下一篇
没有更多文章