本文目录导读:

我来帮你设计一个PHP项目统计高球传中争顶成功率的方案,这个功能通常用于足球数据统计系统。
数据库设计
-- 球员表
CREATE TABLE players (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
team_id INT,
position VARCHAR(20)
);
-- 传中数据表
CREATE TABLE crosses (
id INT PRIMARY KEY AUTO_INCREMENT,
player_id INT,
match_id INT,
cross_time INT, -- 比赛时间(分钟)
cross_type VARCHAR(20), -- 传中类型: 'high_ball', 'low_ball', 'cross'
from_x DECIMAL(5,2), -- 传中位置X坐标
from_y DECIMAL(5,2), -- 传中位置Y坐标
to_x DECIMAL(5,2), -- 落点X坐标
to_y DECIMAL(5,2), -- 落点Y坐标
is_successful BOOLEAN, -- 是否成功
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 争顶数据表
CREATE TABLE headed_duels (
id INT PRIMARY KEY AUTO_INCREMENT,
cross_id INT,
player_id INT,
is_won BOOLEAN, -- 是否赢得争顶
is_clear BOOLEAN, -- 是否解围
touch_type VARCHAR(20), -- 触球类型
is_goal BOOLEAN, -- 是否进球
FOREIGN KEY (cross_id) REFERENCES crosses(id)
);
核心统计类
<?php
class HeadingSuccessRateCalculator {
private $db;
public function __construct($dbConnection) {
$this->db = $dbConnection;
}
/**
* 计算球员传中争顶成功率
* @param int $playerId 球员ID
* @param string $startDate 开始日期
* @param string $endDate 结束日期
* @return array 统计结果
*/
public function calculatePlayerHeadingRate($playerId, $startDate = null, $endDate = null) {
$where = "c.player_id = ?";
$params = [$playerId];
if ($startDate && $endDate) {
$where .= " AND c.create_time BETWEEN ? AND ?";
$params[] = $startDate;
$params[] = $endDate;
}
// 查询球员的传中数据
$query = "SELECT
COUNT(*) as total_crosses,
SUM(CASE WHEN c.is_successful = 1 THEN 1 ELSE 0 END) as successful_crosses,
SUM(CASE WHEN hd.is_won = 1 THEN 1 ELSE 0 END) as won_headers,
SUM(CASE WHEN hd.is_goal = 1 THEN 1 ELSE 0 END) as header_goals,
SUM(CASE WHEN c.is_successful = 1 AND hd.is_won = 1 THEN 1 ELSE 0 END) as successful_headers
FROM crosses c
LEFT JOIN headed_duels hd ON hd.cross_id = c.id
WHERE $where
GROUP BY c.player_id";
$stmt = $this->db->prepare($query);
$stmt->execute($params);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
// 计算各种成功率
$totalCrosses = $result['total_crosses'] ?? 0;
$successfulCrosses = $result['successful_crosses'] ?? 0;
$wonHeaders = $result['won_headers'] ?? 0;
$headerGoals = $result['header_goals'] ?? 0;
return [
'player_id' => $playerId,
'total_crosses' => $totalCrosses,
'successful_crosses' => $successfulCrosses,
'cross_success_rate' => $totalCrosses > 0 ?
round(($successfulCrosses / $totalCrosses) * 100, 2) : 0,
'total_headers' => $totalCrosses,
'won_headers' => $wonHeaders,
'heading_success_rate' => $totalCrosses > 0 ?
round(($wonHeaders / $totalCrosses) * 100, 2) : 0,
'header_goals' => $headerGoals,
'header_goal_rate' => $totalCrosses > 0 ?
round(($headerGoals / $totalCrosses) * 100, 2) : 0,
// 综合效率:争顶成功且传中成功
'overall_efficiency' => $totalCrosses > 0 ?
round((($wonHeaders + $successfulCrosses) / (2 * $totalCrosses)) * 100, 2) : 0,
// 争顶成功率(在成功传中中的比例)
'heading_rate_in_successful_crosses' => $successfulCrosses > 0 ?
round(($wonHeaders / $successfulCrosses) * 100, 2) : 0
];
}
/**
* 计算球队的传中争顶成功率
*/
public function calculateTeamHeadingRate($teamId, $startDate = null, $endDate = null) {
$query = "SELECT
p.team_id,
COUNT(c.id) as total_crosses,
SUM(CASE WHEN c.is_successful = 1 THEN 1 ELSE 0 END) as successful_crosses,
SUM(CASE WHEN hd.is_won = 1 THEN 1 ELSE 0 END) as won_headers,
SUM(CASE WHEN hd.is_goal = 1 THEN 1 ELSE 0 END) as header_goals
FROM crosses c
INNER JOIN players p ON p.id = c.player_id
LEFT JOIN headed_duels hd ON hd.cross_id = c.id
WHERE p.team_id = ?";
if ($startDate && $endDate) {
$query .= " AND c.create_time BETWEEN ? AND ?";
$params = [$teamId, $startDate, $endDate];
} else {
$params = [$teamId];
}
$query .= " GROUP BY p.team_id";
$stmt = $this->db->prepare($query);
$stmt->execute($params);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
/**
* 获取球员争顶热区数据
*/
public function getPlayerHeadingHeatmap($playerId, $startDate, $endDate) {
$query = "SELECT
ROUND(c.from_x, 2) as x_coord,
ROUND(c.from_y, 2) as y_coord,
COUNT(*) as cross_count,
SUM(CASE WHEN hd.is_won = 1 THEN 1 ELSE 0 END) as heading_wins
FROM crosses c
LEFT JOIN headed_duels hd ON hd.cross_id = c.id
WHERE c.player_id = ?
AND c.create_time BETWEEN ? AND ?
GROUP BY c.from_x, c.from_y
ORDER BY cross_count DESC
LIMIT 20";
$stmt = $this->db->prepare($query);
$stmt->execute([$playerId, $startDate, $endDate]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 比较两名球员的争顶成功率
*/
public function comparePlayers($player1Id, $player2Id, $startDate = null, $endDate = null) {
$player1Stats = $this->calculatePlayerHeadingRate($player1Id, $startDate, $endDate);
$player2Stats = $this->calculatePlayerHeadingRate($player2Id, $startDate, $endDate);
return [
'player1' => $player1Stats,
'player2' => $player2Stats,
'comparison' => [
'better_heading_rate' => $player1Stats['heading_success_rate'] >
$player2Stats['heading_success_rate'] ? 'player1' : 'player2',
'better_cross_rate' => $player1Stats['cross_success_rate'] >
$player2Stats['cross_success_rate'] ? 'player1' : 'player2'
]
];
}
}
API 控制器示例
<?php
class HeadingStatsController {
private $calculator;
public function __construct($db) {
$this->calculator = new HeadingSuccessRateCalculator($db);
}
// 获取球员统计信息
public function getPlayerStats($playerId) {
header('Content-Type: application/json');
try {
$startDate = $_GET['start_date'] ?? date('Y-m-d', strtotime('-30 days'));
$endDate = $_GET['end_date'] ?? date('Y-m-d');
$stats = $this->calculator->calculatePlayerHeadingRate(
$playerId, $startDate, $endDate
);
// 获取比赛记录
$matches = $this->getPlayerMatchHistory($playerId, $startDate, $endDate);
echo json_encode([
'status' => 'success',
'data' => [
'basic_stats' => $stats,
'match_history' => $matches
]
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'status' => 'error',
'message' => $e->getMessage()
]);
}
}
// 获取比赛历史
private function getPlayerMatchHistory($playerId, $startDate, $endDate) {
$query = "SELECT
m.match_date,
m.opponent,
m.home_away,
COUNT(c.id) as crosses,
SUM(CASE WHEN hd.is_won = 1 THEN 1 ELSE 0 END) as heading_wins,
SUM(CASE WHEN hd.is_goal = 1 THEN 1 ELSE 0 END) as goals
FROM crosses c
INNER JOIN matches m ON m.id = c.match_id
LEFT JOIN headed_duels hd ON hd.cross_id = c.id
WHERE c.player_id = ?
AND m.match_date BETWEEN ? AND ?
GROUP BY m.id
ORDER BY m.match_date DESC";
$stmt = $this->db->prepare($query);
$stmt->execute([$playerId, $startDate, $endDate]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
前端展示示例
<!DOCTYPE html>
<html>
<head>传中争顶成功率统计</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="container">
<h2>球员争顶成功率统计</h2>
<!-- 球员选择 -->
<div class="player-select">
<select id="playerSelect">
<option value="">选择球员</option>
</select>
<button onclick="loadStats()">查看统计</button>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<h3>传中成功率</h3>
<p id="crossRate">0%</p>
</div>
<div class="stat-card">
<h3>争顶成功率</h3>
<p id="headingRate">0%</p>
</div>
<div class="stat-card">
<h3>头球进球数</h3>
<p id="headerGoals">0</p>
</div>
<div class="stat-card">
<h3>综合效率</h3>
<p id="overallEfficiency">0%</p>
</div>
</div>
<!-- 图表区域 -->
<canvas id="statsChart" width="400" height="200"></canvas>
<!-- 数据记录表格 -->
<table id="matchHistory">
<thead>
<tr>
<th>比赛日期</th>
<th>对手</th>
<th>传中数</th>
<th>争顶成功</th>
<th>进球</th>
</tr>
</thead>
<tbody id="historyBody"></tbody>
</table>
</div>
<script>
async function loadStats() {
const playerId = document.getElementById('playerSelect').value;
if (!playerId) return;
try {
const response = await fetch(`/api/heading-stats/player/${playerId}`);
const data = await response.json();
if (data.status === 'success') {
updateUI(data.data);
}
} catch (error) {
console.error('Error loading stats:', error);
}
}
function updateUI(data) {
// 更新统计卡片
document.getElementById('crossRate').textContent =
data.basic_stats.cross_success_rate + '%';
document.getElementById('headingRate').textContent =
data.basic_stats.heading_success_rate + '%';
document.getElementById('headerGoals').textContent =
data.basic_stats.header_goals;
document.getElementById('overallEfficiency').textContent =
data.basic_stats.overall_efficiency + '%';
// 更新图表
updateChart(data.basic_stats);
// 更新比赛历史
updateMatchHistory(data.match_history);
}
function updateChart(stats) {
const ctx = document.getElementById('statsChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['传中成功率', '争顶成功率', '头球进球率', '综合效率'],
datasets: [{
label: '成功率 (%)',
data: [
stats.cross_success_rate,
stats.heading_success_rate,
stats.header_goal_rate,
stats.overall_efficiency
],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)'
],
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
}
function updateMatchHistory(matches) {
const tbody = document.getElementById('historyBody');
tbody.innerHTML = '';
matches.forEach(match => {
const row = tbody.insertRow();
row.innerHTML = `
<td>${match.match_date}</td>
<td>${match.opponent}</td>
<td>${match.crosses}</td>
<td>${match.heading_wins}</td>
<td>${match.goals}</td>
`;
});
}
</script>
</body>
</html>
高级统计功能
<?php
class AdvancedHeadingStats extends HeadingSuccessRateCalculator {
/**
* 计算移动平均成功率(用于趋势分析)
*/
public function calculateMovingAverage($playerId, $windowSize = 5) {
$query = "SELECT
c.create_time as match_date,
COUNT(*) as crosses,
SUM(CASE WHEN hd.is_won = 1 THEN 1 ELSE 0 END) as heading_wins
FROM crosses c
LEFT JOIN headed_duels hd ON hd.cross_id = c.id
WHERE c.player_id = ?
GROUP BY c.match_id
ORDER BY c.create_time
LIMIT 50";
$stmt = $this->db->prepare($query);
$stmt->execute([$playerId]);
$matches = $stmt->fetchAll(PDO::FETCH_ASSOC);
$movingAvgs = [];
for ($i = 0; $i < count($matches); $i++) {
$start = max(0, $i - $windowSize + 1);
$windowData = array_slice($matches, $start, $windowSize);
$totalCrosses = array_sum(array_column($windowData, 'crosses'));
$totalWins = array_sum(array_column($windowData, 'heading_wins'));
$movingAvgs[] = [
'date' => $matches[$i]['match_date'],
'moving_avg' => $totalCrosses > 0 ?
round(($totalWins / $totalCrosses) * 100, 2) : 0
];
}
return $movingAvgs;
}
/**
* 位置区域成功率分析
*/
public function getZoneSuccessRates($playerId) {
// 将球场分为多个区域
$zones = [
'left_wing' => [0, 33], // 左路
'center' => [33, 66], // 中路
'right_wing' => [66, 100] // 右路
];
$results = [];
foreach ($zones as $zoneName => $zone) {
$query = "SELECT
COUNT(*) as total,
SUM(CASE WHEN hd.is_won = 1 THEN 1 ELSE 0 END) as won
FROM crosses c
LEFT JOIN headed_duels hd ON hd.cross_id = c.id
WHERE c.player_id = ?
AND c.from_x BETWEEN ? AND ?";
$stmt = $this->db->prepare($query);
$stmt->execute([$playerId, $zone[0], $zone[1]]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$results[$zoneName] = [
'total' => $result['total'],
'won' => $result['won'],
'rate' => $result['total'] > 0 ?
round(($result['won'] / $result['total']) * 100, 2) : 0
];
}
return $results;
}
}
这个系统提供了:
- 基础统计:传中成功率、争顶成功率、进球效率
- 详细分析:按区域统计、时间趋势、对手比较
- 可视化:通过图表展示统计数据
- 灵活查询:按时间范围、球员、球队筛选
- 扩展性:可添加更多高级分析功能
你可以根据实际需求调整字段和统计逻辑,如果需要更多特定功能的实现,请告诉我。