Java+JSP+SSM家具库存系统实战:从零搭建到库存预警与报表导出
发布时间:2026/9/30 5:47:10来源:尧图网络
简介本资源是一份面向计算机专业本科生及Java初学者的毕业设计文档聚焦家具销售库存管理信息系统的设计与实现解决传统手工管理效率低、数据易出错、信息难追溯等实际问题。文档完整呈现了基于JSPJavaMySQL技术栈的B/S架构系统开发全过程涵盖需求分析、系统设计含功能模块划分与数据库ER图、关键技术实现如用户权限控制、数据加密传输与备份机制、界面设计原则及安全防护方案附有中英文摘要、关键词、目录及详细章节论述。资源为单个1.35MB的DOCX文件内容结构规范适合作为课程设计参考、毕设开题与写作范本或Java Web项目学习素材。目前已有63人学习下载读者可直接获取完整论文框架、技术选型依据、模块功能说明及系统测试要点快速掌握中小型管理信息系统从理论到落地的关键环节。1. 为什么一个“家具销售库存管理信息系统”要用 Java JSP MySQL SSM——不是为了堆技术而是让小家具店老板能看懂、能改、能查账你手头可能正压着一份毕业设计任务书标题写着《基于Java的家具销售库存管理信息系统设计与实现》附件是那个带“.docx”后缀的Word文档。别急着打开它——先问自己一句这系统真要卖给红木厂还是给县城里开三间门面的“老张家具城”用答案通常是后者。这类系统的核心诉求从来不是高并发或微服务而是数据不丢、单据不乱、老板能自己查昨天卖了哪张餐桌、仓库还剩几把餐椅、财务月底能直接导出利润表。Java 的稳、JSP 的直白HTML混写Java逻辑改个页面不用重编译、MySQL 的轻量可靠5000条商品记录跑得比Excel还顺再加上 SSMSpring SpringMVC MyBatis这个被国内中小项目验证过十年的“铁三角”组合起来不是炫技是降低维护门槛店员打错字能当场改老板想加个“促销价”字段你改两行XML配置一个JSP input框就能上线。我去年帮本地一家定制家具厂搭过一版他们连SQL都不会写但学会了在浏览器里点“库存预警”按钮看哪些沙发快断货——这才是这个标题背后最该落地的价值。2. 从零建起SSM骨架用IDEA新建JSP项目绕开80%的“找不到类/404/空指针”血泪坑2.1 创建Maven Webapp工程选对Archetype是第一道生死线很多同学卡在第一步就报错“Artifact is not fully configured”本质是IDEA默认用的maven-archetype-webapp太老只生成web.xml没配Servlet 3.0注解支持。正确做法是手动创建空Maven项目再补全Web结构# 在终端执行确保已装Maven mvn archetype:generate -DgroupIdcom.furniture -DartifactIdfurniture-system -DarchetypeArtifactIdmaven-archetype-webapp -DinteractiveModefalse提示不要用IDEA向导里的“Maven Web Application”模板它会自动生成过时的web.xml和空src目录后续整合SpringMVC时会因Servlet版本冲突导致DispatcherServlet不加载。创建后在IDEA中右键项目 →Add Framework Support→ 勾选Web Application注意版本选4.0IDEA会自动补全src/main/webapp/WEB-INF/web.xml和webapp目录结构。此时项目结构应为furniture-system/ ├── src/ │ ├── main/ │ │ ├── java/ # 存放Controller/Service/DAO │ │ ├── resources/ # 存放spring-config.xml, jdbc.properties │ │ └── webapp/ # JSP/JS/CSS入口 │ └── test/ └── pom.xml2.2 配置pom.xmlSSM核心依赖的版本锁死策略别盲目抄网上教程的最新版SSM组合有严格兼容性约束。经实测JDK 1.8 Tomcat 9.0以下版本组合零报错!-- pom.xml -- properties spring.version5.3.31/spring.version !-- Spring 5.x是JDK8最后稳定版 -- mybatis.version3.4.6/mybatis.version !-- MyBatis 3.4.x适配Spring 5 -- mysql.version8.0.33/mysql.version !-- MySQL 8需用cj驱动 -- /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- MyBatis整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version1.3.2/version !-- 注意不是mybatis-spring-boot-starter这是传统SSM -- /dependency !-- 数据库驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.version}/version scoperuntime/scope /dependency !-- JSP支持关键否则JSP页面编译失败 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies逻辑说明javax.servlet-api设为provided是因为Tomcat已自带Servlet容器重复引入会导致java.lang.LinkageErrorjstl必须显式声明否则JSP里的c:forEach标签会报Cannot find the tag library descriptorMyBatis版本锁定在3.4.6而非4.x因为MyBatis 4要求Spring 6需JDK17与本项目JDK8环境冲突。2.3 初始化SpringMVCweb.xml中DispatcherServlet的3个致命参数很多404错误源于web.xml配置缺失。以下是经过生产环境验证的最小可用配置!-- src/main/webapp/WEB-INF/web.xml -- ?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- 1. 加载Spring根容器IoC -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-config.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- 2. 配置DispatcherServletMVC核心 -- servlet servlet-namespringmvc/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup !-- 必须设为1确保启动时加载 -- /servlet servlet-mapping servlet-namespringmvc/servlet-name url-pattern//url-pattern !-- 关键用/而非*.do否则静态资源404 -- /servlet-mapping !-- 3. 解决中文乱码POST请求 -- filter filter-nameCharacterEncodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping /web-app参数说明url-pattern//url-pattern是解决“JSP页面能访问但CSS/JS加载失败”的核心——它让DispatcherServlet接管所有请求再通过mvc:resources放行静态文件load-on-startup1/load-on-startup确保Spring容器在Tomcat启动时初始化避免首次请求时因Bean未加载而报NullPointerExceptionforceEncodingtrue强制覆盖客户端编码防止家具名称含“实木”“橡木”等中文时入库乱码。3. 数据库设计实战家具行业特有的3类主表如何建模附MySQL建表SQL3.1 商品表furniture_product区分“成品家具”与“原材料”的双模式家具行业特殊性在于一张沙发既是销售商品其框架、海绵、布料又是采购来的原材料。因此不能简单用product_type字段区分而要采用垂直分表外键关联-- 基础商品表存储通用属性 CREATE TABLE furniture_product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) NOT NULL COMMENT 商品编码如SF-2023-001, name VARCHAR(100) NOT NULL COMMENT 商品名称如北欧风布艺沙发, category ENUM(FINISHED, MATERIAL) NOT NULL COMMENT 类型成品/原材料, unit VARCHAR(10) DEFAULT 件 COMMENT 计量单位, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT 状态1启用0停用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品基础信息表; -- 成品家具扩展表仅成品有 CREATE TABLE furniture_finished ( id BIGINT PRIMARY KEY, product_id BIGINT NOT NULL, size VARCHAR(50) COMMENT 尺寸如200*90*85cm, weight DECIMAL(8,2) COMMENT 重量(kg), warranty_month INT DEFAULT 24 COMMENT 保修月数, FOREIGN KEY (product_id) REFERENCES furniture_product(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 原材料扩展表仅原材料有 CREATE TABLE furniture_material ( id BIGINT PRIMARY KEY, product_id BIGINT NOT NULL, supplier_name VARCHAR(100) COMMENT 供应商名称, spec VARCHAR(100) COMMENT 规格如高密度海绵密度35kg/m³, FOREIGN KEY (product_id) REFERENCES furniture_product(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;设计理由避免在主表中塞满NULL字段如成品不需要supplier_name原材料不需要warranty_monthON DELETE CASCADE保证删除商品时自动清理扩展信息防止数据残留code字段加索引后续查询常用但不用UNIQUE——同一款沙发可能有不同颜色变体编码规则可设计为SF-{年份}-{序号}-{颜色}。3.2 库存表furniture_stock用“仓库商品批次”三维定位防错小家具店常有多个仓库展厅仓、工厂仓、售后仓且同款沙发不同批次成本不同如首批进口布料第二批国产。因此库存必须按仓库商品批次三维度统计CREATE TABLE furniture_warehouse ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 仓库名如总部展厅仓, address VARCHAR(200) COMMENT 地址, manager VARCHAR(20) COMMENT 负责人 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT仓库信息表; CREATE TABLE furniture_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, warehouse_id BIGINT NOT NULL, product_id BIGINT NOT NULL, batch_no VARCHAR(50) DEFAULT DEFAULT COMMENT 批次号如2023-Q3-001, quantity INT NOT NULL DEFAULT 0 COMMENT 当前库存数量, cost_price DECIMAL(10,2) COMMENT 入库成本价元, last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_warehouse_product_batch (warehouse_id, product_id, batch_no), FOREIGN KEY (warehouse_id) REFERENCES furniture_warehouse(id), FOREIGN KEY (product_id) REFERENCES furniture_product(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存明细表;关键点UNIQUE KEY uk_warehouse_product_batch强制同一仓库、同一商品、同一批次只能有一条记录杜绝重复录入cost_price存的是该批次的采购成本非平均成本——这样计算销售毛利才准确例卖出10张沙发其中5张来自高价批次5张来自低价批次需分别结转成本。3.3 销售单表furniture_sale_order用JSON字段存商品明细规避关系型数据库的N1查询陷阱销售单包含多行商品一张订单买沙发茶几地毯传统做法是建sale_order和sale_order_item两张表。但家具行业有个现实老板经常临时加减商品、修改单价、备注特殊要求如“茶几腿加高5cm”。若用关联表每次修改都要删旧增新事务复杂且易出错。更优解是用MySQL 5.7的JSON类型存明细CREATE TABLE furniture_sale_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(30) NOT NULL UNIQUE COMMENT 订单号如SO-20231001-001, customer_name VARCHAR(100) NOT NULL COMMENT 客户姓名, phone VARCHAR(20) COMMENT 联系电话, total_amount DECIMAL(12,2) NOT NULL COMMENT 订单总金额, status ENUM(DRAFT,CONFIRMED,SHIPPED,COMPLETED,CANCELLED) DEFAULT DRAFT, remark TEXT COMMENT 订单备注如客户要求加急明天送货, items JSON NOT NULL COMMENT 商品明细JSON格式见下文, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销售订单主表; -- JSON字段items示例实际由Java代码序列化写入 -- [ -- { -- productId: 1001, -- productName: 北欧风布艺沙发, -- quantity: 2, -- unitPrice: 3999.00, -- subtotal: 7998.00, -- remark: 加装防滑脚垫 -- }, -- { -- productId: 1002, -- productName: 大理石茶几, -- quantity: 1, -- unitPrice: 2599.00, -- subtotal: 2599.00, -- remark: -- } -- ]优势单次INSERT/UPDATE完成整单操作无事务嵌套风险前端JSP页面提交时JavaScript将表格数据JSON.stringify()后传给后端Java用RequestBody String itemsJson接收再用Fastjson解析查询时用MySQL的JSON_CONTAINS()函数可快速筛选含某商品的订单如查所有买了“沙发”的订单。4. 核心功能落地用JSPSSM实现“库存预警”页面含实时库存计算逻辑4.1 后端Controller暴露REST接口返回预警商品列表预警规则库存数量 ≤ 安全库存商品表中safe_stock字段且状态为启用。注意安全库存需在商品表中新增字段ALTER TABLE furniture_product ADD COLUMN safe_stock INT DEFAULT 0 COMMENT 安全库存数量;Controller代码ProductController.javaController RequestMapping(/product) public class ProductController { Autowired private ProductService productService; /** * 获取库存预警商品列表供JSP页面AJAX调用 * return JSON格式{code:200,data:[{...}]} */ ResponseBody RequestMapping(value /lowStockAlert, method RequestMethod.GET) public MapString, Object getLowStockAlert() { MapString, Object result new HashMap(); try { ListProductVO alertList productService.findLowStockProducts(); result.put(code, 200); result.put(data, alertList); } catch (Exception e) { result.put(code, 500); result.put(msg, 查询失败 e.getMessage()); } return result; } }逻辑说明ResponseBody确保返回JSON而非视图名ProductVO是专为页面展示设计的VO类含商品名、当前库存、安全库存、仓库名避免直接返回Entity暴露数据库字段异常捕获后返回标准JSON结构方便JSP前端统一处理。4.2 Service层跨表关联查询的MyBatis动态SQL写法ProductService调用ProductMapper关键在findLowStockProducts方法。MyBatis XML映射文件ProductMapper.xml需关联furniture_product、furniture_stock、furniture_warehouse三张表并处理“同一商品在多个仓库库存不同”的聚合逻辑!-- ProductMapper.xml -- select idfindLowStockProducts resultTypecom.furniture.vo.ProductVO SELECT p.id AS productId, p.name AS productName, p.code AS productCode, p.safe_stock AS safeStock, COALESCE(SUM(s.quantity), 0) AS currentStock, -- 当前总库存多仓库求和 GROUP_CONCAT(w.name SEPARATOR , ) AS warehouseNames -- 所在仓库名逗号分隔 FROM furniture_product p LEFT JOIN furniture_stock s ON p.id s.product_id LEFT JOIN furniture_warehouse w ON s.warehouse_id w.id WHERE p.status 1 AND p.safe_stock 0 GROUP BY p.id, p.name, p.code, p.safe_stock HAVING COALESCE(SUM(s.quantity), 0) p.safe_stock ORDER BY currentStock ASC /select参数说明COALESCE(SUM(s.quantity), 0)处理商品无库存记录时SUM返回NULL的问题强制转为0GROUP_CONCAT(w.name SEPARATOR , )将同一商品所在的所有仓库名拼成字符串如“总部展厅仓, 售后仓”方便页面显示HAVING子句在分组后过滤比在WHERE中过滤更准确WHERE无法使用聚合函数。4.3 JSP预警页面用原生JavaScript轮询Bootstrap表格渲染创建/WEB-INF/jsp/alert/lowStock.jsp不依赖jQuery减少外部依赖% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head meta charsetUTF-8 title库存预警/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.1.3/dist/css/bootstrap.min.css relstylesheet /head body div classcontainer mt-4 h2⚠️ 库存预警数量 ≤ 安全库存/h2 div classalert alert-info strong提示/strong每30秒自动刷新红色行表示库存为0。 /div table classtable table-striped table-hover thead tr th商品编码/th th商品名称/th th当前库存/th th安全库存/th th所在仓库/th th操作/th /tr /thead tbody idalertTableBody !-- 数据由JS填充 -- /tbody /table /div script // 每30秒拉取预警数据 function loadAlertData() { fetch(${pageContext.request.contextPath}/product/lowStockAlert) .then(response response.json()) .then(data { if (data.code 200) { const tbody document.getElementById(alertTableBody); tbody.innerHTML ; // 清空旧数据 data.data.forEach(item { const row document.createElement(tr); // 库存为0时标红 if (item.currentStock 0) { row.className table-danger; } row.innerHTML td${item.productCode}/td td${item.productName}/td td${item.currentStock}/td td${item.safeStock}/td td${item.warehouseNames || 暂无库存}/td td a href${pageContext.request.contextPath}/product/edit?id${item.productId} classbtn btn-sm btn-primary编辑/a /td ; tbody.appendChild(row); }); } }) .catch(err console.error(加载预警失败:, err)); } // 页面加载后立即执行然后每30秒执行 loadAlertData(); setInterval(loadAlertData, 30000); /script /body /html关键细节fetch替代$.ajax避免引入jQuerytable-danger类用Bootstrap内置样式标红零库存行setInterval实现无感轮询比Meta Refresh更友好href链接指向商品编辑页路径用${pageContext.request.contextPath}确保上下文路径正确部署到子路径如/furniture-system时仍有效。5. 避坑指南SSMJSP项目上线前必查的5个致命问题5.1 现象JSP页面中文显示为“???”但控制台日志中文正常原因JSP文件本身编码与Tomcat默认编码不一致。IDEA新建JSP时默认用UTF-8但Tomcat 9在Windows下可能读取系统编码GBK导致JSP中的中文被错误解码。解决在web.xml中添加JSP编码声明放在web-app根节点内jsp-config jsp-property-group url-pattern*.jsp/url-pattern page-encodingUTF-8/page-encoding scripting-invalidfalse/scripting-invalid /jsp-property-group /jsp-config同时检查IDEAFile → Settings → Editor → File Encodings将Global Encoding、Project Encoding、Default encoding for properties files全部设为UTF-8。5.2 现象MySQL 8连接报错Public Key Retrieval is not allowed原因MySQL 8默认启用caching_sha2_password认证插件而mysql-connector-java 8.0.33驱动要求显式允许公钥检索存在安全风险故默认禁用。解决在jdbc.properties的URL末尾添加参数jdbc.urljdbc:mysql://localhost:3306/furniture_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse注意allowPublicKeyRetrievaltrue仅用于开发环境生产环境应改用sha256_password插件并配置SSL证书。5.3 现象Tomcat启动成功但访问/login.jsp返回404而/index.jsp能打开原因web.xml中servlet-mapping的url-pattern配置错误。常见误写为*.do或/login导致JSP请求未被容器识别。解决确认web.xml中DispatcherServlet的映射为url-pattern//url-pattern见2.3节并确保login.jsp位于/WEB-INF/jsp/目录下非/WEB-INF/根目录。JSP路径必须与Controller返回的视图名严格匹配// Controller中返回login则JSP路径必须是 /WEB-INF/jsp/login.jsp RequestMapping(/login) public String showLogin() { return login; // 自动解析为 /WEB-INF/jsp/login.jsp }5.4 现象MyBatis执行INSERT后id返回0但数据库已插入成功原因MySQL表主键未设为AUTO_INCREMENT或MyBatis的insert标签未配置useGeneratedKeystrue。解决检查MySQL建表语句确保主键有AUTO_INCREMENT如id BIGINT PRIMARY KEY AUTO_INCREMENT在Mapper XML中明确配置insert idinsertProduct parameterTypeProduct useGeneratedKeystrue keyPropertyid INSERT INTO furniture_product (code, name, category) VALUES (#{code}, #{name}, #{category}) /insertkeyPropertyid告诉MyBatis将生成的主键值回填到Product对象的id属性。5.5 现象JSP页面中c:forEach遍历集合为空但后台Debug确认List不为空原因JSTL标签库未正确引入或web.xml中web-app的version与JSTL版本不匹配。解决确认pom.xml已添加jstl依赖见2.2节检查web.xml顶部web-app的version是否为4.0对应JSTL 1.2在JSP页面顶部添加标准Taglib声明% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %注意URI必须完全一致http://java.sun.com/jsp/jstl/core不能写成http://jakarta.apache.org/taglibs/core等旧地址。6. 进阶技巧用POI导出“销售业绩报表”——不写Excel公式用Java动态计算同比环比6.1 需求场景老板要的不是原始数据而是“这个月比上个月多卖了23%比去年同月增长15%”家具销售分析的关键指标是环比vs 上月和同比vs 去年同月。如果导出纯数据表老板还得打开Excel手动算体验极差。POI可以做到在导出的Excel中Java层直接计算好结果写入单元格用户打开即见结论。6.2 实现步骤用XSSFWorkbook生成带公式的Excel但公式由Java预计算不推荐在Excel中写(B2-B1)/B1这类公式——因为POI写公式需要处理单元格引用易出错。更稳的方案是Java计算好数值直接写入结果单元格。以“月度销售汇总表”为例// SalesReportService.java public void exportMonthlyReport(HttpServletResponse response) throws IOException { // 1. 查询近3个月销售数据SQL用DATE_SUB和GROUP BY按月聚合 ListSalesSummary summaries salesMapper.findMonthlySales( DateUtils.addMonths(new Date(), -2), // 3个月前 new Date() // 当前日期 ); // 2. 创建Excel工作簿 XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(销售业绩); // 3. 写入表头 XSSFRow headerRow sheet.createRow(0); String[] headers {月份, 销售额(元), 环比增长率, 同比增长率, 订单数}; for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 4. 写入数据行重点动态计算增长率 for (int i 0; i summaries.size(); i) { SalesSummary s summaries.get(i); XSSFRow row sheet.createRow(i 1); // 月份格式化为2023-10 row.createCell(0).setCellValue(s.getMonthStr()); // 销售额 row.createCell(1).setCellValue(s.getTotalAmount()); // 环比增长率本月/上月 - 1需处理上月不存在的情况 if (i 0) { double lastMonthAmount summaries.get(i - 1).getTotalAmount(); double monthOnMonth lastMonthAmount 0 ? (s.getTotalAmount() - lastMonthAmount) / lastMonthAmount : 0; row.createCell(2).setCellValue(monthOnMonth); } else { row.createCell(2).setCellValue(0.0); // 第一个月无环比 } // 同比增长率需查去年同月数据假设已查好存入summary.yoyAmount double yearOnYear s.getYoyAmount() 0 ? (s.getTotalAmount() - s.getYoyAmount()) / s.getYoyAmount() : 0; row.createCell(3).setCellValue(yearOnYear); // 订单数 row.createCell(4).setCellValue(s.getOrderCount()); } // 5. 设置列宽和数字格式 sheet.setColumnWidth(0, 3000); // 月份列 sheet.setColumnWidth(1, 5000); // 销售额列 sheet.setColumnWidth(2, 5000); // 环比列 sheet.setColumnWidth(3, 5000); // 同比列 sheet.setColumnWidth(4, 3000); // 订单数列 // 为增长率列设置百分比格式 XSSFCellStyle percentStyle workbook.createCellStyle(); XSSFDataFormat format workbook.createDataFormat(); percentStyle.setDataFormat(format.getFormat(0.00%)); for (int r 1; r summaries.size(); r) { sheet.getRow(r).getCell(2).setCellStyle(percentStyle); sheet.getRow(r).getCell(3).setCellStyle(percentStyle); } // 6. 输出响应 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenamesales_report_ new SimpleDateFormat(yyyyMMdd).format(new Date()) .xlsx); workbook.write(response.getOutputStream()); workbook.close(); }技巧说明XSSFCellStyle配合XSSFDataFormat实现“0.00%”格式单元格显示15.23%而非0.1523DateUtils.addMonths用Apache Commons Lang的工具类避免手动计算月份如1月减1个月是12月非0月yoyAmount字段由SQL子查询或二次查询获取例如SELECT DATE_FORMAT(create_time, %Y-%m) as month_str, SUM(total_amount) as total_amount, (SELECT SUM(total_amount) FROM furniture_sale_order WHERE DATE_FORMAT(create_time, %Y-%m) DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 1 YEAR), %Y-%m)) as yoy_amount FROM furniture_sale_order WHERE create_time DATE_SUB(NOW(), INTERVAL 2 MONTH) GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month_str;6.3 最后一道防线用Log4j2记录导出行为防背锅老板说“报表数据不对”你得能快速定位是数据源问题还是导出逻辑问题。在导出方法开头加审计日志private static final Logger logger LogManager.getLogger(SalesReportService.class); public void exportMonthlyReport(HttpServletResponse response) throws IOException { long startTime System.currentTimeMillis(); logger.info(开始导出销售报表时间范围{} 至 {}, DateUtils.format(DateUtils.addMonths(new Date(), -2), yyyy-MM-dd), DateUtils.format(new Date(), yyyy-MM-dd)); try { // ... 导出逻辑 ... long duration System.currentTimeMillis() - startTime; logger.info(销售报表导出成功耗时 {}ms共 {} 行数据, duration, summaries.size()); } catch (Exception e) { logger.error(销售报表导出失败, e); throw e; } }这样当老板质疑时你打开logs/app.log输入grep 销售报表导出成功 app.log立刻看到2023-10-05 09:23:45 INFO SalesReportService - 销售报表导出成功耗时 1245ms共 3 行数据再结合数据库查询确认summaries.size()是否等于实际数据行数问题边界瞬间清晰。我带过的实习生里有三个栽在“导出报表数字对不上”上翻了两天SQL才发现是GROUP BY漏了WHERE statusCOMPLETED导致把已取消订单也算进去了。后来我养成习惯所有导出功能上线前先用Postman调一次接口把返回的JSON数据粘贴到Excel里用SUM()函数手动验算一遍总额——这招比写单元测试还快。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网