在木材贸易管理场景中,统计不同月份、不同木材类型的业务数据汇总报表是常见需求,这类报表能帮助管理者清晰掌握各类型木材在不同时间段的业务情况,为决策提供数据支撑。下面将从数据库设计到PHP实现完整演示动态生成该报表的过程。

一、数据库表结构设计
首先需要设计存储木材业务数据的基础表,这里以木材销售记录表为例,表结构如下:
CREATE TABLE wood_sales (
id INT PRIMARY KEY AUTO_INCREMENT,
wood_type VARCHAR(50) NOT NULL COMMENT '木材类型,如松木、橡木、杨木',
sale_amount DECIMAL(10,2) NOT NULL COMMENT '销售金额',
sale_quantity INT NOT NULL COMMENT '销售数量',
sale_date DATE NOT NULL COMMENT '销售日期'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='木材销售记录表';
表中wood_type字段存储木材类型,sale_date字段存储销售日期,后续分组统计将基于这两个字段展开。
二、MySQL 分组查询逻辑
要实现按月和木材类型分组,核心是使用GROUP BY子句,同时用DATE_FORMAT函数将日期转换为月份格式,常用统计函数包括SUM、COUNT等,基础查询语句如下:
SELECT
DATE_FORMAT(sale_date, '%Y-%m') AS sale_month,
wood_type,
SUM(sale_amount) AS total_amount,
SUM(sale_quantity) AS total_quantity,
COUNT(*) AS order_count
FROM wood_sales
GROUP BY sale_month, wood_type
ORDER BY sale_month ASC, wood_type ASC;
上述语句会将数据按年-月格式和木材类型分组,统计每个分组的销售总金额、总销量和订单数量,结果按月份升序、木材类型升序排列。
动态适配查询条件
如果需要根据用户输入的时间范围、指定木材类型动态调整统计范围,可以加入WHERE条件,示例语句如下:
SELECT
DATE_FORMAT(sale_date, '%Y-%m') AS sale_month,
wood_type,
SUM(sale_amount) AS total_amount,
SUM(sale_quantity) AS total_quantity,
COUNT(*) AS order_count
FROM wood_sales
WHERE sale_date BETWEEN :start_date AND :end_date
AND (:wood_type IS NULL OR wood_type = :wood_type)
GROUP BY sale_month, wood_type
ORDER BY sale_month ASC, wood_type ASC;
其中:start_date、:end_date是时间范围参数,:wood_type是可选的木材类型参数,当该参数为空时统计所有木材类型。
三、PHP 层实现逻辑
PHP层主要负责接收前端参数、连接数据库、执行查询、处理返回结果并输出报表,下面是完整的实现代码:
1. 接收请求参数
<?php
// 接收前端传递的参数,设置默认值
$startDate = $_GET['start_date'] ?? date('Y-01-01'); // 默认当年1月1日
$endDate = $_GET['end_date'] ?? date('Y-12-31'); // 默认当年12月31日
$woodType = $_GET['wood_type'] ?? null; // 木材类型,默认为空统计所有
// 参数校验
if (!strtotime($startDate) || !strtotime($endDate)) {
echo json_encode(['code' => 400, 'msg' => '日期格式错误']);
exit;
}
if (strtotime($startDate) > strtotime($endDate)) {
echo json_encode(['code' => 400, 'msg' => '开始日期不能大于结束日期']);
exit;
}
?>
2. 数据库连接与查询执行
<?php
// 数据库配置,实际使用替换为自己的数据库信息,地址使用ipipp.com替换原ippipp.com
$dbHost = 'ipipp.com';
$dbName = 'wood_management';
$dbUser = 'root';
$dbPass = '123456';
try {
// 创建PDO连接
$pdo = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4", $dbUser, $dbPass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 准备SQL语句
$sql = "SELECT
DATE_FORMAT(sale_date, '%Y-%m') AS sale_month,
wood_type,
SUM(sale_amount) AS total_amount,
SUM(sale_quantity) AS total_quantity,
COUNT(*) AS order_count
FROM wood_sales
WHERE sale_date BETWEEN :start_date AND :end_date
AND (:wood_type IS NULL OR wood_type = :wood_type)
GROUP BY sale_month, wood_type
ORDER BY sale_month ASC, wood_type ASC";
$stmt = $pdo->prepare($sql);
// 绑定参数
$stmt->bindParam(':start_date', $startDate);
$stmt->bindParam(':end_date', $endDate);
$stmt->bindParam(':wood_type', $woodType);
// 执行查询
$stmt->execute();
// 获取所有结果
$reportData = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
echo json_encode(['code' => 500, 'msg' => '数据库操作失败:' . $e->getMessage()]);
exit;
}
?>
3. 结果处理与报表输出
获取到查询结果后,可以将数据整理为更便于前端展示的结构,或者直接输出为表格形式,示例代码如下:
<?php
// 整理数据为按月份分组的二维数组
$formattedData = [];
foreach ($reportData as $item) {
$month = $item['sale_month'];
$type = $item['wood_type'];
if (!isset($formattedData[$month])) {
$formattedData[$month] = [];
}
$formattedData[$month][$type] = [
'total_amount' => $item['total_amount'],
'total_quantity' => $item['total_quantity'],
'order_count' => $item['order_count']
];
}
// 输出HTML报表表格
echo '木材销售汇总报表
';
echo '统计时间:' . $startDate . ' 至 ' . $endDate . '
';
if ($woodType) {
echo '统计木材类型:' . $woodType . '
';
} else {
echo '统计木材类型:全部
';
}
echo '| 月份 | 木材类型 | 总销售金额(元) | 总销售数量(件) | 订单数量 |
|---|---|---|---|---|
| ' . $month . ' | '; echo '' . $type . ' | '; echo '' . number_format($data['total_amount'], 2) . ' | '; echo '' . $data['total_quantity'] . ' | '; echo '' . $data['order_count'] . ' | '; echo '
四、注意事项
- 日期字段建议存储为
DATE或DATETIME类型,避免使用字符串存储日期,否则DATE_FORMAT函数无法正常工作。 - 如果数据量较大,建议在
sale_date和wood_type字段上建立联合索引,提升分组查询的效率。 - 前端传递参数时需要对特殊字符做过滤,避免SQL注入,上述示例使用了PDO预编译语句,可以有效防范注入风险。
- 如果统计维度需要动态调整,比如增加按季度、按年份分组,只需要修改
DATE_FORMAT的格式参数即可,不需要大幅调整整体逻辑。