本文目录导读:

下面给您一个完整的Java报表系统案例,从技术选型到核心代码实现,涵盖实际项目中最常用的技术方案。
需求:为企业开发一个销售报表系统,实现销售数据的可视化展示与导出功能。
技术栈:
- 后端:Spring Boot 2.x + MyBatis-Plus
- 报表引擎:JasperReports(服务端生成PDF/Excel)
- ECharts(前端可视化图表)
- 数据库:MySQL
- 工具:Maven
数据库设计
-- 销售订单表 CREATE TABLE `sales_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `product_name` varchar(100) NOT NULL COMMENT '商品名称', `category` varchar(50) DEFAULT NULL COMMENT '商品分类', `quantity` int(11) NOT NULL COMMENT '销售数量', `amount` decimal(10,2) NOT NULL COMMENT '销售金额', `salesman` varchar(50) DEFAULT NULL COMMENT '销售员', `order_date` date NOT NULL COMMENT '订单日期', PRIMARY KEY (`id`), KEY `idx_order_date` (`order_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
后端核心代码
Entity实体类
@Data
@TableName("sales_order")
public class SalesOrder {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private String productName;
private String category;
private Integer quantity;
private BigDecimal amount;
private String salesman;
private Date orderDate;
}
Mapper层
@Mapper
public interface SalesOrderMapper extends BaseMapper<SalesOrder> {
// 按日期区间汇总销售额
@Select("SELECT DATE_FORMAT(order_date, '%Y-%m-%d') as date, " +
"SUM(amount) as totalAmount, SUM(quantity) as totalQuantity " +
"FROM sales_order " +
"WHERE order_date BETWEEN #{startDate} AND #{endDate} " +
"GROUP BY DATE_FORMAT(order_date, '%Y-%m-%d')")
List<Map<String, Object>> getDailySalesSummary(@Param("startDate") String startDate,
@Param("endDate") String endDate);
// 按商品分类统计
@Select("SELECT category, COUNT(*) as orderCount, " +
"SUM(amount) as totalAmount " +
"FROM sales_order " +
"WHERE order_date BETWEEN #{startDate} AND #{endDate} " +
"GROUP BY category")
List<Map<String, Object>> getCategorySummary(@Param("startDate") String startDate,
@Param("endDate") String endDate);
}
Service层
@Service
public class ReportService {
@Autowired
private SalesOrderMapper salesOrderMapper;
// 获取销售趋势数据(用于ECharts)
public Map<String, Object> getSalesTrend(String startDate, String endDate) {
List<Map<String, Object>> dailyData = salesOrderMapper.getDailySalesSummary(startDate, endDate);
List<String> dates = new ArrayList<>();
List<BigDecimal> amounts = new ArrayList<>();
for (Map<String, Object> item : dailyData) {
dates.add(item.get("date").toString());
amounts.add(new BigDecimal(item.get("totalAmount").toString()));
}
Map<String, Object> result = new HashMap<>();
result.put("dates", dates);
result.put("amounts", amounts);
return result;
}
// 获取分类占比数据
public List<Map<String, Object>> getCategoryData(String startDate, String endDate) {
return salesOrderMapper.getCategorySummary(startDate, endDate);
}
// 导出PDF报表
public byte[] exportPdfReport(String startDate, String endDate) throws Exception {
// 获取数据
List<SalesOrder> orders = salesOrderMapper.selectList(
new LambdaQueryWrapper<SalesOrder>()
.between(SalesOrder::getOrderDate, startDate, endDate)
);
// 加载JasperReports模板
InputStream inputStream = getClass().getResourceAsStream("/reports/sales_report.jrxml");
JasperReport jasperReport = JasperCompileManager.compileReport(inputStream);
// 填充数据
JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(orders);
Map<String, Object> parameters = new HashMap<>();
parameters.put("startDate", startDate);
parameters.put("endDate", endDate);
parameters.put("totalAmount", orders.stream()
.map(SalesOrder::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add));
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, dataSource);
// 导出为PDF
return JasperExportManager.exportReportToPdf(jasperPrint);
}
// 导出Excel
public void exportExcel(String startDate, String endDate, HttpServletResponse response) throws Exception {
List<SalesOrder> orders = salesOrderMapper.selectList(
new LambdaQueryWrapper<SalesOrder>()
.between(SalesOrder::getOrderDate, startDate, endDate)
);
// 使用EasyExcel
EasyExcel.write(response.getOutputStream(), SalesOrder.class)
.sheet("销售报表")
.doWrite(orders);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("销售报表", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
}
}
Controller层
@RestController
@RequestMapping("/api/report")
public class ReportController {
@Autowired
private ReportService reportService;
// 获取销售趋势JSON
@GetMapping("/sales-trend")
public Result<Map<String, Object>> getSalesTrend(
@RequestParam String startDate,
@RequestParam String endDate) {
return Result.success(reportService.getSalesTrend(startDate, endDate));
}
// 获取分类数据
@GetMapping("/category")
public Result<List<Map<String, Object>>> getCategoryData(
@RequestParam String startDate,
@RequestParam String endDate) {
return Result.success(reportService.getCategoryData(startDate, endDate));
}
// 导出PDF
@GetMapping("/export/pdf")
public void exportPdf(@RequestParam String startDate,
@RequestParam String endDate,
HttpServletResponse response) throws Exception {
byte[] pdfBytes = reportService.exportPdfReport(startDate, endDate);
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename=sales_report.pdf");
response.getOutputStream().write(pdfBytes);
}
// 导出Excel
@GetMapping("/export/excel")
public void exportExcel(@RequestParam String startDate,
@RequestParam String endDate,
HttpServletResponse response) throws Exception {
reportService.exportExcel(startDate, endDate, response);
}
}
前端页面(HTML + ECharts)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">销售报表系统</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<style>
.container { display: flex; flex-wrap: wrap; padding: 20px; }
.chart-container {
width: 48%;
height: 400px;
margin: 1%;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.date-input { padding: 20px; }
.btn { padding: 8px 16px; margin-right: 10px; }
</style>
</head>
<body>
<div class="date-input">
开始日期:<input type="date" id="startDate" value="2024-01-01">
结束日期:<input type="date" id="endDate" value="2024-12-31">
<button class="btn" onclick="loadData()">查询</button>
<button class="btn" onclick="exportPdf()">导出PDF</button>
<button class="btn" onclick="exportExcel()">导出Excel</button>
</div>
<div class="container">
<div id="salesTrend" class="chart-container"></div>
<div id="categoryChart" class="chart-container"></div>
</div>
<script>
// 初始化销售趋势图
var trendChart = echarts.init(document.getElementById('salesTrend'));
var categoryChart = echarts.init(document.getElementById('categoryChart'));
function loadData() {
var startDate = $('#startDate').val();
var endDate = $('#endDate').val();
// 加载销售趋势数据
$.ajax({
url: '/api/report/sales-trend',
method: 'GET',
data: { startDate: startDate, endDate: endDate },
success: function(response) {
var data = response.data;
trendChart.setOption({
title: { text: '销售趋势' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: data.dates },
yAxis: { type: 'value', name: '销售额(元)' },
series: [{
name: '销售额',
type: 'line',
smooth: true,
data: data.amounts,
areaStyle: { color: 'rgba(54, 162, 235, 0.2)' },
itemStyle: { color: '#3398DB' }
}]
});
}
});
// 加载分类占比数据
$.ajax({
url: '/api/report/category',
method: 'GET',
data: { startDate: startDate, endDate: endDate },
success: function(response) {
var data = response.data;
categoryChart.setOption({
title: { text: '商品分类销售占比' },
tooltip: { trigger: 'item', formatter: '{b}: £{c} ({d}%)' },
series: [{
name: '销售占比',
type: 'pie',
radius: '60%',
data: data.map(function(item) {
return {
name: item.category,
value: item.totalAmount
};
}),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
});
}
});
}
function exportPdf() {
var startDate = $('#startDate').val();
var endDate = $('#endDate').val();
window.location.href = '/api/report/export/pdf?startDate=' + startDate + '&endDate=' + endDate;
}
function exportExcel() {
var startDate = $('#startDate').val();
var endDate = $('#endDate').val();
window.location.href = '/api/report/export/excel?startDate=' + startDate + '&endDate=' + endDate;
}
// 页面加载时自动查询
$(document).ready(function() {
loadData();
});
</script>
</body>
</html>
JasperReports模板配置
在 src/main/resources/reports/sales_report.jrxml 中定义报表模板:
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
name="Sales Report" pageWidth="595" pageHeight="842">
<parameter name="startDate" class="java.lang.String"/>
<parameter name="endDate" class="java.lang.String"/>
<parameter name="totalAmount" class="java.math.BigDecimal"/>
<band height="50">
<staticText>
<reportElement x="180" y="10" width="200" height="30"/>
<textElement textAlignment="Center">
<font size="16" isBold="true"/>
</textElement>
<text><![CDATA[销售报表]]></text>
</staticText>
</band>
</title>
<columnHeader>
<band height="20">
<staticText>
<reportElement x="0" y="0" width="100" height="20"/>
<text><![CDATA[订单号]]></text>
</staticText>
<staticText>
<reportElement x="100" y="0" width="100" height="20"/>
<text><![CDATA[商品名称]]></text>
</staticText>
<!-- 其他列... -->
</band>
</columnHeader>
<detail>
<band height="20">
<textField>
<reportElement x="0" y="0" width="100" height="20"/>
<textFieldExpression><![CDATA[$F{orderNo}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="100" y="0" width="100" height="20"/>
<textFieldExpression><![CDATA[$F{productName}]]></textFieldExpression>
</textField>
<!-- 其他列... -->
</band>
</detail>
<summary>
<band height="30">
<staticText>
<reportElement x="0" y="0" width="100" height="20"/>
<text><![CDATA[总金额:]]></text>
</staticText>
<textField>
<reportElement x="100" y="0" width="100" height="20"/>
<textFieldExpression><![CDATA[$P{totalAmount}]]></textFieldExpression>
</textField>
</band>
</summary>
</jasperReport>
项目配置
pom.xml 关键依赖:
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- JasperReports -->
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>6.20.0</version>
</dependency>
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
核心功能演示效果
- 趋势图:折线图展示每日销售额变化
- 分类饼图:展示不同商品分类的销售占比
- PDF导出:服务端生成专业格式的PDF报表
- Excel导出:支持数据分析和二次加工
优化建议
| 优化方向 | 实现方案 |
|---|---|
| 大数据量 | 使用Elasticsearch做聚合分析 |
| 缓存 | Redis缓存高频查询结果 |
| 异步 | Spring @Async异步生成大报表 |
| 安全 | 加入Shiro/JWT权限控制 |
扩展功能
- 定时报表:使用Spring Scheduled定时发送邮件
- 多维度分析:按客户、区域、时间等多维度切片
- 可视化大屏:接入Grafana或自研大屏
- 智能预警:销售额异常自动告警
这个案例覆盖了报表系统从数据查询、可视化展示到文件导出的完整流程,您可以根据实际业务需求调整数据统计逻辑和展示样式。