Commit 0c953e5c authored by 刘小敏's avatar 刘小敏

数据中心优化;计划任务日志优化;

parent fb3982b7
...@@ -180,45 +180,54 @@ class DataStatsCenter extends Command ...@@ -180,45 +180,54 @@ class DataStatsCenter extends Command
$currentRange = $this->buildRange($days, $snapshotTime); $currentRange = $this->buildRange($days, $snapshotTime);
$previousRange = $this->buildRange($days, $currentRange['start'] - 1); $previousRange = $this->buildRange($days, $currentRange['start'] - 1);
// 获取当前周期和上一周期的核心交易指标 $this->customLog('info', '周期时间:[ ' . date('Y-m-d', $currentRange['start']) . '] 至 [ ' . date('Y-m-d', $currentRange['end']) . ']');
$this->customLog('info', '开始获取当前&上一期统计周期核心交易指标');
// 获取当前周期和上一周期的核心交易指标 (成交总额、净利润率,客单价,订单数)
$this->customLog('info', PHP_EOL . '核心指标-开始构建 : 成交总额、净利润率,客单价,订单数,访客数,累计注册用户数,转化率');
$currentRevenue = $this->getRevenueMetrics($currentRange['start'], $currentRange['end']); $currentRevenue = $this->getRevenueMetrics($currentRange['start'], $currentRange['end']);
$this->customLog('info', '核心指标-当前周期-核心交易指标 成交总额、净利润率,客单价,订单数 : ' . json_encode($currentRevenue));
$previousRevenue = $this->getRevenueMetrics($previousRange['start'], $previousRange['end']); $previousRevenue = $this->getRevenueMetrics($previousRange['start'], $previousRange['end']);
$this->customLog('info', '核心指标-上一周期-核心交易指标 成交总额、净利润率,客单价,订单数 : ' . json_encode($previousRevenue));
// 获取当前周期和上一周期的访客数 // 获取当前周期和上一周期的访客数
$this->customLog('info', '开始获取当前&上一期统计周期访客数');
$currentVisitors = $this->getVisitorCount($currentRange['start'], $currentRange['end']); $currentVisitors = $this->getVisitorCount($currentRange['start'], $currentRange['end']);
$this->customLog('info', '核心指标-当前周期-访客数 : ' . $currentVisitors);
$previousVisitors = $this->getVisitorCount($previousRange['start'], $previousRange['end']); $previousVisitors = $this->getVisitorCount($previousRange['start'], $previousRange['end']);
$this->customLog('info', '核心指标-上一周期-访客数 : ' . $previousVisitors);
// 获取累计注册用户数(截至当前周期结束和上一周期结束) // 获取累计注册用户数(截至当前周期结束和上一周期结束)
$this->customLog('info', '开始获取当前&上一期统计周期累计注册用户数');
$currentRegisterTotal = $this->getRegisterCount(0, $currentRange['end']); $currentRegisterTotal = $this->getRegisterCount(0, $currentRange['end']);
$this->customLog('info', '核心指标-当前周期-累计注册用户数 : ' . $currentRegisterTotal);
$previousRegisterTotal = $this->getRegisterCount(0, $previousRange['end']); $previousRegisterTotal = $this->getRegisterCount(0, $previousRange['end']);
$this->customLog('info', '核心指标-上一周期-累计注册用户数 : ' . $previousRegisterTotal);
// 计算转化率(购买用户数/访客数) // 计算转化率(购买用户数/访客数)
$this->customLog('info', '开始计算当前&上一期统计周期转化率');
$currentConversion = $currentVisitors > 0 ? round($currentRevenue['buyer_count'] / $currentVisitors * 100, 2) : 0; $currentConversion = $currentVisitors > 0 ? round($currentRevenue['buyer_count'] / $currentVisitors * 100, 2) : 0;
$previousConversion = $previousVisitors > 0 ? round($previousRevenue['buyer_count'] / $previousVisitors * 100, 2) : 0; $previousConversion = $previousVisitors > 0 ? round($previousRevenue['buyer_count'] / $previousVisitors * 100, 2) : 0;
$this->customLog('info', '核心指标-当前周期-转化率 : ' . $currentConversion . ' 核心指标-上一周期-转化率 : ' . $previousConversion . PHP_EOL);
// 构建趋势数据 // 构建趋势数据
$this->customLog('info', '开始构建当前统计周期趋势数据'); $this->customLog('info', '趋势数据-开始构建 : 成交总额趋势、订单数趋势、注册用户增长趋势');
$trendData = [ $trendData = [
'amount' => $this->buildAmountTrend($days, $snapshotTime), 'amount' => $this->buildAmountTrend($days, $snapshotTime),
'orders' => $this->buildOrderTrend($days, $snapshotTime), 'orders' => $this->buildOrderTrend($days, $snapshotTime),
'register' => $this->buildRegisterTrend($days, $snapshotTime), 'register' => $this->buildRegisterTrend($days, $snapshotTime),
]; ];
$this->customLog('info', '趋势数据-统计结果 : ' . json_encode($trendData) . PHP_EOL);
// 构建漏斗数据 // 构建漏斗数据
$this->customLog('info', '开始构建当前统计周期转化漏斗数据'); $this->customLog('info', '转化漏斗数据-开始构建');
$funnelData = $this->buildFunnelData($currentRange['start'], $currentRange['end']); $funnelData = $this->buildFunnelData($currentRange['start'], $currentRange['end']);
$this->customLog('info', '转化漏斗数据-统计结果 : ' . json_encode($funnelData) . PHP_EOL);
// 构建价格分布 // 构建价格分布
$this->customLog('info', '开始构建当前统计周期客单价分布'); $this->customLog('info', '客单价分布-开始构建');
$priceDistribution = $this->buildPriceDistribution($currentRange['start'], $currentRange['end']); $priceDistribution = $this->buildPriceDistribution($currentRange['start'], $currentRange['end']);
$this->customLog('info', '客单价分布-统计结果 : ' . json_encode($priceDistribution) . PHP_EOL);
// 获取最近订单 // 获取最近订单 页面单独分页访问,不需要在快照中包含,暂时去掉
$this->customLog('info', '开始获取当前统计周期最近订单'); // $this->customLog('info', '开始获取当前统计周期最近订单');
$recentOrders = $this->getRecentOrders(10); // $recentOrders = $this->getRecentOrders(10);
// 组装完整的统计数据结构 // 组装完整的统计数据结构
return [ return [
...@@ -244,7 +253,7 @@ class DataStatsCenter extends Command ...@@ -244,7 +253,7 @@ class DataStatsCenter extends Command
'trend' => $trendData, 'trend' => $trendData,
'funnel' => $funnelData, 'funnel' => $funnelData,
'price_distribution' => $priceDistribution, 'price_distribution' => $priceDistribution,
'recent_orders' => $recentOrders, // 'recent_orders' => $recentOrders,
'extra' => [ 'extra' => [
'current_revenue' => $currentRevenue, 'current_revenue' => $currentRevenue,
'previous_revenue' => $previousRevenue, 'previous_revenue' => $previousRevenue,
...@@ -271,7 +280,7 @@ class DataStatsCenter extends Command ...@@ -271,7 +280,7 @@ class DataStatsCenter extends Command
'trend_json' => json_encode($stats['trend'], JSON_UNESCAPED_UNICODE), 'trend_json' => json_encode($stats['trend'], JSON_UNESCAPED_UNICODE),
'funnel_json' => json_encode($stats['funnel'], JSON_UNESCAPED_UNICODE), 'funnel_json' => json_encode($stats['funnel'], JSON_UNESCAPED_UNICODE),
'distribution_json' => json_encode($stats['price_distribution'], JSON_UNESCAPED_UNICODE), 'distribution_json' => json_encode($stats['price_distribution'], JSON_UNESCAPED_UNICODE),
'recent_orders_json' => json_encode($stats['recent_orders'], JSON_UNESCAPED_UNICODE), // 'recent_orders_json' => json_encode($stats['recent_orders'], JSON_UNESCAPED_UNICODE),
'extra_json' => json_encode($stats['extra'], JSON_UNESCAPED_UNICODE), 'extra_json' => json_encode($stats['extra'], JSON_UNESCAPED_UNICODE),
'updated_at' => $now, 'updated_at' => $now,
]; ];
...@@ -301,7 +310,7 @@ class DataStatsCenter extends Command ...@@ -301,7 +310,7 @@ class DataStatsCenter extends Command
*/ */
protected function buildRange($days, $endTime) protected function buildRange($days, $endTime)
{ {
$endTime = (int)$endTime; $endTime = strtotime(date('Y-m-d 23:59:59', $endTime));
// 计算开始时间:从结束时间往前推 days-1 天的 00:00:00 // 计算开始时间:从结束时间往前推 days-1 天的 00:00:00
$startTime = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days', $endTime))); $startTime = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days', $endTime)));
...@@ -332,15 +341,15 @@ class DataStatsCenter extends Command ...@@ -332,15 +341,15 @@ class DataStatsCenter extends Command
->where('paid_time', 'between', [$startTime, $endTime]) ->where('paid_time', 'between', [$startTime, $endTime])
->count(); ->count();
// 统计购买用户数(去重) // 统计购买用户数(去重)-用于计算转化率
$buyerCount = (int)Db::name('shopro_order') $buyerCount = (int)Db::name('shopro_order')
->whereIn('status', ['paid', 'completed']) ->whereIn('status', ['paid', 'completed'])
->where('paid_time', 'between', [$startTime, $endTime]) ->where('paid_time', 'between', [$startTime, $endTime])
->where('user_id', '>', 0) ->where('user_id', '>', 0)
->count('DISTINCT user_id'); ->count('DISTINCT user_id');
// 计算客单价(成交额/订单数) // 计算客单价(成交额/购买用户数)
$avgAmount = $orderCount > 0 ? round($totalAmount / $orderCount, 2) : 0; $avgAmount = $buyerCount > 0 ? round($totalAmount / $buyerCount, 2) : 0;
// 统计商品成本(成本价 × 数量) // 统计商品成本(成本价 × 数量)
$table = $this->getFullTableName('shopro_order_item'); $table = $this->getFullTableName('shopro_order_item');
...@@ -396,6 +405,7 @@ class DataStatsCenter extends Command ...@@ -396,6 +405,7 @@ class DataStatsCenter extends Command
WHERE create_time BETWEEN ? AND ?"; WHERE create_time BETWEEN ? AND ?";
$result = Db::query($sql, [$startTime, $endTime]); $result = Db::query($sql, [$startTime, $endTime]);
$count = isset($result[0]['total']) ? (int)$result[0]['total'] : 0; $count = isset($result[0]['total']) ? (int)$result[0]['total'] : 0;
return $count; return $count;
...@@ -440,7 +450,7 @@ class DataStatsCenter extends Command ...@@ -440,7 +450,7 @@ class DataStatsCenter extends Command
{ {
try { try {
// 获取各阶段的用户数 // 获取各阶段的用户数
$this->customLog('info', '开始获取当前统计周期访问用户数'); $this->customLog('info', '转化漏斗数据-开始获取当前统计周期访问用户数');
$visit = $this->getTrackVisitorCount($startTime, $endTime); $visit = $this->getTrackVisitorCount($startTime, $endTime);
$browse = $this->getTrackGoodViewUserCount($startTime, $endTime); $browse = $this->getTrackGoodViewUserCount($startTime, $endTime);
$cart = $this->getTrackCartAddUserCount($startTime, $endTime); $cart = $this->getTrackCartAddUserCount($startTime, $endTime);
...@@ -448,7 +458,7 @@ class DataStatsCenter extends Command ...@@ -448,7 +458,7 @@ class DataStatsCenter extends Command
$pay = $this->getTrackPayConfirmUserCount($startTime, $endTime); $pay = $this->getTrackPayConfirmUserCount($startTime, $endTime);
// 获取各阶段的事件数 // 获取各阶段的事件数
$this->customLog('info', '开始获取当前统计周期访问事件数'); $this->customLog('info', '转化漏斗数据-开始获取当前统计周期访问事件数');
$visitCount = $this->getTrackVisitCount($startTime, $endTime); $visitCount = $this->getTrackVisitCount($startTime, $endTime);
$browseCount = $this->getTrackGoodViewCount($startTime, $endTime); $browseCount = $this->getTrackGoodViewCount($startTime, $endTime);
$cartCount = $this->getTrackCartAddCount($startTime, $endTime); $cartCount = $this->getTrackCartAddCount($startTime, $endTime);
...@@ -468,6 +478,7 @@ class DataStatsCenter extends Command ...@@ -468,6 +478,7 @@ class DataStatsCenter extends Command
]; ];
} catch (\Exception $e) { } catch (\Exception $e) {
$this->customLog('warning', '使用埋点表构建转化漏斗失败' . $e->getMessage()); $this->customLog('warning', '使用埋点表构建转化漏斗失败' . $e->getMessage());
return [];
} }
} }
...@@ -529,7 +540,7 @@ class DataStatsCenter extends Command ...@@ -529,7 +540,7 @@ class DataStatsCenter extends Command
END) as total END) as total
FROM `{$table}` FROM `{$table}`
WHERE create_time BETWEEN ? AND ? WHERE create_time BETWEEN ? AND ?
AND (event_code = 'good_view' OR page_path LIKE '/pages/goods/%')"; AND (event_code = 'good_view' )";
$result = Db::query($sql, [$startTime, $endTime]); $result = Db::query($sql, [$startTime, $endTime]);
return (int)($result[0]['total'] ?? 0); return (int)($result[0]['total'] ?? 0);
} }
......
...@@ -9,10 +9,13 @@ use think\Db; ...@@ -9,10 +9,13 @@ use think\Db;
use think\Queue; use think\Queue;
/** /**
* 每日统计任务 PV UV 等指标 * 每日统计任务 PV UV 等指标
* 每天凌晨1点半执行 * 每天凌晨1点半执行
* *
* 任务描述:每天凌晨1点半执行,统计昨日的PV UV 等指标 * 任务描述:每天凌晨1点半执行,统计昨日的PV UV 等指标
*
* 暂时没有业务用得到这张表,所以先不执行,待后续业务需要时,再开启执行 2026.05.25
*
*/ */
class StatsDaily extends Command class StatsDaily extends Command
{ {
......
...@@ -9,7 +9,7 @@ use think\Db; ...@@ -9,7 +9,7 @@ use think\Db;
use think\Queue; use think\Queue;
/** /**
* 重试失败的埋点数据 PV UV 等指标 * 重试失败的埋点数据 PV UV 等指标
* 每1个小时执行一次 * 每1个小时执行一次
* *
* 任务描述:每1个小时执行一次,重试失败的埋点数据 PV UV 等指标 * 任务描述:每1个小时执行一次,重试失败的埋点数据 PV UV 等指标
...@@ -32,7 +32,7 @@ class TrackFailedRetry extends Command ...@@ -32,7 +32,7 @@ class TrackFailedRetry extends Command
protected function customLog($level, $message) { protected function customLog($level, $message) {
$this->output->writeln($message); $this->output->writeln($message);
custom_log($message, 'stat', $level); custom_log($message, 'stat_track_failed_retry', $level);
} }
protected function execute(Input $input, Output $output) { protected function execute(Input $input, Output $output) {
......
...@@ -494,14 +494,19 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function ( ...@@ -494,14 +494,19 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
return false; return false;
}); });
} }
// 加载转化漏斗数据
if (data.funnel_data) {
updateFunnel(data.funnel_data);
}
// 加载价格分布数据
if (data.price_distribution_data) {
updatePriceDistribution(data.price_distribution_data);
}
} }
return false; return false;
}); });
// 同时加载其他数据
loadFunnel(timeRange);
loadPriceDistribution(timeRange);
loadRecentOrders();
} }
/** /**
...@@ -557,93 +562,73 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function ( ...@@ -557,93 +562,73 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
} }
/** /**
* 加载转化漏斗数据 * 更新转化漏斗数据
* @param {string} timeRange - 时间范围 * @param {object} funnelData - 漏斗数据对象
*/ */
function loadFunnel(timeRange) { function updateFunnel(funnelData) {
Fast.api.ajax({ var max = funnelData.visit || 1;
url: 'shopro/dashboard/funnel',
type: 'GET',
data: { time_range: timeRange }
}, function (ret, res) {
if (res.code === 1) {
var data = res.data;
var max = data.visit || 1;
var funnelVisit = document.getElementById('funnel-visit'); var funnelVisit = document.getElementById('funnel-visit');
if (funnelVisit) funnelVisit.textContent = formatNumber(data.visit); if (funnelVisit) funnelVisit.textContent = formatNumber(funnelData.visit);
var funnelBrowse = document.getElementById('funnel-browse'); var funnelBrowse = document.getElementById('funnel-browse');
var browsePercent = max > 0 ? Math.round(data.browse / max * 100) : 0; var browsePercent = max > 0 ? Math.round(funnelData.browse / max * 100) : 0;
if (funnelBrowse) { if (funnelBrowse) {
funnelBrowse.textContent = formatNumber(data.browse); funnelBrowse.textContent = formatNumber(funnelData.browse);
funnelBrowse.style.width = browsePercent + '%'; funnelBrowse.style.width = browsePercent + '%';
} }
var funnelCart = document.getElementById('funnel-cart'); var funnelCart = document.getElementById('funnel-cart');
var cartPercent = max > 0 ? Math.round(data.cart / max * 100) : 0; var cartPercent = max > 0 ? Math.round(funnelData.cart / max * 100) : 0;
if (funnelCart) { if (funnelCart) {
funnelCart.textContent = formatNumber(data.cart); funnelCart.textContent = formatNumber(funnelData.cart);
funnelCart.style.width = cartPercent + '%'; funnelCart.style.width = cartPercent + '%';
} }
var funnelSubmit = document.getElementById('funnel-submit'); var funnelSubmit = document.getElementById('funnel-submit');
var submitPercent = max > 0 ? Math.round(data.submit_order / max * 100) : 0; var submitPercent = max > 0 ? Math.round(funnelData.submit_order / max * 100) : 0;
if (funnelSubmit) { if (funnelSubmit) {
funnelSubmit.textContent = formatNumber(data.submit_order); funnelSubmit.textContent = formatNumber(funnelData.submit_order);
funnelSubmit.style.width = submitPercent + '%'; funnelSubmit.style.width = submitPercent + '%';
} }
var funnelPay = document.getElementById('funnel-pay'); var funnelPay = document.getElementById('funnel-pay');
var payPercent = max > 0 ? Math.round(data.pay / max * 100) : 0; var payPercent = max > 0 ? Math.round(funnelData.pay / max * 100) : 0;
if (funnelPay) { if (funnelPay) {
funnelPay.textContent = formatNumber(data.pay); funnelPay.textContent = formatNumber(funnelData.pay);
funnelPay.style.width = payPercent + '%'; funnelPay.style.width = payPercent + '%';
} }
} }
return false;
});
}
/** /**
* 加载价格分布数据 * 更新价格分布数据
* @param {string} timeRange - 时间范围 * @param {object} distributionData - 价格分布数据对象
*/ */
function loadPriceDistribution(timeRange) { function updatePriceDistribution(distributionData) {
Fast.api.ajax({
url: 'shopro/dashboard/priceDistribution',
type: 'GET',
data: { time_range: timeRange }
}, function (ret, res) {
if (res.code === 1) {
var data = res.data;
var price0100 = document.getElementById('price-0-100'); var price0100 = document.getElementById('price-0-100');
var price0100Percent = document.getElementById('price-0-100-percent'); var price0100Percent = document.getElementById('price-0-100-percent');
if (price0100) price0100.style.width = data['0-100'] + '%'; if (price0100) price0100.style.width = (distributionData['0-100'] || 0) + '%';
if (price0100Percent) price0100Percent.textContent = data['0-100'] + '%'; if (price0100Percent) price0100Percent.textContent = (distributionData['0-100'] || 0) + '%';
var price100300 = document.getElementById('price-100-300'); var price100300 = document.getElementById('price-100-300');
var price100300Percent = document.getElementById('price-100-300-percent'); var price100300Percent = document.getElementById('price-100-300-percent');
if (price100300) price100300.style.width = data['100-300'] + '%'; if (price100300) price100300.style.width = (distributionData['100-300'] || 0) + '%';
if (price100300Percent) price100300Percent.textContent = data['100-300'] + '%'; if (price100300Percent) price100300Percent.textContent = (distributionData['100-300'] || 0) + '%';
var price300500 = document.getElementById('price-300-500'); var price300500 = document.getElementById('price-300-500');
var price300500Percent = document.getElementById('price-300-500-percent'); var price300500Percent = document.getElementById('price-300-500-percent');
if (price300500) price300500.style.width = data['300-500'] + '%'; if (price300500) price300500.style.width = (distributionData['300-500'] || 0) + '%';
if (price300500Percent) price300500Percent.textContent = data['300-500'] + '%'; if (price300500Percent) price300500Percent.textContent = (distributionData['300-500'] || 0) + '%';
var price5001000 = document.getElementById('price-500-1000'); var price5001000 = document.getElementById('price-500-1000');
var price5001000Percent = document.getElementById('price-500-1000-percent'); var price5001000Percent = document.getElementById('price-500-1000-percent');
if (price5001000) price5001000.style.width = data['500-1000'] + '%'; if (price5001000) price5001000.style.width = (distributionData['500-1000'] || 0) + '%';
if (price5001000Percent) price5001000Percent.textContent = data['500-1000'] + '%'; if (price5001000Percent) price5001000Percent.textContent = (distributionData['500-1000'] || 0) + '%';
var price1000 = document.getElementById('price-1000'); var price1000 = document.getElementById('price-1000');
var price1000Percent = document.getElementById('price-1000-percent'); var price1000Percent = document.getElementById('price-1000-percent');
if (price1000) price1000.style.width = data['1000+'] + '%'; if (price1000) price1000.style.width = (distributionData['1000+'] || 0) + '%';
if (price1000Percent) price1000Percent.textContent = data['1000+'] + '%'; if (price1000Percent) price1000Percent.textContent = (distributionData['1000+'] || 0) + '%';
}
return false;
});
} }
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment