本文目录导读:

我来帮你设计一个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;
}
}
这个系统提供了:
- 基础统计:传中成功率、争顶成功率、进球效率
- 详细分析:按区域统计、时间趋势、对手比较
- 可视化:通过图表展示统计数据
- 灵活查询:按时间范围、球员、球队筛选
- 扩展性:可添加更多高级分析功能
你可以根据实际需求调整字段和统计逻辑,如果需要更多特定功能的实现,请告诉我。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播