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

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

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