Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
L
loveisland
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
刘小敏
loveisland
Commits
8da5c178
Commit
8da5c178
authored
May 22, 2026
by
刘小敏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
数据统计中心计划任务脚本,页面,接口
parent
e7e8b184
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
2103 additions
and
379 deletions
+2103
-379
TrackProcessJob.php
addons/shopro/job/TrackProcessJob.php
+67
-8
Dashboard.php
application/admin/controller/shopro/Dashboard.php
+643
-274
script.html
application/admin/view/shopro/common/script.html
+2
-0
index.html
application/admin/view/shopro/dashboard/index.html
+262
-82
command.php
application/command.php
+4
-0
DataStatsCenter.php
application/command/DataStatsCenter.php
+1001
-0
TrackFailedRetry.php
application/command/TrackFailedRetry.php
+107
-3
dashboard.js
public/assets/js/backend/shopro/dashboard.js
+17
-12
No files found.
addons/shopro/job/TrackProcessJob.php
View file @
8da5c178
...
...
@@ -7,6 +7,10 @@ use think\queue\Job;
use
think\Db
;
use
think\Queue
;
/**
* 埋点数据处理任务
* PV UV 等统计
*/
class
TrackProcessJob
{
/**
...
...
@@ -16,6 +20,7 @@ class TrackProcessJob
try
{
$trackData
=
$data
[
'data'
]
??
[];
$clientIp
=
$data
[
'ip'
]
??
''
;
$jobTimestamp
=
isset
(
$data
[
'timestamp'
])
?
(
int
)
$data
[
'timestamp'
]
:
time
();
if
(
empty
(
$trackData
))
{
$job
->
delete
();
...
...
@@ -23,7 +28,7 @@ class TrackProcessJob
}
// 批量插入埋点数据
$this
->
insertTracks
(
$trackData
,
$clientIp
);
$this
->
insertTracks
(
$trackData
,
$clientIp
,
$jobTimestamp
);
// 删除任务
$job
->
delete
();
...
...
@@ -43,22 +48,39 @@ class TrackProcessJob
/**
* 批量插入埋点数据
*/
protected
function
insertTracks
(
$trackData
,
$clientIp
)
{
protected
function
insertTracks
(
$trackData
,
$clientIp
,
$jobTimestamp
)
{
$insertData
=
[];
$
now
=
time
()
;
$
seen
=
[]
;
foreach
(
$trackData
as
$item
)
{
$
insertData
[]
=
[
$
trackRow
=
[
'user_id'
=>
$item
[
'user_id'
]
??
0
,
'openid'
=>
$item
[
'openid'
]
??
''
,
'session_id'
=>
$item
[
'session_id'
]
??
''
,
'event_type'
=>
$item
[
'event_type'
]
??
''
,
'event_code'
=>
$item
[
'event_code'
]
??
''
,
'page_path'
=>
$item
[
'page_path'
]
??
''
,
'page_title'
=>
$item
[
'page_title'
]
??
''
,
'params'
=>
!
empty
(
$item
[
'params'
])
?
json_encode
(
$item
[
'params'
])
:
''
,
'ip'
=>
$clientIp
,
'create_time'
=>
isset
(
$item
[
'create_time'
])
?
(
int
)(
$item
[
'create_time'
]
/
1000
)
:
$
now
'create_time'
=>
isset
(
$item
[
'create_time'
])
?
(
int
)(
$item
[
'create_time'
]
/
1000
)
:
$
jobTimestamp
];
$dedupKey
=
$this
->
buildTrackDedupKey
(
$trackRow
);
if
(
isset
(
$seen
[
$dedupKey
]))
{
continue
;
}
$seen
[
$dedupKey
]
=
true
;
if
(
$this
->
trackExists
(
$trackRow
))
{
continue
;
}
$insertData
[]
=
$trackRow
;
}
if
(
empty
(
$insertData
))
{
return
;
}
// 分批插入(每批500条)
...
...
@@ -69,10 +91,48 @@ class TrackProcessJob
}
/**
* 构建埋点判重键
*/
protected
function
buildTrackDedupKey
(
$trackRow
)
{
return
md5
(
implode
(
'|'
,
[
(
string
)
$trackRow
[
'user_id'
],
(
string
)
$trackRow
[
'openid'
],
(
string
)
$trackRow
[
'session_id'
],
(
string
)
$trackRow
[
'event_type'
],
(
string
)
$trackRow
[
'event_code'
],
(
string
)
$trackRow
[
'page_path'
],
(
string
)
$trackRow
[
'page_title'
],
(
string
)
$trackRow
[
'params'
],
(
string
)
$trackRow
[
'ip'
],
(
string
)
$trackRow
[
'create_time'
],
]));
}
/**
* 判断埋点是否已存在
*/
protected
function
trackExists
(
$trackRow
)
{
return
Db
::
name
(
'shopro_miniprogram_track'
)
->
where
(
'user_id'
,
$trackRow
[
'user_id'
])
->
where
(
'openid'
,
$trackRow
[
'openid'
])
->
where
(
'session_id'
,
$trackRow
[
'session_id'
])
->
where
(
'event_type'
,
$trackRow
[
'event_type'
])
->
where
(
'event_code'
,
$trackRow
[
'event_code'
])
->
where
(
'page_path'
,
$trackRow
[
'page_path'
])
->
where
(
'page_title'
,
$trackRow
[
'page_title'
])
->
where
(
'params'
,
$trackRow
[
'params'
])
->
where
(
'ip'
,
$trackRow
[
'ip'
])
->
where
(
'create_time'
,
$trackRow
[
'create_time'
])
->
find
()
?
true
:
false
;
}
/**
* 保存失败数据
*/
protected
function
saveFailed
(
$data
,
$error
)
{
Db
::
name
(
'shopro_track_failed'
)
->
insert
([
Db
::
name
(
'shopro_
miniprogram_
track_failed'
)
->
insert
([
'data'
=>
json_encode
(
$data
),
'error'
=>
$error
,
'attempts'
=>
3
,
...
...
@@ -81,4 +141,4 @@ class TrackProcessJob
}
}
\ No newline at end of file
}
application/admin/controller/shopro/Dashboard.php
View file @
8da5c178
...
...
@@ -10,9 +10,30 @@ use app\admin\model\shopro\Share;
use
addons\shopro\service\SearchHistory
;
use
think\Db
;
/**
* 数据统计中心控制器
*
* 负责管理后台数据统计页面的数据接口,包括:
* - 核心业务指标统计(成交额、订单数、客单价、利润率等)
* - 趋势数据(支持日/周/月周期切换)
* - 转化漏斗分析
* - 客单价分布
* - 实时订单动态
*
* 数据来源优先级:
* 1. 从缓存读取 DataStatsCenter 定时任务生成的统计数据
* 2. 缓存未命中时,降级到数据库实时查询
*/
class
Dashboard
extends
Common
{
/**
* 数据统计中心首页
*
* 返回数据统计中心页面,配置卡片权限
*
* @return mixed 渲染后的视图
*/
public
function
index
()
{
$cardList
=
[
...
...
@@ -37,85 +58,271 @@ class Dashboard extends Common
}
/**
* 获取统计数据
* 获取核心统计数据
*
* 该接口优先从缓存读取 DataStatsCenter 定时任务生成的统计数据,
* 缓存未命中时降级到数据库实时查询。
*
* 请求参数:
* - time_range: 时间范围,可选值:'7days'、'30days'、'90days',默认 '30days'
* - period: 周期类型,可选值:'day'、'week'、'month',默认 'day'
*
* 返回数据:
* - total_amount: 成交总额(已支付和已完成订单的支付金额总和)
* - profit_rate: 净利润率(百分比格式,如 '18.6%')
* - avg_amount: 客单价(成交总额 / 订单数)
* - order_count: 订单数(已支付和已完成订单数量)
* - visitor_count: 访客数(基于小程序行为埋点统计的去重用户数)
* - conversion_rate: 转化率(订单数 / 访客数,百分比格式)
* - register_users: 注册用户数(累计注册用户总数)
* - trend_data: 趋势数据数组,每个元素包含:
* - date: 日期标签
* - amount: 该日期的成交额
* - order_num: 该日期的订单数
* - register_num: 该日期的注册用户数
* - from_cache: 数据来源标识,true 表示来自缓存,false 表示来自实时查询
*/
public
function
statistics
()
{
$timeRange
=
$this
->
request
->
param
(
'time_range'
,
'30days'
);
$period
=
$this
->
request
->
param
(
'period'
,
'day'
);
$cacheKey
=
'shopro:data_stats_center:'
.
$timeRange
;
$cachedData
=
cache
(
$cacheKey
);
if
(
$cachedData
!==
false
&&
isset
(
$cachedData
[
'summary'
])
&&
isset
(
$cachedData
[
'trend'
]))
{
$summary
=
$cachedData
[
'summary'
];
$trendData
=
$this
->
buildTrendDataForChart
(
$cachedData
[
'trend'
],
$period
);
$result
=
[
'total_amount'
=>
$summary
[
'total_amount'
][
'value'
],
'profit_rate'
=>
$summary
[
'profit_rate'
][
'formatted'
],
'avg_amount'
=>
$summary
[
'avg_amount'
][
'value'
],
'order_count'
=>
$summary
[
'order_count'
][
'value'
],
'visitor_count'
=>
$summary
[
'visitor_count'
][
'value'
],
'conversion_rate'
=>
$summary
[
'conversion_rate'
][
'formatted'
],
'register_users'
=>
$summary
[
'register_users'
][
'value'
],
'trend_data'
=>
$trendData
,
'from_cache'
=>
true
];
// 根据时间范围获取条件
$timeCondition
=
$this
->
getTimeCondition
(
$timeRange
);
$this
->
success
(
'获取成功'
,
null
,
$result
);
}
// 成交总额
$totalAmount
=
Order
::
whereTime
(
'createtime'
,
$timeCondition
)
->
whereIn
(
'status'
,
[
Order
::
STATUS_PAID
,
Order
::
STATUS_COMPLETED
])
->
sum
(
'pay_fee'
)
?:
0
;
$days
=
$this
->
getDaysByTimeRange
(
$timeRange
);
$trendData
=
$this
->
getTrendData
(
$days
,
$period
);
$result
=
[
'total_amount'
=>
$trendData
[
'total_amount'
],
'profit_rate'
=>
$trendData
[
'profit_rate'
],
'avg_amount'
=>
$trendData
[
'avg_amount'
],
'order_count'
=>
$trendData
[
'order_count'
],
'visitor_count'
=>
$trendData
[
'visitor_count'
],
'conversion_rate'
=>
$trendData
[
'conversion_rate'
],
'register_users'
=>
$trendData
[
'register_users'
],
'trend_data'
=>
$trendData
[
'trend_data'
],
'from_cache'
=>
false
];
// 订单数
$orderCount
=
Order
::
whereTime
(
'createtime'
,
$timeCondition
)
->
whereIn
(
'status'
,
[
Order
::
STATUS_PAID
,
Order
::
STATUS_COMPLETED
])
->
count
();
$this
->
success
(
'获取成功'
,
null
,
$result
);
}
// 客单价
$avgAmount
=
$orderCount
>
0
?
bcdiv
(
$totalAmount
,
$orderCount
,
2
)
:
0
;
/**
* 根据周期聚合趋势数据
*
* 将按天统计的趋势数据按照指定的周期(日/周/月)进行聚合
*
* @param array $trendData 趋势数据,包含 amount、orders、register 三个子数组
* @param string $period 周期类型:'day'、'week'、'month'
* @return array 聚合后的趋势数据,每个元素包含 date、amount、order_num、register_num
*/
private
function
buildTrendDataForChart
(
$trendData
,
$period
)
{
$amountData
=
$trendData
[
'amount'
];
$orderData
=
$trendData
[
'orders'
];
$registerData
=
$trendData
[
'register'
];
// 净利润率 (模拟计算:假设成本率80%)
$profitRate
=
$totalAmount
>
0
?
'18.6%'
:
'0%'
;
$labels
=
$amountData
[
'labels'
];
$amountValues
=
$amountData
[
'values'
];
$orderValues
=
$orderData
[
'values'
];
$registerValues
=
$registerData
[
'new_users'
];
// 访客数 (从用户商品浏览日志表获取)
$visitorCount
=
0
;
try
{
$visitorCount
=
Db
::
name
(
'shopro_user_goods_log'
)
->
whereTime
(
'createtime'
,
$timeCondition
)
->
distinct
(
true
)
->
count
(
'user_id'
);
}
catch
(
\Exception
$e
)
{
$visitorCount
=
0
;
// 强制按天展示数据,确保图表显示完整的每日数据
$result
=
[];
foreach
(
$labels
as
$index
=>
$label
)
{
$result
[]
=
[
'date'
=>
$label
,
'amount'
=>
$amountValues
[
$index
]
??
0
,
'order_num'
=>
$orderValues
[
$index
]
??
0
,
'register_num'
=>
$registerValues
[
$index
]
??
0
];
}
return
$result
;
// 周/月聚合逻辑(暂时禁用,需要时取消注释)
/*
if ($period === 'week') {
return $this->aggregateByWeek($labels, $amountValues, $orderValues, $registerValues);
} elseif ($period === 'month') {
return $this->aggregateByMonth($labels, $amountValues, $orderValues, $registerValues);
}
// 转化率 = 支付订单数 / 访客数
$conversionRate
=
$visitorCount
>
0
?
bcdiv
(
$orderCount
*
100
,
$visitorCount
,
2
)
.
'%'
:
'0%'
;
$result = [];
foreach ($labels as $index => $label) {
$result[] = [
'date' => $label,
'amount' => $amountValues[$index] ?? 0,
'order_num' => $orderValues[$index] ?? 0,
'register_num' => $registerValues[$index] ?? 0
];
}
return $result;
*/
}
// 注册用户
$registerUsers
=
User
::
whereTime
(
'createtime'
,
$timeCondition
)
->
count
();
/**
* 按周聚合数据
*
* 将按天统计的数据按周聚合,每周从周一开始计算
*
* @param array $labels 日期标签数组
* @param array $amountValues 成交额数组
* @param array $orderValues 订单数数组
* @param array $registerValues 注册用户数数组
* @return array 按周聚合后的数据,date 字段格式为 '第X周'
*/
private
function
aggregateByWeek
(
$labels
,
$amountValues
,
$orderValues
,
$registerValues
)
{
$result
=
[];
$currentWeek
=
0
;
$weekAmount
=
0
;
$weekOrder
=
0
;
$weekRegister
=
0
;
foreach
(
$labels
as
$index
=>
$label
)
{
$date
=
date
(
'Y'
)
.
'-'
.
$label
;
$dayOfWeek
=
date
(
'w'
,
strtotime
(
$date
));
if
(
$dayOfWeek
==
1
||
$index
==
0
)
{
if
(
$index
>
0
)
{
$result
[]
=
[
'date'
=>
'第'
.
$currentWeek
.
'周'
,
'amount'
=>
$weekAmount
,
'order_num'
=>
$weekOrder
,
'register_num'
=>
$weekRegister
];
}
$currentWeek
++
;
$weekAmount
=
0
;
$weekOrder
=
0
;
$weekRegister
=
0
;
}
$weekAmount
+=
$amountValues
[
$index
]
??
0
;
$weekOrder
+=
$orderValues
[
$index
]
??
0
;
$weekRegister
+=
$registerValues
[
$index
]
??
0
;
}
// 获取趋势数据
$trendData
=
$this
->
getTrendData
(
$timeRange
);
if
(
$currentWeek
>
0
)
{
$result
[]
=
[
'date'
=>
'第'
.
$currentWeek
.
'周'
,
'amount'
=>
$weekAmount
,
'order_num'
=>
$weekOrder
,
'register_num'
=>
$weekRegister
];
}
$this
->
success
(
'获取成功'
,
null
,
[
'total_amount'
=>
$totalAmount
,
'profit_rate'
=>
$profitRate
,
'avg_amount'
=>
$avgAmount
,
'order_count'
=>
$orderCount
,
'visitor_count'
=>
$visitorCount
,
'conversion_rate'
=>
$conversionRate
,
'register_users'
=>
$registerUsers
,
'trend_data'
=>
$trendData
]);
return
$result
;
}
/**
* 按月聚合数据
*
* 将按天统计的数据按月聚合
*
* @param array $labels 日期标签数组
* @param array $amountValues 成交额数组
* @param array $orderValues 订单数数组
* @param array $registerValues 注册用户数数组
* @return array 按月聚合后的数据,date 字段格式为 'XX月'
*/
private
function
aggregateByMonth
(
$labels
,
$amountValues
,
$orderValues
,
$registerValues
)
{
$result
=
[];
$currentMonth
=
''
;
$monthAmount
=
0
;
$monthOrder
=
0
;
$monthRegister
=
0
;
foreach
(
$labels
as
$index
=>
$label
)
{
$month
=
substr
(
$label
,
0
,
2
);
if
(
$month
!=
$currentMonth
||
$index
==
0
)
{
if
(
$index
>
0
)
{
$result
[]
=
[
'date'
=>
$currentMonth
.
'月'
,
'amount'
=>
$monthAmount
,
'order_num'
=>
$monthOrder
,
'register_num'
=>
$monthRegister
];
}
$currentMonth
=
$month
;
$monthAmount
=
0
;
$monthOrder
=
0
;
$monthRegister
=
0
;
}
$monthAmount
+=
$amountValues
[
$index
]
??
0
;
$monthOrder
+=
$orderValues
[
$index
]
??
0
;
$monthRegister
+=
$registerValues
[
$index
]
??
0
;
}
if
(
$currentMonth
)
{
$result
[]
=
[
'date'
=>
$currentMonth
.
'月'
,
'amount'
=>
$monthAmount
,
'order_num'
=>
$monthOrder
,
'register_num'
=>
$monthRegister
];
}
return
$result
;
}
/**
* 获取时间条件
* 根据时间范围获取天数
*
* @param string $timeRange 时间范围标识
* @return int 对应的天数
*/
private
function
get
TimeCondition
(
$timeRange
)
private
function
get
DaysByTimeRange
(
$timeRange
)
{
switch
(
$timeRange
)
{
case
'7days'
:
return
'week'
;
return
7
;
case
'90days'
:
return
'month'
;
return
90
;
case
'30days'
:
default
:
return
'month'
;
return
30
;
}
}
/**
* 获取趋势数据
* 获取趋势数据(降级方案)
*
* 当缓存未命中时,直接从数据库查询并计算趋势数据
*
* @param int $days 统计天数
* @param string $period 周期类型
* @return array 包含各项指标和趋势数据的数组
*/
private
function
getTrendData
(
$
timeRange
)
private
function
getTrendData
(
$
days
,
$period
)
{
$days
=
$timeRange
==
'7days'
?
7
:
(
$timeRange
==
'90days'
?
90
:
30
);
$data
=
[];
$totalAmount
=
0
;
$orderCount
=
0
;
$registerUsers
=
0
;
$visitorCount
=
0
;
$format
=
$this
->
getDateFormat
(
$period
);
for
(
$i
=
$days
-
1
;
$i
>=
0
;
$i
--
)
{
$date
=
date
(
'Y-m-d'
,
strtotime
(
"-
$i
days"
));
$startTime
=
strtotime
(
$date
.
' 00:00:00'
);
...
...
@@ -131,51 +338,136 @@ class Dashboard extends Common
$registerNum
=
User
::
whereBetween
(
'createtime'
,
[
$startTime
,
$endTime
])
->
count
();
try
{
$visitorNum
=
Db
::
name
(
'shopro_user_goods_log'
)
->
whereBetween
(
'createtime'
,
[
$startTime
,
$endTime
])
->
distinct
(
true
)
->
count
(
'user_id'
);
}
catch
(
\Exception
$e
)
{
$visitorNum
=
0
;
}
$data
[]
=
[
'date'
=>
$date
,
'date'
=>
date
(
$format
,
$startTime
)
,
'amount'
=>
$amount
,
'order_num'
=>
$orderNum
,
'register_num'
=>
$registerNum
];
$totalAmount
+=
$amount
;
$orderCount
+=
$orderNum
;
$registerUsers
+=
$registerNum
;
$visitorCount
+=
$visitorNum
;
}
return
$data
;
$avgAmount
=
$orderCount
>
0
?
bcdiv
(
$totalAmount
,
$orderCount
,
2
)
:
0
;
$profitRate
=
$totalAmount
>
0
?
'18.6%'
:
'0%'
;
$conversionRate
=
$visitorCount
>
0
?
bcdiv
(
$orderCount
*
100
,
$visitorCount
,
2
)
.
'%'
:
'0%'
;
return
[
'total_amount'
=>
$totalAmount
,
'order_count'
=>
$orderCount
,
'avg_amount'
=>
$avgAmount
,
'profit_rate'
=>
$profitRate
,
'visitor_count'
=>
$visitorCount
,
'conversion_rate'
=>
$conversionRate
,
'register_users'
=>
$registerUsers
,
'trend_data'
=>
$data
];
}
/**
* 获取日期格式化字符串
*
* @param string $period 周期类型
* @return string PHP 日期格式化字符串
*/
private
function
getDateFormat
(
$period
)
{
switch
(
$period
)
{
case
'week'
:
return
'Y年W周'
;
case
'month'
:
return
'Y年m月'
;
case
'day'
:
default
:
return
'm-d'
;
}
}
/**
* 获取转化漏斗数据
*
* 统计用户从访问到完成支付的转化路径各环节的人数
* 优先从缓存读取,缓存未命中时降级到数据库查询
*
* 请求参数:
* - time_range: 时间范围,可选值:'7days'、'30days'、'90days',默认 '30days'
*
* 返回数据:
* - visit: 访问人数(基于小程序行为埋点统计的去重用户数)
* - browse: 浏览商品人数(type='browse' 的去重用户数)
* - cart: 加入购物车人数(type='cart' 的去重用户数)
* - submit_order: 提交订单人数(下单的去重用户数)
* - pay: 完成支付人数(已支付或已完成订单的去重用户数)
*/
public
function
funnel
()
{
$timeRange
=
$this
->
request
->
param
(
'time_range'
,
'30days'
);
$timeCondition
=
$this
->
getTimeCondition
(
$timeRange
);
// 访问用户数
$visitCount
=
Db
::
name
(
'shopro_user_goods_log'
)
->
whereTime
(
'createtime'
,
$timeCondition
)
->
distinct
(
true
)
->
count
(
'user_id'
);
$cacheKey
=
'shopro:data_stats_center:'
.
$timeRange
;
$cachedData
=
cache
(
$cacheKey
);
// 浏览商品用户数
$browseCount
=
Db
::
name
(
'shopro_user_goods_log'
)
->
whereTime
(
'createtime'
,
$timeCondition
)
->
where
(
'type'
,
'browse'
)
->
distinct
(
true
)
->
count
(
'user_id'
);
if
(
$cachedData
!==
false
&&
isset
(
$cachedData
[
'funnel'
]))
{
$funnel
=
$cachedData
[
'funnel'
];
$result
=
[];
foreach
(
$funnel
as
$item
)
{
$result
[
$item
[
'key'
]]
=
$item
[
'value'
];
}
$this
->
success
(
'获取成功'
,
null
,
$result
);
}
// 加入购物车用户数
$cartCount
=
Db
::
name
(
'shopro_user_goods_log'
)
->
whereTime
(
'createtime'
,
$timeCondition
)
->
where
(
'type'
,
'cart'
)
->
distinct
(
true
)
->
count
(
'user_id'
);
$days
=
$this
->
getDaysByTimeRange
(
$timeRange
);
$endTime
=
time
();
$startTime
=
strtotime
(
"-
$days
days"
);
// 提交订单用户数
$submitOrderCount
=
Order
::
whereTime
(
'createtime'
,
$timeCondition
)
$visitCount
=
0
;
try
{
$visitCount
=
Db
::
name
(
'shopro_user_goods_log'
)
->
whereBetween
(
'createtime'
,
[
$startTime
,
$endTime
])
->
distinct
(
true
)
->
count
(
'user_id'
);
}
catch
(
\Exception
$e
)
{
$visitCount
=
0
;
}
$browseCount
=
0
;
try
{
$browseCount
=
Db
::
name
(
'shopro_user_goods_log'
)
->
whereBetween
(
'createtime'
,
[
$startTime
,
$endTime
])
->
where
(
'type'
,
'browse'
)
->
distinct
(
true
)
->
count
(
'user_id'
);
}
catch
(
\Exception
$e
)
{
$browseCount
=
0
;
}
$cartCount
=
0
;
try
{
$cartCount
=
Db
::
name
(
'shopro_user_goods_log'
)
->
whereBetween
(
'createtime'
,
[
$startTime
,
$endTime
])
->
where
(
'type'
,
'cart'
)
->
distinct
(
true
)
->
count
(
'user_id'
);
}
catch
(
\Exception
$e
)
{
$cartCount
=
0
;
}
$submitOrderCount
=
Order
::
whereBetween
(
'createtime'
,
[
$startTime
,
$endTime
])
->
distinct
(
true
)
->
count
(
'user_id'
);
// 完成支付用户数
$payCount
=
Order
::
whereTime
(
'createtime'
,
$timeCondition
)
$payCount
=
Order
::
whereBetween
(
'createtime'
,
[
$startTime
,
$endTime
])
->
whereIn
(
'status'
,
[
Order
::
STATUS_PAID
,
Order
::
STATUS_COMPLETED
])
->
distinct
(
true
)
->
count
(
'user_id'
);
...
...
@@ -188,15 +480,44 @@ class Dashboard extends Common
'pay'
=>
$payCount
]);
}
/**
* 获取客单价分布
*
* 统计不同价格区间的订单占比
* 优先从缓存读取,缓存未命中时降级到数据库查询
*
* 请求参数:
* - time_range: 时间范围,可选值:'7days'、'30days'、'90days',默认 '30days'
*
* 返回数据:
* - '0-100': 0-100元订单占比(百分比)
* - '100-300': 100-300元订单占比(百分比)
* - '300-500': 300-500元订单占比(百分比)
* - '500-1000': 500-1000元订单占比(百分比)
* - '1000+': 1000元以上订单占比(百分比)
*/
public
function
priceDistribution
()
{
$timeRange
=
$this
->
request
->
param
(
'time_range'
,
'30days'
);
$timeCondition
=
$this
->
getTimeCondition
(
$timeRange
);
$orders
=
Order
::
whereTime
(
'createtime'
,
$timeCondition
)
$cacheKey
=
'shopro:data_stats_center:'
.
$timeRange
;
$cachedData
=
cache
(
$cacheKey
);
if
(
$cachedData
!==
false
&&
isset
(
$cachedData
[
'price_distribution'
]))
{
$distribution
=
$cachedData
[
'price_distribution'
];
$result
=
[];
foreach
(
$distribution
as
$item
)
{
$result
[
$item
[
'label'
]]
=
$item
[
'rate'
];
}
$this
->
success
(
'获取成功'
,
null
,
$result
);
}
$days
=
$this
->
getDaysByTimeRange
(
$timeRange
);
$endTime
=
time
();
$startTime
=
strtotime
(
"-
$days
days"
);
$orders
=
Order
::
whereBetween
(
'createtime'
,
[
$startTime
,
$endTime
])
->
whereIn
(
'status'
,
[
Order
::
STATUS_PAID
,
Order
::
STATUS_COMPLETED
])
->
field
(
'pay_fee'
)
->
select
();
...
...
@@ -226,18 +547,42 @@ class Dashboard extends Common
}
}
// 转换为百分比
foreach
(
$distribution
as
&
$val
)
{
$val
=
$total
>
0
?
round
(
$val
/
$total
*
100
)
:
0
;
}
$this
->
success
(
'获取成功'
,
null
,
$distribution
);
}
/**
* 获取实时订单动态
*
* 获取最近10条已支付或已完成的订单信息
* 优先从缓存读取,缓存未命中时降级到数据库查询
*
* 返回数据数组,每个元素包含:
* - amount: 订单金额
* - time: 订单时间(格式化为相对时间,如'刚刚'、'5分钟前'等)
* - location: 用户位置(城市名称)
*/
public
function
recentOrders
()
{
$cacheKey
=
'shopro:data_stats_center:30days'
;
$cachedData
=
cache
(
$cacheKey
);
if
(
$cachedData
!==
false
&&
isset
(
$cachedData
[
'recent_orders'
]))
{
$orders
=
$cachedData
[
'recent_orders'
];
$result
=
[];
foreach
(
$orders
as
$order
)
{
$result
[]
=
[
'amount'
=>
$order
[
'amount'
],
'time'
=>
$this
->
formatTime
(
strtotime
(
$order
[
'time'
])),
'location'
=>
$order
[
'user'
]
?:
'未知'
];
}
$this
->
success
(
'获取成功'
,
null
,
$result
);
}
$orders
=
Order
::
whereIn
(
'status'
,
[
Order
::
STATUS_PAID
,
Order
::
STATUS_COMPLETED
])
->
order
(
'createtime'
,
'desc'
)
->
limit
(
10
)
...
...
@@ -255,13 +600,17 @@ class Dashboard extends Common
$this
->
success
(
'获取成功'
,
null
,
$result
);
}
/**
* 格式化时间显示
* 格式化时间为相对时间
*
* @param int $timestamp 时间戳
* @return string 格式化后的时间字符串
*/
private
function
formatTime
(
$timestamp
)
{
$now
=
time
();
$diff
=
$now
-
$timestamp
;
$diff
=
$now
-
(
int
)(
$timestamp
)
;
if
(
$diff
<
60
)
{
return
'刚刚'
;
...
...
@@ -274,207 +623,226 @@ class Dashboard extends Common
}
}
public
function
total
()
/**
* 获取用户、分销商、分享统计数据
*
* 返回数据:
* - user_data: 用户数据
* - total: 总用户数
* - today: 今日新增用户数
* - week: 本周新增用户数
* - array: 今日用户创建时间数组(用于图表)
* - agent_data: 分销商数据
* - total: 总分销商数
* - today: 今日新增分销商数
* - week: 本周新增分销商数
* - array: 今日分销商创建时间数组(用于图表)
* - share_data: 分享数据
* - total: 总分享次数
* - today: 今日分享次数
* - week: 本周分享次数
* - array: 今日分享创建时间数组(用于图表)
*/
public
function
old_total
()
{
// 用户数据
$userData
=
[
'total'
=>
User
::
count
(),
'today'
=>
User
::
whereTime
(
'createtime'
,
'today'
)
->
count
(),
'week'
=>
User
::
whereTime
(
'createtime'
,
'week'
)
->
count
(),
'array'
=>
collection
(
User
::
field
(
'id,createtime'
)
->
whereTime
(
'createtime'
,
'today'
)
->
select
())
->
each
(
function
(
$user
)
{
$user
->
counter
=
1
;
$user
->
createtime_unix
=
$user
->
getData
(
'createtime'
)
*
1000
;
})
];
// -- commission code start --
$agentData
=
[
'total'
=>
\app\admin\model\shopro\commission\Agent
::
count
(),
'today'
=>
\app\admin\model\shopro\commission\Agent
::
whereTime
(
'createtime'
,
'today'
)
->
count
(),
'week'
=>
\app\admin\model\shopro\commission\Agent
::
whereTime
(
'createtime'
,
'week'
)
->
count
(),
'array'
=>
collection
(
\app\admin\model\shopro\commission\Agent
::
field
(
'user_id,createtime'
)
->
whereTime
(
'createtime'
,
'today'
)
->
select
())
->
each
(
function
(
$agent
)
{
$agent
->
counter
=
1
;
$agent
->
createtime_unix
=
$agent
->
getData
(
'createtime'
)
*
1000
;
})
];
// -- commission code end --
// 分享数据
$shareData
=
[
'total'
=>
Share
::
count
(),
'today'
=>
Share
::
whereTime
(
'createtime'
,
'today'
)
->
count
(),
'week'
=>
Share
::
whereTime
(
'createtime'
,
'week'
)
->
count
(),
'array'
=>
collection
(
Share
::
field
(
'id,createtime'
)
->
whereTime
(
'createtime'
,
'today'
)
->
select
())
->
each
(
function
(
$share
)
{
$share
->
counter
=
1
;
$share
->
createtime_unix
=
$share
->
getData
(
'createtime'
)
*
1000
;
})
];
$this
->
success
(
'获取成功'
,
null
,
[
'user_data'
=>
$userData
,
'agent_data'
=>
$agentData
??
null
,
'share_data'
=>
$shareData
]);
// $userData = [
// 'total' => User::count(),
// 'today' => User::whereTime('createtime', 'today')->count(),
// 'week' => User::whereTime('createtime', 'week')->count(),
// 'array' => collection(User::field('id,createtime')->whereTime('createtime', 'today')->select())->each(function ($user) {
// $user->counter = 1;
// $user->createtime_unix = $user->getData('createtime') * 1000;
// })
// ];
// $agentData = [
// 'total' => \app\admin\model\shopro\commission\Agent::count(),
// 'today' => \app\admin\model\shopro\commission\Agent::whereTime('createtime', 'today')->count(),
// 'week' => \app\admin\model\shopro\commission\Agent::whereTime('createtime', 'week')->count(),
// 'array' => collection(\app\admin\model\shopro\commission\Agent::field('user_id,createtime')->whereTime('createtime', 'today')->select())->each(function ($agent) {
// $agent->counter = 1;
// $agent->createtime_unix = $agent->getData('createtime') * 1000;
// })
// ];
// $shareData = [
// 'total' => Share::count(),
// 'today' => Share::whereTime('createtime', 'today')->count(),
// 'week' => Share::whereTime('createtime', 'week')->count(),
// 'array' => collection(Share::field('id,createtime')->whereTime('createtime', 'today')->select())->each(function ($share) {
// $share->counter = 1;
// $share->createtime_unix = $share->getData('createtime') * 1000;
// })
// ];
// $this->success('获取成功', null, [
// 'user_data' => $userData,
// 'agent_data' => $agentData ?? null,
// 'share_data' => $shareData
// ]);
}
public
function
chart
()
/**
* 获取订单图表数据
*
* 根据指定时间范围统计订单数据,用于渲染订单趋势图表
*
* 请求参数:
* - date: 时间范围,格式为 '开始时间 - 结束时间'
*
* 返回数据:
* - orderNum: 订单总数
* - orderArr: 订单创建时间数组(用于图表)
* - payOrderNum: 支付订单数
* - payOrderArr: 支付订单创建时间数组(用于图表)
* - payAmountNum: 支付金额总数
* - payAmountArr: 支付金额创建时间数组(用于图表)
* - payUserNum: 支付用户数(去重)
* - payUserArr: 支付用户创建时间数组(用于图表)
* - noSendNum: 待发货订单数
* - noSendArr: 待发货订单创建时间数组(用于图表)
* - aftersaleNum: 售后维权订单数
* - aftersaleArr: 售后维权订单创建时间数组(用于图表)
* - refundNum: 退款订单数
* - refundArr: 退款订单创建时间数组(用于图表)
*/
public
function
old_chart
()
{
$date
=
$this
->
request
->
param
(
'date'
,
''
);
$date
=
array_values
(
array_filter
(
explode
(
' - '
,
$date
)));
$orders
=
Order
::
with
([
'items'
])
->
whereTime
(
'createtime'
,
'between'
,
$date
)
->
order
(
'id'
,
'asc'
)
->
select
();
// 订单数
$data
[
'orderNum'
]
=
count
(
$orders
);
$data
[
'orderArr'
]
=
[];
// 支付订单(包含退款的订单, 不包含货到付款还未收货(未支付)订单)
$data
[
'payOrderNum'
]
=
0
;
$data
[
'payOrderArr'
]
=
[];
//支付金额(包含退款的订单, 不包含货到付款还未收货(未支付)订单)
$data
[
'payAmountNum'
]
=
0
;
$data
[
'payAmountArr'
]
=
[];
// 支付用户(一个人不管下多少单,都算一个,包含退款的订单, 不包含货到付款还未收货(未支付)订单)
$userIds
=
[];
$data
[
'payUserNum'
]
=
0
;
$data
[
'payUserArr'
]
=
[];
// 代发货(包含货到付款)
$data
[
'noSendNum'
]
=
0
;
$data
[
'noSendArr'
]
=
[];
//售后维权
$data
[
'aftersaleNum'
]
=
0
;
$data
[
'aftersaleArr'
]
=
[];
//退款订单
$data
[
'refundNum'
]
=
0
;
$data
[
'refundArr'
]
=
[];
foreach
(
$orders
as
$key
=>
$order
)
{
$data
[
'orderArr'
][]
=
[
'counter'
=>
1
,
'createtime'
=>
$order
->
getData
(
'createtime'
)
*
1000
,
'user_id'
=>
$order
->
user_id
];
// 已支付的,不包含,货到付款未支付的
if
(
in_array
(
$order
->
status
,
[
Order
::
STATUS_PAID
,
Order
::
STATUS_COMPLETED
]))
{
// 支付订单数
$data
[
'payOrderNum'
]
++
;
$data
[
'payOrderArr'
][]
=
[
'counter'
=>
1
,
'createtime'
=>
$order
->
getData
(
'createtime'
)
*
1000
,
'user_id'
=>
$order
->
user_id
];
// 支付金额
$data
[
'payAmountNum'
]
=
bcadd
((
string
)
$data
[
'payAmountNum'
],
$order
->
pay_fee
,
2
);
$data
[
'payAmountArr'
][]
=
[
'counter'
=>
$order
->
pay_fee
,
'createtime'
=>
$order
->
getData
(
'createtime'
)
*
1000
,
];
// 下单用户
if
(
!
in_array
(
$order
->
user_id
,
$userIds
))
{
$data
[
'payUserNum'
]
++
;
$data
[
'payUserArr'
][]
=
[
'counter'
=>
1
,
'createtime'
=>
$order
->
getData
(
'createtime'
)
*
1000
,
'user_id'
=>
$order
->
user_id
];
}
}
// 已支付的,和 货到付款未支付的
if
(
in_array
(
$order
->
status
,
[
Order
::
STATUS_PAID
,
Order
::
STATUS_COMPLETED
])
||
$order
->
isOffline
(
$order
))
{
$flagnoSend
=
false
;
$flagaftersale
=
false
;
$flagrefund
=
false
;
$aftersaleIng
=
false
;
foreach
(
$order
->
items
as
$k
=>
$item
)
{
if
(
!
$flagnoSend
&&
$item
->
dispatch_status
==
OrderItem
::
DISPATCH_STATUS_NOSEND
&&
$item
->
refund_status
==
OrderItem
::
REFUND_STATUS_NOREFUND
&&
in_array
(
$order
->
apply_refund_status
,
[
Order
::
APPLY_REFUND_STATUS_NOAPPLY
,
Order
::
APPLY_REFUND_STATUS_REFUSE
])
)
{
$flagnoSend
=
true
;
}
if
(
$item
->
aftersale_status
==
OrderItem
::
AFTERSALE_STATUS_ING
&&
$item
->
dispatch_status
==
OrderItem
::
DISPATCH_STATUS_NOSEND
&&
$item
->
refund_status
==
OrderItem
::
REFUND_STATUS_NOREFUND
)
{
$aftersaleIng
=
true
;
}
if
(
!
$flagaftersale
&&
$item
->
aftersale_status
!=
OrderItem
::
AFTERSALE_STATUS_NOAFTER
)
{
$data
[
'aftersaleNum'
]
++
;
// $data['aftersaleArr'][] = [
// 'counter' => 1,
// 'createtime' => $order->getData('createtime') * 1000,
// ];
$flagaftersale
=
true
;
}
if
(
!
$flagrefund
&&
$item
->
refund_status
>
OrderItem
::
REFUND_STATUS_NOREFUND
)
{
$data
[
'refundNum'
]
++
;
// $data['refundArr'][] = [
// 'counter' => 1,
// 'createtime' => $order->getData('createtime') * 1000,
// ];
$flagrefund
=
true
;
}
}
if
(
!
$aftersaleIng
&&
$flagnoSend
)
{
// 存在正在售后中的订单,不算待发货(和订单列表保持一直)
$data
[
'noSendNum'
]
++
;
// $data['noSendArr'][] = [
// 'counter' => 1,
// 'createtime' => $order->getData('createtime') * 1000,
// ];
}
}
}
$this
->
success
(
'获取成功'
,
null
,
$data
);
// $date = $this->request->param('date', '');
// $date = array_values(array_filter(explode(' - ', $date)));
// $orders = Order::with(['items'])->whereTime('createtime', 'between', $date)
// ->order('id', 'asc')->select();
// $data['orderNum'] = count($orders);
// $data['orderArr'] = [];
// $data['payOrderNum'] = 0;
// $data['payOrderArr'] = [];
// $data['payAmountNum'] = 0;
// $data['payAmountArr'] = [];
// $userIds = [];
// $data['payUserNum'] = 0;
// $data['payUserArr'] = [];
// $data['noSendNum'] = 0;
// $data['noSendArr'] = [];
// $data['aftersaleNum'] = 0;
// $data['aftersaleArr'] = [];
// $data['refundNum'] = 0;
// $data['refundArr'] = [];
// foreach ($orders as $key => $order) {
// $data['orderArr'][] = [
// 'counter' => 1,
// 'createtime' => $order->getData('createtime') * 1000,
// 'user_id' => $order->user_id
// ];
// if (in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED])) {
// $data['payOrderNum']++;
// $data['payOrderArr'][] = [
// 'counter' => 1,
// 'createtime' => $order->getData('createtime') * 1000,
// 'user_id' => $order->user_id
// ];
// $data['payAmountNum'] = bcadd((string)$data['payAmountNum'], $order->pay_fee, 2);
// $data['payAmountArr'][] = [
// 'counter' => $order->pay_fee,
// 'createtime' => $order->getData('createtime') * 1000,
// ];
// if (!in_array($order->user_id, $userIds)) {
// $data['payUserNum']++;
// $data['payUserArr'][] = [
// 'counter' => 1,
// 'createtime' => $order->getData('createtime') * 1000,
// 'user_id' => $order->user_id
// ];
// }
// }
// if (in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED]) || $order->isOffline($order)) {
// $flagnoSend = false;
// $flagaftersale = false;
// $flagrefund = false;
// $aftersaleIng = false;
// foreach ($order->items as $k => $item) {
// if (
// !$flagnoSend
// && $item->dispatch_status == OrderItem::DISPATCH_STATUS_NOSEND
// && $item->refund_status == OrderItem::REFUND_STATUS_NOREFUND
// && in_array($order->apply_refund_status, [
// Order::APPLY_REFUND_STATUS_NOAPPLY,
// Order::APPLY_REFUND_STATUS_REFUSE
// ])
// ) {
// $flagnoSend = true;
// }
// if (
// $item->aftersale_status == OrderItem::AFTERSALE_STATUS_ING
// && $item->dispatch_status == OrderItem::DISPATCH_STATUS_NOSEND
// && $item->refund_status == OrderItem::REFUND_STATUS_NOREFUND
// ) {
// $aftersaleIng = true;
// }
// if (!$flagaftersale && $item->aftersale_status != OrderItem::AFTERSALE_STATUS_NOAFTER) {
// $data['aftersaleNum']++;
// $flagaftersale = true;
// }
// if (!$flagrefund && $item->refund_status > OrderItem::REFUND_STATUS_NOREFUND) {
// $data['refundNum']++;
// $flagrefund = true;
// }
// }
// if (!$aftersaleIng && $flagnoSend) {
// $data['noSendNum']++;
// }
// }
// }
// $this->success('获取成功', null, $data);
}
public
function
ranking
()
/**
* 获取商品排名和热门搜索
*
* 返回数据:
* - goods: 销量前5的商品列表,每个商品包含:
* - id: 商品ID
* - title: 商品标题
* - sales: 销量
* - real_sales: 真实销量(包含虚拟销量)
* - sale_total_money: 销售总额
* - hot_search: 热门搜索关键词列表,每个关键词包含:
* - keyword: 搜索词
* - num: 搜索次数
*/
public
function
old_ranking
()
{
$goods
=
Goods
::
limit
(
5
)
->
order
(
'sales'
,
'desc'
)
->
select
();
foreach
(
$goods
as
$key
=>
$gd
)
{
$gd
->
append
([
'real_sales'
]);
$result
=
OrderItem
::
field
(
'sum(goods_num * goods_price) as sale_total_money'
)
->
where
(
'goods_id'
,
$gd
[
'id'
])
->
whereExists
(
function
(
$query
)
use
(
$gd
)
{
$order_table_name
=
(
new
Order
())
->
getQuery
()
->
getTable
();
$table_name
=
(
new
OrderItem
())
->
getQuery
()
->
getTable
();
$query
->
table
(
$order_table_name
)
->
where
(
$table_name
.
'.order_id='
.
$order_table_name
.
'.id'
)
->
whereIn
(
'status'
,
[
Order
::
STATUS_PAID
,
Order
::
STATUS_COMPLETED
]);
// 已支付的订单
})
->
find
();
$gd
[
'sale_total_money'
]
=
$result
[
'sale_total_money'
]
?:
0
;
}
$searchHistory
=
new
SearchHistory
();
$this
->
success
(
'获取成功'
,
null
,
[
'goods'
=>
$goods
,
'hot_search'
=>
$searchHistory
->
hotSearch
()
]);
//
$goods = Goods::limit(5)->order('sales', 'desc')->select();
//
foreach ($goods as $key => $gd) {
//
$gd->append(['real_sales']);
//
$result = OrderItem::field('sum(goods_num * goods_price) as sale_total_money')->where('goods_id', $gd['id'])
//
->whereExists(function ($query) use ($gd) {
//
$order_table_name = (new Order())->getQuery()->getTable();
//
$table_name = (new OrderItem())->getQuery()->getTable();
//
$query->table($order_table_name)->where($table_name . '.order_id=' . $order_table_name . '.id')
// ->whereIn('status', [Order::STATUS_PAID, Order::STATUS_COMPLETED]);
//
})->find();
//
$gd['sale_total_money'] = $result['sale_total_money'] ?: 0;
//
}
//
$searchHistory = new SearchHistory();
//
$this->success('获取成功', null, [
//
'goods' => $goods,
//
'hot_search' => $searchHistory->hotSearch()
//
]);
}
}
}
\ No newline at end of file
application/admin/view/shopro/common/script.html
View file @
8da5c178
<link
rel=
"stylesheet"
href=
"//at.alicdn.com/t/c/font_2385137_b8qygb2jne.css?v={$site.version|htmlentities}"
>
<link
rel=
"stylesheet"
href=
"__CDN__/assets/addons/shopro/css/index.css?v={$site.version|htmlentities}"
>
<script
src=
"__CDN__/assets/libs/jquery/dist/jquery.min.js?v={$site.version|htmlentities}"
></script>
{if condition="$DARK_TYPE neq 'none'" }
<link
rel=
"stylesheet"
href=
"__CDN__/assets/addons/shopro/css/dark.css?v={$site.version|htmlentities}"
data-render=
"darktheme"
/>
{/if}
...
...
application/admin/view/shopro/dashboard/index.html
View file @
8da5c178
...
...
@@ -3,7 +3,6 @@
<script
src=
"__CDN__/assets/addons/shopro/libs/echarts.min.js?v={$site.version|htmlentities}"
></script>
<style>
/* 新增统计卡片样式 - 白色主题 */
.stats-center
{
background
:
#ffffff
;
border-radius
:
12px
;
...
...
@@ -32,6 +31,69 @@
.stats-center-time
{
font-size
:
12px
;
color
:
#9ca3af
;
margin-top
:
4px
;
}
.header-right
{
display
:
flex
;
align-items
:
center
;
gap
:
16px
;
}
.time-select-wrapper
{
position
:
relative
;
}
.time-select
{
background
:
#ffffff
;
border
:
1px
solid
#d1d5db
;
border-radius
:
8px
;
padding
:
8px
24px
8px
12px
;
color
:
#374151
;
font-size
:
13px
;
cursor
:
pointer
;
appearance
:
none
;
outline
:
none
;
min-width
:
100px
;
transition
:
border-color
0.2s
ease
;
}
.time-select
:hover
{
border-color
:
#3b82f6
;
}
.time-select
:focus
{
border-color
:
#3b82f6
;
box-shadow
:
0
0
0
3px
rgba
(
59
,
130
,
246
,
0.1
);
}
.time-select-wrapper
::after
{
content
:
'\f078'
;
font-family
:
'FontAwesome'
;
position
:
absolute
;
right
:
10px
;
top
:
50%
;
transform
:
translateY
(
-50%
);
color
:
#9ca3af
;
pointer-events
:
none
;
font-size
:
12px
;
}
.refresh-btn
{
display
:
flex
;
align-items
:
center
;
gap
:
6px
;
padding
:
8px
16px
;
background
:
linear-gradient
(
135deg
,
#3b82f6
,
#2563eb
);
border
:
none
;
border-radius
:
8px
;
color
:
#ffffff
;
cursor
:
pointer
;
font-size
:
13px
;
transition
:
all
0.2s
ease
;
}
.refresh-btn
:hover
{
transform
:
translateY
(
-1px
);
box-shadow
:
0
4px
12px
rgba
(
59
,
130
,
246
,
0.3
);
}
.refresh-btn
:active
{
transform
:
translateY
(
0
);
}
.refresh-btn
i
{
font-size
:
14px
;
}
.stats-card-row
{
display
:
flex
;
...
...
@@ -68,6 +130,7 @@
.stats-card.card-5
::before
{
background
:
linear-gradient
(
180deg
,
#3b82f6
0%
,
#2563eb
100%
);
}
.stats-card.card-6
::before
{
background
:
linear-gradient
(
180deg
,
#8b5cf6
0%
,
#7c3aed
100%
);
}
.stats-card.card-7
::before
{
background
:
linear-gradient
(
180deg
,
#ec4899
0%
,
#db2777
100%
);
}
.stats-card.card-8
::before
{
background
:
linear-gradient
(
180deg
,
#06b6d4
0%
,
#0891b2
100%
);
}
.stats-card-header
{
display
:
flex
;
justify-content
:
space-between
;
...
...
@@ -90,6 +153,7 @@
.card-5
.stats-card-icon
{
background
:
rgba
(
59
,
130
,
246
,
0.15
);
color
:
#3b82f6
;
}
.card-6
.stats-card-icon
{
background
:
rgba
(
139
,
92
,
246
,
0.15
);
color
:
#8b5cf6
;
}
.card-7
.stats-card-icon
{
background
:
rgba
(
236
,
72
,
153
,
0.15
);
color
:
#ec4899
;
}
.card-8
.stats-card-icon
{
background
:
rgba
(
6
,
182
,
212
,
0.15
);
color
:
#06b6d4
;
}
.stats-card-label
{
font-size
:
13px
;
color
:
#6b7280
;
...
...
@@ -117,42 +181,6 @@
margin-right
:
4px
;
font-size
:
10px
;
}
/* 时间筛选下拉框样式 */
.time-select-wrapper
{
position
:
relative
;
}
.time-select
{
background
:
#ffffff
;
border
:
1px
solid
#d1d5db
;
border-radius
:
8px
;
padding
:
8px
24px
8px
12px
;
color
:
#374151
;
font-size
:
13px
;
cursor
:
pointer
;
appearance
:
none
;
outline
:
none
;
min-width
:
100px
;
transition
:
border-color
0.2s
ease
;
}
.time-select
:hover
{
border-color
:
#3b82f6
;
}
.time-select
:focus
{
border-color
:
#3b82f6
;
box-shadow
:
0
0
0
3px
rgba
(
59
,
130
,
246
,
0.1
);
}
.time-select-wrapper
::after
{
content
:
'\f078'
;
font-family
:
'FontAwesome'
;
position
:
absolute
;
right
:
10px
;
top
:
50%
;
transform
:
translateY
(
-50%
);
color
:
#9ca3af
;
pointer-events
:
none
;
font-size
:
12px
;
}
/* 图表卡片样式 */
.chart-panel
{
background
:
#ffffff
;
border-radius
:
12px
;
...
...
@@ -179,24 +207,29 @@
.chart-tabs
{
display
:
flex
;
gap
:
8px
;
background
:
#f3f4f6
;
padding
:
4px
;
border-radius
:
8px
;
}
.chart-tab
{
padding
:
4px
12
px
;
padding
:
6px
14
px
;
border-radius
:
6px
;
font-size
:
12px
;
cursor
:
pointer
;
transition
:
all
0.2s
ease
;
color
:
#6b7280
;
background
:
#f3f4f6
;
background
:
transparent
;
border
:
none
;
}
.chart-tab.active
{
background
:
#3b82f6
;
color
:
#ffffff
;
background
:
#ffffff
;
color
:
#3b82f6
;
font-weight
:
600
;
box-shadow
:
0
1px
3px
rgba
(
0
,
0
,
0
,
0.1
);
}
.chart-container
{
height
:
220px
;
}
/* 转化漏斗样式 */
.funnel-item
{
display
:
flex
;
align-items
:
center
;
...
...
@@ -222,16 +255,16 @@
display
:
flex
;
align-items
:
center
;
padding-left
:
12px
;
color
:
#
ffffff
;
color
:
#
1f2937
;
font-size
:
12px
;
font-weight
:
500
;
font-weight
:
600
;
text-shadow
:
0
0
2px
rgba
(
255
,
255
,
255
,
0.8
);
}
.funnel-bar.color-1
{
background
:
linear-gradient
(
90deg
,
#3b82f6
,
#60a5fa
);
}
.funnel-bar.color-2
{
background
:
linear-gradient
(
90deg
,
#06b6d4
,
#22d3ee
);
}
.funnel-bar.color-3
{
background
:
linear-gradient
(
90deg
,
#10b981
,
#34d399
);
}
.funnel-bar.color-4
{
background
:
linear-gradient
(
90deg
,
#f59e0b
,
#fbbf24
);
}
.funnel-bar.color-5
{
background
:
linear-gradient
(
90deg
,
#ec4899
,
#f472b6
);
}
/* 客单价分布 */
.price-dist-item
{
display
:
flex
;
align-items
:
center
;
...
...
@@ -262,7 +295,6 @@
color
:
#9ca3af
;
margin-left
:
10px
;
}
/* 实时订单动态 */
.order-list
{
max-height
:
200px
;
overflow-y
:
auto
;
...
...
@@ -303,7 +335,6 @@
border-radius
:
4px
;
font-size
:
11px
;
}
/* 注册用户增长数字 */
.growth-badge
{
background
:
linear-gradient
(
135deg
,
#10b981
,
#059669
);
color
:
#ffffff
;
...
...
@@ -312,15 +343,30 @@
font-size
:
14px
;
font-weight
:
600
;
}
.data-status
{
display
:
flex
;
align-items
:
center
;
gap
:
6px
;
font-size
:
12px
;
color
:
#10b981
;
}
.status-dot
{
width
:
6px
;
height
:
6px
;
background
:
#10b981
;
border-radius
:
50%
;
animation
:
pulse
2s
infinite
;
}
@keyframes
pulse
{
0
%,
100
%
{
opacity
:
1
;
}
50
%
{
opacity
:
0.5
;
}
}
</style>
<div
id=
"index"
class=
"dashboard-index panel panel-default panel-intro"
v-cloak
>
<el-container
class=
"panel-block"
>
<el-main
class=
"dashboard-index-main"
>
<el-scrollbar
height=
"100%"
>
<!--统计图 Begin-->
<!-- 新增统计卡片区域 - 白色主题 -->
<div
class=
"stats-center"
>
<div
class=
"stats-center-header"
>
<div>
...
...
@@ -328,15 +374,24 @@
<i
class=
"fa fa-bar-chart"
></i>
数据统计中心
</div>
<div
class=
"stats-center-time"
>
实时业务数据监控 · 更新于
{:date('Y-m-d H:i:s')}
</div>
<div
class=
"stats-center-time"
>
实时业务数据监控 · 更新于
<span
id=
"update-time"
>
{:date('Y-m-d H:i:s')}
</span>
</div>
</div>
<!-- 时间筛选下拉框 -->
<div
class=
"time-select-wrapper"
>
<select
id=
"time_range"
class=
"time-select"
onchange=
"loadStatistics(this.value)"
>
<option
value=
"7days"
>
近7天
</option>
<option
value=
"30days"
selected
>
近30天
</option>
<option
value=
"90days"
>
近90天
</option>
</select>
<div
class=
"header-right"
>
<div
class=
"data-status"
>
<span
class=
"status-dot"
></span>
<span>
数据同步中
</span>
</div>
<div
class=
"time-select-wrapper"
>
<select
id=
"time_range"
class=
"time-select"
onchange=
"loadStatistics(this.value)"
>
<option
value=
"7days"
>
近7天
</option>
<option
value=
"30days"
selected
>
近30天
</option>
<option
value=
"90days"
>
近90天
</option>
</select>
</div>
<button
class=
"refresh-btn"
id=
"refreshBtn"
onclick=
"refreshAllData()"
>
<i
class=
"fa fa-refresh"
></i>
<span>
刷新数据
</span>
</button>
</div>
</div>
<div
class=
"stats-card-row"
>
...
...
@@ -390,28 +445,35 @@
</div>
<div
class=
"stats-card card-7"
>
<div
class=
"stats-card-header"
>
<span
class=
"stats-card-label"
>
注册
用户
</span>
<span
class=
"stats-card-label"
>
新增
用户
</span>
<span
class=
"stats-card-icon"
><i
class=
"fa fa-user-plus"
></i></span>
</div>
<div
class=
"stats-card-value"
id=
"stat-new-users"
>
0
</div>
<div
class=
"stats-card-trend up"
><i
class=
"fa fa-arrow-up"
></i>
<span
id=
"stat-new-trend"
>
+0(+0%)
</span></div>
</div>
<div
class=
"stats-card card-8"
>
<div
class=
"stats-card-header"
>
<span
class=
"stats-card-label"
>
注册用户
</span>
<span
class=
"stats-card-icon"
><i
class=
"fa fa-users"
></i></span>
</div>
<div
class=
"stats-card-value"
id=
"stat-register-users"
>
0
</div>
<div
class=
"stats-card-trend up"
><i
class=
"fa fa-arrow-up"
></i>
<span
id=
"stat-register-trend"
>
+0(+0%)
</span></div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div
class=
"row"
>
<div
class=
"col-lg-6"
>
<div
class=
"chart-panel"
>
<div
class=
"chart-panel-header"
>
<div>
<div
class=
"chart-panel-title"
>
成交总额趋势
</div>
<div
class=
"chart-panel-subtitle"
>
近30天每日GMV走势
</div>
<div
class=
"chart-panel-subtitle"
id=
"gmv-subtitle"
>
近30天每日GMV走势
</div>
</div>
<div
class=
"chart-tabs"
>
<
span
class=
"chart-tab active"
onclick=
"switchChartTab('gmv', 'day')"
>
日
</spa
n>
<
span
class=
"chart-tab"
onclick=
"switchChartTab('gmv', 'week')"
>
周
</spa
n>
<
span
class=
"chart-tab"
onclick=
"switchChartTab('gmv', 'month')"
>
月
</spa
n>
<
button
class=
"chart-tab active"
id=
"gmv-day"
onclick=
"switchPeriod('gmv', 'day')"
>
日
</butto
n>
<
button
class=
"chart-tab"
id=
"gmv-week"
onclick=
"switchPeriod('gmv', 'week')"
>
周
</butto
n>
<
button
class=
"chart-tab"
id=
"gmv-month"
onclick=
"switchPeriod('gmv', 'month')"
>
月
</butto
n>
</div>
</div>
<div
id=
"gmv-chart"
class=
"chart-container"
></div>
...
...
@@ -422,12 +484,12 @@
<div
class=
"chart-panel-header"
>
<div>
<div
class=
"chart-panel-title"
>
订单数趋势
</div>
<div
class=
"chart-panel-subtitle"
>
近30天订单量走势
</div>
<div
class=
"chart-panel-subtitle"
id=
"order-subtitle"
>
近30天订单量走势
</div>
</div>
<div
class=
"chart-tabs"
>
<
span
class=
"chart-tab active"
onclick=
"switchChartTab('order', 'day')"
>
日
</spa
n>
<
span
class=
"chart-tab"
onclick=
"switchChartTab('order', 'week')"
>
周
</spa
n>
<
span
class=
"chart-tab"
onclick=
"switchChartTab('order', 'month')"
>
月
</spa
n>
<
button
class=
"chart-tab active"
id=
"order-day"
onclick=
"switchPeriod('order', 'day')"
>
日
</butto
n>
<
button
class=
"chart-tab"
id=
"order-week"
onclick=
"switchPeriod('order', 'week')"
>
周
</butto
n>
<
button
class=
"chart-tab"
id=
"order-month"
onclick=
"switchPeriod('order', 'month')"
>
月
</butto
n>
</div>
</div>
<div
id=
"order-chart"
class=
"chart-container"
></div>
...
...
@@ -484,7 +546,10 @@
<div
class=
"chart-panel-title"
>
注册用户增长
</div>
<div
class=
"chart-panel-subtitle"
>
近30天新增用户趋势
</div>
</div>
<div>
<span
class=
"growth-badge"
id=
"growth-badge"
>
+0
</span>
<p
class=
"growth-badge-text"
>
累计注册用户
</p>
</div>
</div>
<div
id=
"user-growth-chart"
class=
"chart-container"
></div>
</div>
...
...
@@ -559,8 +624,6 @@
</div>
</div>
</div>
<!-- 图表初始化脚本 -->
<!--统计图 End-->
</el-scrollbar>
</el-main>
</el-container>
...
...
@@ -569,6 +632,9 @@
<script
type=
"text/javascript"
>
var
gmvChart
,
orderChart
,
userGrowthChart
;
var
currentTimeRange
=
'30days'
;
var
currentGmvPeriod
=
'day'
;
var
currentOrderPeriod
=
'day'
;
function
generateDateLabels
(
days
)
{
var
labels
=
[];
for
(
var
i
=
days
-
1
;
i
>=
0
;
i
--
)
{
...
...
@@ -578,6 +644,7 @@
}
return
labels
;
}
function
generateRandomData
(
length
,
min
,
max
)
{
var
data
=
[];
for
(
var
i
=
0
;
i
<
length
;
i
++
)
{
...
...
@@ -585,6 +652,7 @@
}
return
data
;
}
function
initCharts
()
{
gmvChart
=
echarts
.
init
(
document
.
getElementById
(
'gmv-chart'
));
var
gmvOption
=
{
...
...
@@ -621,6 +689,7 @@
}]
};
gmvChart
.
setOption
(
gmvOption
);
orderChart
=
echarts
.
init
(
document
.
getElementById
(
'order-chart'
));
var
orderOption
=
{
grid
:
{
top
:
20
,
right
:
20
,
bottom
:
30
,
left
:
50
},
...
...
@@ -650,11 +719,12 @@
}]
};
orderChart
.
setOption
(
orderOption
);
userGrowthChart
=
echarts
.
init
(
document
.
getElementById
(
'user-growth-chart'
));
var
userGrowthOption
=
{
grid
:
{
top
:
20
,
right
:
20
,
bottom
:
30
,
left
:
50
},
legend
:
{
data
:
[
'
新增用户'
,
'活跃
用户'
],
data
:
[
'
活跃用户'
,
'新增
用户'
],
top
:
0
,
textStyle
:
{
color
:
'#6b7280'
,
fontSize
:
12
}
},
...
...
@@ -671,16 +741,15 @@
splitLine
:
{
lineStyle
:
{
color
:
'#f3f4f6'
}
},
axisLabel
:
{
color
:
'#9ca3af'
,
fontSize
:
10
,
formatter
:
function
(
value
)
{
return
(
value
/
10000
).
toFixed
(
0
)
+
'万'
;
}
fontSize
:
10
}
},
series
:
[
{
name
:
'
新增
用户'
,
name
:
'
活跃
用户'
,
type
:
'line'
,
smooth
:
true
,
data
:
generateRandomData
(
30
,
2000
,
8000
)
,
data
:
[
120
,
156
,
132
,
189
,
210
,
198
,
234
,
256
,
212
,
188
,
245
,
267
,
234
,
289
,
312
,
278
,
245
,
267
,
298
,
321
,
287
,
265
,
290
,
310
,
334
,
301
,
276
,
289
,
312
,
345
]
,
lineStyle
:
{
color
:
'#f59e0b'
,
width
:
2
},
areaStyle
:
{
color
:
new
echarts
.
graphic
.
LinearGradient
(
0
,
0
,
0
,
1
,
[
...
...
@@ -691,10 +760,10 @@
symbol
:
'none'
},
{
name
:
'
活跃
用户'
,
name
:
'
新增
用户'
,
type
:
'line'
,
smooth
:
true
,
data
:
generateRandomData
(
30
,
8000
,
15000
)
,
data
:
[
45
,
67
,
52
,
78
,
89
,
76
,
98
,
102
,
87
,
72
,
95
,
110
,
98
,
120
,
135
,
112
,
95
,
105
,
125
,
140
,
128
,
115
,
130
,
145
,
156
,
142
,
128
,
135
,
152
,
168
]
,
lineStyle
:
{
color
:
'#06b6d4'
,
width
:
2
},
areaStyle
:
{
color
:
new
echarts
.
graphic
.
LinearGradient
(
0
,
0
,
0
,
1
,
[
...
...
@@ -707,20 +776,59 @@
]
};
userGrowthChart
.
setOption
(
userGrowthOption
);
window
.
addEventListener
(
'resize'
,
function
()
{
gmvChart
.
resize
();
orderChart
.
resize
();
userGrowthChart
.
resize
();
});
}
function
switchChartTab
(
type
,
period
)
{
console
.
log
(
'Switching '
+
type
+
' chart to '
+
period
);
function
switchPeriod
(
chartType
,
period
)
{
if
(
chartType
===
'gmv'
)
{
currentGmvPeriod
=
period
;
document
.
querySelectorAll
(
'#gmv-day, #gmv-week, #gmv-month'
).
forEach
(
el
=>
el
.
classList
.
remove
(
'active'
));
document
.
getElementById
(
'gmv-'
+
period
).
classList
.
add
(
'active'
);
var
subtitle
=
''
;
if
(
period
===
'day'
)
{
subtitle
=
'近'
+
currentTimeRange
.
replace
(
'days'
,
''
)
+
'天每日GMV走势'
;
}
else
if
(
period
===
'week'
)
{
subtitle
=
'近'
+
currentTimeRange
.
replace
(
'days'
,
''
)
+
'天每周GMV走势'
;
}
else
{
subtitle
=
'近'
+
currentTimeRange
.
replace
(
'days'
,
''
)
+
'天每月GMV走势'
;
}
document
.
getElementById
(
'gmv-subtitle'
).
textContent
=
subtitle
;
}
else
{
currentOrderPeriod
=
period
;
document
.
querySelectorAll
(
'#order-day, #order-week, #order-month'
).
forEach
(
el
=>
el
.
classList
.
remove
(
'active'
));
document
.
getElementById
(
'order-'
+
period
).
classList
.
add
(
'active'
);
var
subtitle
=
''
;
if
(
period
===
'day'
)
{
subtitle
=
'近'
+
currentTimeRange
.
replace
(
'days'
,
''
)
+
'天每日订单量走势'
;
}
else
if
(
period
===
'week'
)
{
subtitle
=
'近'
+
currentTimeRange
.
replace
(
'days'
,
''
)
+
'天每周订单量走势'
;
}
else
{
subtitle
=
'近'
+
currentTimeRange
.
replace
(
'days'
,
''
)
+
'天每月订单量走势'
;
}
document
.
getElementById
(
'order-subtitle'
).
textContent
=
subtitle
;
}
loadStatistics
(
currentTimeRange
);
}
function
loadStatistics
(
timeRange
)
{
currentTimeRange
=
timeRange
;
$
.
getJSON
(
'{:url("shopro/dashboard/statistics")}'
,
{
time_range
:
timeRange
},
function
(
res
)
{
$
.
getJSON
(
'{:url("shopro/dashboard/statistics")}'
,
{
time_range
:
timeRange
,
period
:
currentGmvPeriod
},
function
(
res
)
{
console
.
log
(
'=== statistics 接口返回数据 ==='
,
res
);
if
(
res
.
code
===
1
)
{
var
data
=
res
.
data
;
console
.
log
(
'data.trend_data:'
,
data
.
trend_data
);
console
.
log
(
'from_cache:'
,
data
.
from_cache
);
document
.
getElementById
(
'stat-total-amount'
).
textContent
=
'¥'
+
formatNumber
(
data
.
total_amount
,
2
);
document
.
getElementById
(
'stat-profit-rate'
).
textContent
=
data
.
profit_rate
;
document
.
getElementById
(
'stat-avg-amount'
).
textContent
=
'¥'
+
formatNumber
(
data
.
avg_amount
,
2
);
...
...
@@ -728,33 +836,89 @@
document
.
getElementById
(
'stat-visitor-count'
).
textContent
=
formatNumber
(
data
.
visitor_count
);
document
.
getElementById
(
'stat-conversion-rate'
).
textContent
=
data
.
conversion_rate
;
document
.
getElementById
(
'stat-register-users'
).
textContent
=
formatNumber
(
data
.
register_users
);
updateGmvChart
(
data
.
trend_data
);
if
(
currentGmvPeriod
===
currentOrderPeriod
)
{
updateOrderChart
(
data
.
trend_data
);
}
else
{
$
.
getJSON
(
'{:url("shopro/dashboard/statistics")}'
,
{
time_range
:
timeRange
,
period
:
currentOrderPeriod
},
function
(
res
)
{
if
(
res
.
code
===
1
)
{
updateOrderChart
(
res
.
data
.
trend_data
);
}
});
}
}
});
loadFunnel
(
timeRange
);
loadPriceDistribution
(
timeRange
);
loadRecentOrders
();
}
function
updateGmvChart
(
trendData
)
{
var
labels
=
trendData
.
map
(
function
(
item
)
{
return
item
.
date
;
});
var
amounts
=
trendData
.
map
(
function
(
item
)
{
return
item
.
amount
;
});
var
orders
=
trendData
.
map
(
function
(
item
)
{
return
item
.
order_num
;
});
var
registers
=
trendData
.
map
(
function
(
item
)
{
return
item
.
register_num
;
});
gmvChart
.
setOption
({
xAxis
:
{
data
:
labels
},
series
:
[{
data
:
amounts
}]
});
userGrowthChart
.
setOption
({
xAxis
:
{
data
:
labels
},
series
:
[
{
data
:
orders
},
{
data
:
registers
}
]
});
var
totalRegister
=
registers
.
reduce
(
function
(
a
,
b
)
{
return
a
+
b
;
},
0
);
document
.
getElementById
(
'growth-badge'
).
textContent
=
'+'
+
formatNumber
(
totalRegister
);
}
function
updateOrderChart
(
trendData
)
{
var
labels
=
trendData
.
map
(
function
(
item
)
{
return
item
.
date
;
});
var
orders
=
trendData
.
map
(
function
(
item
)
{
return
item
.
order_num
;
});
orderChart
.
setOption
({
xAxis
:
{
data
:
labels
},
series
:
[{
data
:
orders
}]
});
}
function
loadFunnel
(
timeRange
)
{
$
.
getJSON
(
'{:url("shopro/dashboard/funnel")}'
,
{
time_range
:
timeRange
},
function
(
res
)
{
if
(
res
.
code
===
1
)
{
var
data
=
res
.
data
;
var
max
=
data
.
visit
||
1
;
document
.
getElementById
(
'funnel-visit'
).
textContent
=
formatNumber
(
data
.
visit
);
var
browsePercent
=
max
>
0
?
Math
.
round
(
data
.
browse
/
max
*
100
)
:
0
;
document
.
getElementById
(
'funnel-browse'
).
textContent
=
formatNumber
(
data
.
browse
);
document
.
getElementById
(
'funnel-browse'
).
style
.
width
=
browsePercent
+
'%'
;
var
cartPercent
=
max
>
0
?
Math
.
round
(
data
.
cart
/
max
*
100
)
:
0
;
document
.
getElementById
(
'funnel-cart'
).
textContent
=
formatNumber
(
data
.
cart
);
document
.
getElementById
(
'funnel-cart'
).
style
.
width
=
cartPercent
+
'%'
;
var
submitPercent
=
max
>
0
?
Math
.
round
(
data
.
submit_order
/
max
*
100
)
:
0
;
document
.
getElementById
(
'funnel-submit'
).
textContent
=
formatNumber
(
data
.
submit_order
);
document
.
getElementById
(
'funnel-submit'
).
style
.
width
=
submitPercent
+
'%'
;
var
payPercent
=
max
>
0
?
Math
.
round
(
data
.
pay
/
max
*
100
)
:
0
;
document
.
getElementById
(
'funnel-pay'
).
textContent
=
formatNumber
(
data
.
pay
);
document
.
getElementById
(
'funnel-pay'
).
style
.
width
=
payPercent
+
'%'
;
}
});
}
function
loadPriceDistribution
(
timeRange
)
{
$
.
getJSON
(
'{:url("shopro/dashboard/priceDistribution")}'
,
{
time_range
:
timeRange
},
function
(
res
)
{
if
(
res
.
code
===
1
)
{
...
...
@@ -772,6 +936,7 @@
}
});
}
function
loadRecentOrders
()
{
$
.
getJSON
(
'{:url("shopro/dashboard/recentOrders")}'
,
function
(
res
)
{
if
(
res
.
code
===
1
)
{
...
...
@@ -791,12 +956,27 @@
}
});
}
function
formatNumber
(
num
,
decimals
)
{
if
(
num
===
null
||
num
===
undefined
)
num
=
0
;
if
(
decimals
!==
undefined
)
{
return
parseFloat
(
num
||
0
).
toFixed
(
decimals
);
return
parseFloat
(
num
).
toFixed
(
decimals
);
}
return
parseInt
(
num
||
0
).
toLocaleString
();
return
parseInt
(
num
).
toLocaleString
();
}
function
refreshAllData
()
{
document
.
getElementById
(
'refreshBtn'
).
innerHTML
=
'<i class="fa fa-spinner fa-spin"></i><span>刷新中...</span>'
;
document
.
getElementById
(
'refreshBtn'
).
disabled
=
true
;
setTimeout
(
function
()
{
loadStatistics
(
currentTimeRange
);
document
.
getElementById
(
'update-time'
).
textContent
=
new
Date
().
toLocaleString
(
'zh-CN'
);
document
.
getElementById
(
'refreshBtn'
).
innerHTML
=
'<i class="fa fa-refresh"></i><span>刷新数据</span>'
;
document
.
getElementById
(
'refreshBtn'
).
disabled
=
false
;
},
500
);
}
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
initCharts
();
loadStatistics
(
'30days'
);
...
...
application/command.php
View file @
8da5c178
...
...
@@ -10,6 +10,8 @@
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
use
Dom\DtdNamedNodeMap
;
return
[
'app\admin\command\Crud'
,
'app\admin\command\Menu'
,
...
...
@@ -21,4 +23,6 @@ return [
'app\command\StatMemberTag'
,
'app\command\StatsDaily'
,
'app\command\TrackFailedRetry'
,
'app\command\DataStatsCenter'
,
];
application/command/DataStatsCenter.php
0 → 100644
View file @
8da5c178
<?php
namespace
app\command
;
use
think\console\Command
;
use
think\console\Input
;
use
think\console\Output
;
use
think\Db
;
/**
* 数据统计中心定时任务
*
* 该类负责定时统计商城核心业务数据,包括:
* 1. 核心交易指标(成交额、订单数、客单价、利润率等)
* 2. 访客统计(基于小程序行为埋点表)
* 3. 注册用户统计
* 4. 转化漏斗分析(访问→浏览→加购→下单→支付)
* 5. 趋势数据(按天统计的成交额、订单数、注册增长趋势)
* 6. 客单价分布
* 7. 实时订单动态
*
* 定时执行时间:每天凌晨1点40分
* 统计范围:近7天、近30天、近90天
*/
class
DataStatsCenter
extends
Command
{
/**
* 缓存过期时间(25小时)
* 设置为25小时是为了确保当天的数据缓存能覆盖到第二天任务执行前
*/
const
CACHE_TTL
=
90000
;
/**
* 统计快照表名称
*/
const
STATS_TABLE
=
'shopro_data_stats_center'
;
/**
* 默认统计时间范围(单位:天)
*/
const
TIME_RANGES
=
[
7
,
30
,
90
];
/**
* 控制台输出对象
* @var Output
*/
protected
$output
=
null
;
/**
* 配置命令信息
*/
protected
function
configure
()
{
$this
->
setName
(
'shopro:data-stats-center'
)
->
setDescription
(
'每天凌晨1点40执行的数据统计中心任务'
);
}
/**
* 自定义日志记录方法
*
* @param string $level 日志级别: info/warning/error
* @param string $message 日志内容
*/
protected
function
customLog
(
$level
,
$message
)
{
// 格式化日志消息,添加时间戳和级别标识
$timestamp
=
date
(
'Y-m-d H:i:s'
);
$formattedMessage
=
"[
{
$timestamp
}
] [
{
$level
}
]
{
$message
}
"
;
// 如果有控制台输出对象,同时输出到控制台
if
(
$this
->
output
)
{
// 根据级别使用不同颜色输出
switch
(
$level
)
{
case
'error'
:
$this
->
output
->
writeln
(
"<error>
{
$formattedMessage
}
</error>"
);
break
;
case
'warning'
:
$this
->
output
->
writeln
(
"<comment>
{
$formattedMessage
}
</comment>"
);
break
;
default
:
$this
->
output
->
writeln
(
"<info>
{
$formattedMessage
}
</info>"
);
break
;
}
}
// 记录到自定义日志文件
custom_log
(
$message
,
'stat'
,
$level
);
}
/**
* 命令执行入口方法
*
* @param Input $input 输入参数
* @param Output $output 输出对象
*/
protected
function
execute
(
Input
$input
,
Output
$output
)
{
$this
->
output
=
$output
;
$this
->
customLog
(
'info'
,
'========== 开始执行数据统计中心定时任务 =========='
);
try
{
// 确保统计快照表存在
// $this->ensureStatsTable();
// 获取当前时间作为快照时间
$snapshotTime
=
time
();
$snapshotDate
=
date
(
'Y-m-d'
,
$snapshotTime
);
$allStats
=
[];
// 遍历所有统计时间范围(7天、30天、90天)
foreach
(
self
::
TIME_RANGES
as
$days
)
{
$rangeKey
=
$days
.
'days'
;
$this
->
customLog
(
'info'
,
'---------- 开始统计近'
.
$days
.
'天数据 ----------'
);
// 构建统计数据
$stats
=
$this
->
buildStatsPayload
(
$days
,
$snapshotTime
);
// 持久化到数据库
$this
->
persistStats
(
$snapshotDate
,
$rangeKey
,
$stats
);
// 缓存统计结果
cache
(
$this
->
buildCacheKey
(
$rangeKey
),
$stats
,
self
::
CACHE_TTL
);
$allStats
[
$rangeKey
]
=
$stats
;
$this
->
customLog
(
'info'
,
'近'
.
$days
.
'天统计完成,缓存已更新'
);
}
// 缓存所有时间范围的统计数据
cache
(
$this
->
buildCacheKey
(
'all'
),
$allStats
,
self
::
CACHE_TTL
);
$this
->
customLog
(
'info'
,
'['
.
date
(
'Y-m-d H:i:s'
)
.
']数据统计中心定时任务执行完成'
);
}
catch
(
\Exception
$e
)
{
// 记录异常日志并输出到控制台
format_log_error_custom_name
(
$e
,
'[数据统计中心定时任务执行失败]'
,
''
,
'stat'
);
}
}
/**
* 自动创建统计快照表(如果不存在)
*
* 表结构包含:
* - 快照日期、时间范围
* - 统计开始/结束时间戳
* - 各统计模块的JSON数据字段
*/
protected
function
ensureStatsTable
()
{
$table
=
$this
->
getFullTableName
(
self
::
STATS_TABLE
);
$sql
=
"CREATE TABLE IF NOT EXISTS `
{
$table
}
` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`snapshot_date` date NOT NULL COMMENT '快照日期',
`time_range` varchar(20) NOT NULL DEFAULT '' COMMENT '时间范围',
`range_start` int unsigned NOT NULL DEFAULT 0 COMMENT '统计开始时间',
`range_end` int unsigned NOT NULL DEFAULT 0 COMMENT '统计结束时间',
`summary_json` mediumtext COMMENT '顶部卡片统计',
`trend_json` mediumtext COMMENT '趋势数据',
`funnel_json` text COMMENT '转化漏斗',
`distribution_json` text COMMENT '客单价分布',
`recent_orders_json` mediumtext COMMENT '实时订单动态',
`extra_json` mediumtext COMMENT '扩展信息',
`created_at` int unsigned NOT NULL DEFAULT 0 COMMENT '创建时间',
`updated_at` int unsigned NOT NULL DEFAULT 0 COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_snapshot_range` (`snapshot_date`, `time_range`),
KEY `idx_time_range` (`time_range`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据统计中心快照表'"
;
Db
::
execute
(
$sql
);
}
/**
* 构建整包统计数据
*
* @param int $days 统计天数
* @param int $snapshotTime 快照时间戳
* @return array 包含所有统计模块的完整数据
*/
protected
function
buildStatsPayload
(
$days
,
$snapshotTime
)
{
// 构建当前统计周期和上一统计周期的时间范围
$currentRange
=
$this
->
buildRange
(
$days
,
$snapshotTime
);
$previousRange
=
$this
->
buildRange
(
$days
,
$currentRange
[
'start'
]
-
1
);
// 获取当前周期和上一周期的核心交易指标
$this
->
customLog
(
'info'
,
'开始获取当前&上一期统计周期核心交易指标'
);
$currentRevenue
=
$this
->
getRevenueMetrics
(
$currentRange
[
'start'
],
$currentRange
[
'end'
]);
$previousRevenue
=
$this
->
getRevenueMetrics
(
$previousRange
[
'start'
],
$previousRange
[
'end'
]);
// 获取当前周期和上一周期的访客数
$this
->
customLog
(
'info'
,
'开始获取当前&上一期统计周期访客数'
);
$currentVisitors
=
$this
->
getVisitorCount
(
$currentRange
[
'start'
],
$currentRange
[
'end'
]);
$previousVisitors
=
$this
->
getVisitorCount
(
$previousRange
[
'start'
],
$previousRange
[
'end'
]);
// 获取累计注册用户数(截至当前周期结束和上一周期结束)
$this
->
customLog
(
'info'
,
'开始获取当前&上一期统计周期累计注册用户数'
);
$currentRegisterTotal
=
$this
->
getRegisterCount
(
0
,
$currentRange
[
'end'
]);
$previousRegisterTotal
=
$this
->
getRegisterCount
(
0
,
$previousRange
[
'end'
]);
// 计算转化率(订单数/访客数)
$this
->
customLog
(
'info'
,
'开始计算当前&上一期统计周期转化率'
);
$currentConversion
=
$currentVisitors
>
0
?
round
(
$currentRevenue
[
'order_count'
]
/
$currentVisitors
*
100
,
2
)
:
0
;
$previousConversion
=
$previousVisitors
>
0
?
round
(
$previousRevenue
[
'order_count'
]
/
$previousVisitors
*
100
,
2
)
:
0
;
// 构建趋势数据
$this
->
customLog
(
'info'
,
'开始构建当前统计周期趋势数据'
);
$trendData
=
[
'amount'
=>
$this
->
buildAmountTrend
(
$days
,
$snapshotTime
),
'orders'
=>
$this
->
buildOrderTrend
(
$days
,
$snapshotTime
),
'register'
=>
$this
->
buildRegisterTrend
(
$days
,
$snapshotTime
),
];
// 构建漏斗数据
$this
->
customLog
(
'info'
,
'开始构建当前统计周期转化漏斗数据'
);
$funnelData
=
$this
->
buildFunnelData
(
$currentRange
[
'start'
],
$currentRange
[
'end'
]);
// 构建价格分布
$this
->
customLog
(
'info'
,
'开始构建当前统计周期客单价分布'
);
$priceDistribution
=
$this
->
buildPriceDistribution
(
$currentRange
[
'start'
],
$currentRange
[
'end'
]);
// 获取最近订单
$this
->
customLog
(
'info'
,
'开始获取当前统计周期最近订单'
);
$recentOrders
=
$this
->
getRecentOrders
(
10
);
// 组装完整的统计数据结构
return
[
'snapshot_date'
=>
date
(
'Y-m-d'
,
$snapshotTime
),
'time_range'
=>
$days
.
'days'
,
'days'
=>
$days
,
'generated_at'
=>
$snapshotTime
,
'range'
=>
[
'start'
=>
$currentRange
[
'start'
],
'end'
=>
$currentRange
[
'end'
],
'start_text'
=>
date
(
'Y-m-d H:i:s'
,
$currentRange
[
'start'
]),
'end_text'
=>
date
(
'Y-m-d H:i:s'
,
$currentRange
[
'end'
]),
],
'summary'
=>
[
'total_amount'
=>
$this
->
buildMetricCard
(
'成交总额'
,
$currentRevenue
[
'total_amount'
],
$previousRevenue
[
'total_amount'
],
'money'
),
'profit_rate'
=>
$this
->
buildMetricCard
(
'净利率'
,
$currentRevenue
[
'profit_rate'
],
$previousRevenue
[
'profit_rate'
],
'percent'
),
'avg_amount'
=>
$this
->
buildMetricCard
(
'客单价'
,
$currentRevenue
[
'avg_amount'
],
$previousRevenue
[
'avg_amount'
],
'money'
),
'order_count'
=>
$this
->
buildMetricCard
(
'订单数'
,
$currentRevenue
[
'order_count'
],
$previousRevenue
[
'order_count'
],
'count'
),
'visitor_count'
=>
$this
->
buildMetricCard
(
'访客数'
,
$currentVisitors
,
$previousVisitors
,
'count'
),
'conversion_rate'
=>
$this
->
buildMetricCard
(
'转化率'
,
$currentConversion
,
$previousConversion
,
'percent'
),
'register_users'
=>
$this
->
buildMetricCard
(
'注册用户'
,
$currentRegisterTotal
,
$previousRegisterTotal
,
'count'
),
],
'trend'
=>
$trendData
,
'funnel'
=>
$funnelData
,
'price_distribution'
=>
$priceDistribution
,
'recent_orders'
=>
$recentOrders
,
'extra'
=>
[
'current_revenue'
=>
$currentRevenue
,
'previous_revenue'
=>
$previousRevenue
,
],
];
}
/**
* 保存统计快照到数据库
*
* @param string $snapshotDate 快照日期(Y-m-d格式)
* @param string $timeRange 时间范围标识(如 '7days')
* @param array $stats 统计数据数组
*/
protected
function
persistStats
(
$snapshotDate
,
$timeRange
,
array
$stats
)
{
$now
=
time
();
$data
=
[
'snapshot_date'
=>
$snapshotDate
,
'time_range'
=>
$timeRange
,
'range_start'
=>
$stats
[
'range'
][
'start'
],
'range_end'
=>
$stats
[
'range'
][
'end'
],
'summary_json'
=>
json_encode
(
$stats
[
'summary'
],
JSON_UNESCAPED_UNICODE
),
'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
),
'extra_json'
=>
json_encode
(
$stats
[
'extra'
],
JSON_UNESCAPED_UNICODE
),
'updated_at'
=>
$now
,
];
// 检查是否已存在同日期同时间范围的记录
$row
=
Db
::
name
(
self
::
STATS_TABLE
)
->
where
(
'snapshot_date'
,
$snapshotDate
)
->
where
(
'time_range'
,
$timeRange
)
->
find
();
if
(
$row
)
{
// 更新已存在的记录
Db
::
name
(
self
::
STATS_TABLE
)
->
where
(
'id'
,
$row
[
'id'
])
->
update
(
$data
);
}
else
{
// 插入新记录
$data
[
'created_at'
]
=
$now
;
Db
::
name
(
self
::
STATS_TABLE
)
->
insert
(
$data
);
}
}
/**
* 构建统计时间区间
*
* @param int $days 统计天数
* @param int $endTime 结束时间戳
* @return array 包含 start 和 end 的时间范围数组
*/
protected
function
buildRange
(
$days
,
$endTime
)
{
$endTime
=
(
int
)
$endTime
;
// 计算开始时间:从结束时间往前推 days-1 天的 00:00:00
$startTime
=
strtotime
(
date
(
'Y-m-d 00:00:00'
,
strtotime
(
'-'
.
(
$days
-
1
)
.
' days'
,
$endTime
)));
return
[
'start'
=>
$startTime
,
'end'
=>
$endTime
,
];
}
/**
* 汇总核心交易指标
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return array 包含成交额、订单数、客单价、成本、退款、利润、利润率
*/
protected
function
getRevenueMetrics
(
$startTime
,
$endTime
)
{
// 统计成交额(已支付和已完成订单的支付金额总和)
$totalAmount
=
round
((
float
)
Db
::
name
(
'shopro_order'
)
->
whereIn
(
'status'
,
[
'paid'
,
'completed'
])
->
where
(
'paid_time'
,
'between'
,
[
$startTime
,
$endTime
])
->
sum
(
'pay_fee'
),
2
);
// 统计订单数
$orderCount
=
(
int
)
Db
::
name
(
'shopro_order'
)
->
whereIn
(
'status'
,
[
'paid'
,
'completed'
])
->
where
(
'paid_time'
,
'between'
,
[
$startTime
,
$endTime
])
->
count
();
// 计算客单价(成交额/订单数)
$avgAmount
=
$orderCount
>
0
?
round
(
$totalAmount
/
$orderCount
,
2
)
:
0
;
// 统计商品成本(成本价 × 数量)
$table
=
$this
->
getFullTableName
(
'shopro_order_item'
);
$orderTable
=
$this
->
getFullTableName
(
'shopro_order'
);
$sql
=
"SELECT SUM(item.cost_price * item.goods_num) AS total FROM `
{
$table
}
` item JOIN `
{
$orderTable
}
` o ON o.id = item.order_id WHERE o.status IN ('paid', 'completed') AND o.paid_time BETWEEN ? AND ?"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
$costAmount
=
round
((
float
)(
isset
(
$result
[
0
][
'total'
])
?
$result
[
0
][
'total'
]
:
0
),
2
);
// 统计退款金额
$refundAmount
=
round
((
float
)
Db
::
name
(
'shopro_refund'
)
->
where
(
'status'
,
'completed'
)
->
where
(
'createtime'
,
'between'
,
[
$startTime
,
$endTime
])
->
sum
(
'refund_fee'
),
2
);
// 计算利润和利润率
$profitAmount
=
round
(
$totalAmount
-
$refundAmount
-
$costAmount
,
2
);
$profitRate
=
$totalAmount
>
0
?
round
(
$profitAmount
/
$totalAmount
*
100
,
2
)
:
0
;
return
[
'total_amount'
=>
$totalAmount
,
'order_count'
=>
$orderCount
,
'avg_amount'
=>
$avgAmount
,
'cost_amount'
=>
$costAmount
,
'refund_amount'
=>
$refundAmount
,
'profit_amount'
=>
$profitAmount
,
'profit_rate'
=>
$profitRate
,
];
}
/**
* 获取访客数
*
* 用户识别策略:
* 1. 优先使用 user_id(登录用户)
* 2. 其次使用 session_id(未登录但有会话的用户)
* 3. 最后使用 ip(完全匿名访客)
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 去重后的访客数量
*/
protected
function
getVisitorCount
(
$startTime
,
$endTime
)
{
try
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(DISTINCT CASE
WHEN user_id > 0 THEN CONCAT('u_', user_id)
WHEN session_id <> '' THEN CONCAT('s_', session_id)
ELSE CONCAT('i_', ip)
END) AS total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
$count
=
isset
(
$result
[
0
][
'total'
])
?
(
int
)
$result
[
0
][
'total'
]
:
0
;
return
$count
;
}
catch
(
\Exception
$e
)
{
$this
->
customLog
(
'warning'
,
'埋点访客统计失败:'
.
$e
->
getMessage
());
return
0
;
}
}
/**
* 获取注册用户数
*
* @param int $startTime 开始时间戳(0表示从最早开始)
* @param int $endTime 结束时间戳
* @return int 注册用户数量
*/
protected
function
getRegisterCount
(
$startTime
,
$endTime
)
{
$query
=
Db
::
name
(
'user'
);
if
(
$startTime
>
0
)
{
$query
->
where
(
'createtime'
,
'between'
,
[
$startTime
,
$endTime
]);
}
else
{
// startTime=0 表示统计截至 endTime 的累计注册用户数
$query
->
where
(
'createtime'
,
'<='
,
$endTime
);
}
return
(
int
)
$query
->
count
();
}
/**
* 构建转化漏斗数据
*
* 使用小程序行为埋点表获取各阶段数据,确保数据源一致性
* 漏斗阶段:访问 → 浏览商品 → 加入购物车 → 提交订单 → 完成支付
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return array 漏斗数据数组
*/
protected
function
buildFunnelData
(
$startTime
,
$endTime
)
{
try
{
// 获取各阶段的用户数
$this
->
customLog
(
'info'
,
'开始获取当前统计周期访问用户数'
);
$visit
=
$this
->
getTrackVisitorCount
(
$startTime
,
$endTime
);
$browse
=
$this
->
getTrackGoodViewUserCount
(
$startTime
,
$endTime
);
$cart
=
$this
->
getTrackCartAddUserCount
(
$startTime
,
$endTime
);
$submitOrder
=
$this
->
getTrackOrderConfirmUserCount
(
$startTime
,
$endTime
);
$pay
=
$this
->
getTrackPayConfirmUserCount
(
$startTime
,
$endTime
);
// 获取各阶段的事件数
$this
->
customLog
(
'info'
,
'开始获取当前统计周期访问事件数'
);
$visitCount
=
$this
->
getTrackVisitCount
(
$startTime
,
$endTime
);
$browseCount
=
$this
->
getTrackGoodViewCount
(
$startTime
,
$endTime
);
$cartCount
=
$this
->
getTrackCartAddCount
(
$startTime
,
$endTime
);
$submitCount
=
$this
->
getTrackOrderConfirmCount
(
$startTime
,
$endTime
);
$payCount
=
$this
->
getTrackPayConfirmCount
(
$startTime
,
$endTime
);
// 计算基准值(避免除以0)
$base
=
max
(
$visit
,
1
);
// 组装漏斗数据
return
[
[
'key'
=>
'visit'
,
'label'
=>
'访问'
,
'value'
=>
$visit
,
'rate'
=>
100
,
'event_count'
=>
$visitCount
],
[
'key'
=>
'browse'
,
'label'
=>
'浏览商品'
,
'value'
=>
$browse
,
'rate'
=>
round
(
$browse
/
$base
*
100
,
2
),
'event_count'
=>
$browseCount
],
[
'key'
=>
'cart'
,
'label'
=>
'加入购物车'
,
'value'
=>
$cart
,
'rate'
=>
round
(
$cart
/
$base
*
100
,
2
),
'event_count'
=>
$cartCount
],
[
'key'
=>
'submit_order'
,
'label'
=>
'提交订单'
,
'value'
=>
$submitOrder
,
'rate'
=>
round
(
$submitOrder
/
$base
*
100
,
2
),
'event_count'
=>
$submitCount
],
[
'key'
=>
'pay'
,
'label'
=>
'完成支付'
,
'value'
=>
$pay
,
'rate'
=>
round
(
$pay
/
$base
*
100
,
2
),
'event_count'
=>
$payCount
],
];
}
catch
(
\Exception
$e
)
{
$this
->
customLog
(
'warning'
,
'使用埋点表构建转化漏斗失败'
.
$e
->
getMessage
());
}
}
/**
* 获取埋点表中访问用户数
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 去重后的访问用户数
*/
protected
function
getTrackVisitorCount
(
$startTime
,
$endTime
)
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(DISTINCT CASE
WHEN user_id > 0 THEN CONCAT('u_', user_id)
WHEN session_id <> '' THEN CONCAT('s_', session_id)
ELSE CONCAT('i_', ip)
END) as total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?
AND (event_code = 'index_view' OR page_path LIKE '/pages/index/%')"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
$this
->
customLog
(
'info'
,
'当前统计周期访问用户数: '
.
json_encode
(
$result
[
0
][
'total'
]
??
0
));
return
(
int
)(
$result
[
0
][
'total'
]
??
0
);
}
/**
* 获取埋点表中访问事件数
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 访问事件总数
*/
protected
function
getTrackVisitCount
(
$startTime
,
$endTime
)
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(*) as total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?
AND (event_code = 'index_view' OR page_path LIKE '/pages/index/%')"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
return
(
int
)(
$result
[
0
][
'total'
]
??
0
);
}
/**
* 获取埋点表中浏览商品用户数
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 去重后的浏览商品用户数
*/
protected
function
getTrackGoodViewUserCount
(
$startTime
,
$endTime
)
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(DISTINCT CASE
WHEN user_id > 0 THEN CONCAT('u_', user_id)
WHEN session_id <> '' THEN CONCAT('s_', session_id)
ELSE CONCAT('i_', ip)
END) as total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?
AND (event_code = 'good_view' OR page_path LIKE '/pages/goods/%')"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
return
(
int
)(
$result
[
0
][
'total'
]
??
0
);
}
/**
* 获取埋点表中浏览商品事件数
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 浏览商品事件总数
*/
protected
function
getTrackGoodViewCount
(
$startTime
,
$endTime
)
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(*) as total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?
AND (event_code = 'good_view' OR page_path LIKE '/pages/goods/%')"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
return
(
int
)(
$result
[
0
][
'total'
]
??
0
);
}
/**
* 获取埋点表中加入购物车用户数
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 去重后的加入购物车用户数
*/
protected
function
getTrackCartAddUserCount
(
$startTime
,
$endTime
)
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(DISTINCT CASE
WHEN user_id > 0 THEN CONCAT('u_', user_id)
WHEN session_id <> '' THEN CONCAT('s_', session_id)
ELSE CONCAT('i_', ip)
END) as total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?
AND event_code = 'cart_add'"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
return
(
int
)(
$result
[
0
][
'total'
]
??
0
);
}
/**
* 获取埋点表中加入购物车事件数
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 加入购物车事件总数
*/
protected
function
getTrackCartAddCount
(
$startTime
,
$endTime
)
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(*) as total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?
AND event_code = 'cart_add'"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
return
(
int
)(
$result
[
0
][
'total'
]
??
0
);
}
/**
* 获取埋点表中提交订单用户数
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 去重后的提交订单用户数
*/
protected
function
getTrackOrderConfirmUserCount
(
$startTime
,
$endTime
)
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(DISTINCT CASE
WHEN user_id > 0 THEN CONCAT('u_', user_id)
WHEN session_id <> '' THEN CONCAT('s_', session_id)
ELSE CONCAT('i_', ip)
END) as total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?
AND event_code = 'order_confirm'"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
return
(
int
)(
$result
[
0
][
'total'
]
??
0
);
}
/**
* 获取埋点表中提交订单事件数
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 提交订单事件总数
*/
protected
function
getTrackOrderConfirmCount
(
$startTime
,
$endTime
)
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(*) as total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?
AND event_code = 'order_confirm'"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
return
(
int
)(
$result
[
0
][
'total'
]
??
0
);
}
/**
* 获取埋点表中完成支付用户数
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 去重后的完成支付用户数
*/
protected
function
getTrackPayConfirmUserCount
(
$startTime
,
$endTime
)
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(DISTINCT CASE
WHEN user_id > 0 THEN CONCAT('u_', user_id)
WHEN session_id <> '' THEN CONCAT('s_', session_id)
ELSE CONCAT('i_', ip)
END) as total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?
AND event_code = 'pay_confirm'"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
return
(
int
)(
$result
[
0
][
'total'
]
??
0
);
}
/**
* 获取埋点表中完成支付事件数
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return int 完成支付事件总数
*/
protected
function
getTrackPayConfirmCount
(
$startTime
,
$endTime
)
{
$table
=
$this
->
getFullTableName
(
'shopro_miniprogram_track'
);
$sql
=
"SELECT COUNT(*) as total
FROM `
{
$table
}
`
WHERE create_time BETWEEN ? AND ?
AND event_code = 'pay_confirm'"
;
$result
=
Db
::
query
(
$sql
,
[
$startTime
,
$endTime
]);
return
(
int
)(
$result
[
0
][
'total'
]
??
0
);
}
/**
* 构建成交额趋势数据
*
* @param int $days 统计天数
* @param int $snapshotTime 快照时间戳
* @return array 包含日期标签和成交额数值的趋势数据
*/
protected
function
buildAmountTrend
(
$days
,
$snapshotTime
)
{
return
$this
->
buildDailySeries
(
$days
,
$snapshotTime
,
function
(
$startTime
,
$endTime
)
{
return
round
((
float
)
Db
::
name
(
'shopro_order'
)
->
whereIn
(
'status'
,
[
'paid'
,
'completed'
])
->
where
(
'paid_time'
,
'between'
,
[
$startTime
,
$endTime
])
->
sum
(
'pay_fee'
),
2
);
},
'成交额'
);
}
/**
* 构建订单数趋势数据
*
* @param int $days 统计天数
* @param int $snapshotTime 快照时间戳
* @return array 包含日期标签和订单数数值的趋势数据
*/
protected
function
buildOrderTrend
(
$days
,
$snapshotTime
)
{
return
$this
->
buildDailySeries
(
$days
,
$snapshotTime
,
function
(
$startTime
,
$endTime
)
{
return
(
int
)
Db
::
name
(
'shopro_order'
)
->
whereIn
(
'status'
,
[
'paid'
,
'completed'
])
->
where
(
'paid_time'
,
'between'
,
[
$startTime
,
$endTime
])
->
count
();
},
'订单数'
);
}
/**
* 构建注册用户增长趋势数据
*
* @param int $days 统计天数
* @param int $snapshotTime 快照时间戳
* @return array 包含日期标签、每日新增用户数和累计用户数的趋势数据
*/
protected
function
buildRegisterTrend
(
$days
,
$snapshotTime
)
{
$range
=
$this
->
buildRange
(
$days
,
$snapshotTime
);
// 获取统计周期开始前的累计注册用户数
$baseTotal
=
(
int
)
Db
::
name
(
'user'
)
->
where
(
'createtime'
,
'<'
,
$range
[
'start'
])
->
count
();
$labels
=
[];
$newUsers
=
[];
$totalUsers
=
[];
$runningTotal
=
$baseTotal
;
// 按天遍历统计周期
for
(
$i
=
$days
-
1
;
$i
>=
0
;
$i
--
)
{
$dayStart
=
strtotime
(
date
(
'Y-m-d 00:00:00'
,
strtotime
(
'-'
.
$i
.
' days'
,
$snapshotTime
)));
$dayEnd
=
min
(
strtotime
(
date
(
'Y-m-d 23:59:59'
,
$dayStart
)),
$snapshotTime
);
// 统计当天新增用户数
$newCount
=
(
int
)
Db
::
name
(
'user'
)
->
where
(
'createtime'
,
'between'
,
[
$dayStart
,
$dayEnd
])
->
count
();
// 累计用户数
$runningTotal
+=
$newCount
;
$labels
[]
=
date
(
'm-d'
,
$dayStart
);
$newUsers
[]
=
$newCount
;
$totalUsers
[]
=
$runningTotal
;
}
return
[
'title'
=>
'注册用户增长'
,
'labels'
=>
$labels
,
'new_users'
=>
$newUsers
,
'total_users'
=>
$totalUsers
,
'current_total'
=>
end
(
$totalUsers
)
?:
0
,
];
}
/**
* 通用日趋势构建方法
*
* @param int $days 统计天数
* @param int $snapshotTime 快照时间戳
* @param callable $callback 数据查询回调函数
* @param string $title 趋势标题
* @return array 趋势数据数组
*/
protected
function
buildDailySeries
(
$days
,
$snapshotTime
,
callable
$callback
,
$title
)
{
$labels
=
[];
$values
=
[];
// 按天遍历统计周期
for
(
$i
=
$days
-
1
;
$i
>=
0
;
$i
--
)
{
$dayStart
=
strtotime
(
date
(
'Y-m-d 00:00:00'
,
strtotime
(
'-'
.
$i
.
' days'
,
$snapshotTime
)));
$dayEnd
=
min
(
strtotime
(
date
(
'Y-m-d 23:59:59'
,
$dayStart
)),
$snapshotTime
);
$labels
[]
=
date
(
'm-d'
,
$dayStart
);
$values
[]
=
$callback
(
$dayStart
,
$dayEnd
);
}
return
[
'title'
=>
$title
,
'labels'
=>
$labels
,
'values'
=>
$values
,
];
}
/**
* 构建客单价分布数据
*
* 客单价区间:0-100、100-300、300-500、500-1000、1000+
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @return array 客单价分布数据数组
*/
protected
function
buildPriceDistribution
(
$startTime
,
$endTime
)
{
// 查询已支付和已完成订单的支付金额
$orders
=
Db
::
name
(
'shopro_order'
)
->
whereIn
(
'status'
,
[
'paid'
,
'completed'
])
->
where
(
'paid_time'
,
'between'
,
[
$startTime
,
$endTime
])
->
field
(
'pay_fee'
)
->
select
();
// 初始化分布区间
$distribution
=
[
'0-100'
=>
0
,
'100-300'
=>
0
,
'300-500'
=>
0
,
'500-1000'
=>
0
,
'1000+'
=>
0
,
];
$total
=
count
(
$orders
);
// 统计每个区间的订单数
foreach
(
$orders
as
$order
)
{
$amount
=
(
float
)
$order
[
'pay_fee'
];
if
(
$amount
<
100
)
{
$distribution
[
'0-100'
]
++
;
}
elseif
(
$amount
<
300
)
{
$distribution
[
'100-300'
]
++
;
}
elseif
(
$amount
<
500
)
{
$distribution
[
'300-500'
]
++
;
}
elseif
(
$amount
<
1000
)
{
$distribution
[
'500-1000'
]
++
;
}
else
{
$distribution
[
'1000+'
]
++
;
}
}
// 组装结果(包含区间标签、数量、占比)
$result
=
[];
foreach
(
$distribution
as
$label
=>
$count
)
{
$result
[]
=
[
'label'
=>
$label
,
'count'
=>
$count
,
'rate'
=>
$total
>
0
?
round
(
$count
/
$total
*
100
,
2
)
:
0
,
];
}
return
$result
;
}
/**
* 获取实时订单动态
*
* @param int $limit 返回订单数量限制
* @return array 订单列表(包含订单号、用户信息、商品信息、金额、时间)
*/
protected
function
getRecentOrders
(
$limit
=
10
)
{
// 查询最近支付的订单
$orders
=
Db
::
name
(
'shopro_order'
)
->
whereIn
(
'status'
,
[
'paid'
,
'completed'
])
->
where
(
'paid_time'
,
'>'
,
0
)
->
order
(
'paid_time desc'
)
->
limit
(
$limit
)
->
field
(
'id,order_sn,user_id,pay_fee,paid_time'
)
->
select
();
$result
=
[];
foreach
(
$orders
as
$order
)
{
// 获取用户信息
$user
=
Db
::
name
(
'user'
)
->
where
(
'id'
,
$order
[
'user_id'
])
->
field
(
'nickname,mobile'
)
->
find
();
// 获取订单商品信息
$item
=
Db
::
name
(
'shopro_order_item'
)
->
where
(
'order_id'
,
$order
[
'id'
])
->
field
(
'goods_title'
)
->
find
();
$result
[]
=
[
'order_id'
=>
(
int
)
$order
[
'id'
],
'order_sn'
=>
$order
[
'order_sn'
],
'user'
=>
$user
&&
$user
[
'nickname'
]
?
$user
[
'nickname'
]
:
(
'用户#'
.
$order
[
'user_id'
]),
'mobile'
=>
$user
&&
$user
[
'mobile'
]
?
$user
[
'mobile'
]
:
''
,
'goods_title'
=>
$item
?
$item
[
'goods_title'
]
:
''
,
'amount'
=>
round
((
float
)
$order
[
'pay_fee'
],
2
),
'time'
=>
date
(
'Y-m-d H:i:s'
,
(
int
)
$order
[
'paid_time'
]),
];
}
return
$result
;
}
/**
* 生成卡片指标数据结构
*
* @param string $label 指标标签
* @param mixed $currentValue 当前周期值
* @param mixed $previousValue 上一周期值
* @param string $type 类型:count(计数)、money(金额)、percent(百分比)
* @return array 卡片指标数据
*/
protected
function
buildMetricCard
(
$label
,
$currentValue
,
$previousValue
,
$type
=
'count'
)
{
// 按类型规格化数值
$currentValue
=
$this
->
normalizeByType
(
$currentValue
,
$type
);
$previousValue
=
$this
->
normalizeByType
(
$previousValue
,
$type
);
$compareValue
=
$this
->
normalizeByType
(
$currentValue
-
$previousValue
,
$type
);
// 计算环比增长率(避免除以0)
if
((
float
)
$previousValue
==
0.0
)
{
$compareRate
=
(
float
)
$currentValue
>
0
?
100.00
:
0.00
;
}
else
{
$compareRate
=
round
(((
$currentValue
-
$previousValue
)
/
$previousValue
)
*
100
,
2
);
}
return
[
'label'
=>
$label
,
'value'
=>
$currentValue
,
'formatted'
=>
$this
->
formatMetric
(
$currentValue
,
$type
),
'previous_value'
=>
$previousValue
,
'compare_value'
=>
$compareValue
,
'compare_rate'
=>
$compareRate
,
'compare_formatted'
=>
$this
->
formatCompareRate
(
$compareRate
),
];
}
/**
* 按类型规格化数值
*
* @param mixed $value 原始值
* @param string $type 类型:count(取整)、其他(保留2位小数)
* @return float|int 规格化后的值
*/
protected
function
normalizeByType
(
$value
,
$type
)
{
if
(
$type
===
'count'
)
{
return
(
int
)
round
(
$value
);
}
return
round
((
float
)
$value
,
2
);
}
/**
* 指标格式化
*
* @param mixed $value 原始值
* @param string $type 类型:count、money、percent
* @return string 格式化后的字符串
*/
protected
function
formatMetric
(
$value
,
$type
)
{
if
(
$type
===
'percent'
)
{
return
round
((
float
)
$value
,
2
)
.
'%'
;
}
if
(
$type
===
'money'
)
{
return
$this
->
formatLargeNumber
((
float
)
$value
,
true
);
}
return
$this
->
formatLargeNumber
((
float
)
$value
,
false
);
}
/**
* 环比增长率格式化
*
* @param float $rate 增长率
* @return string 格式化后的增长率字符串(带正负号)
*/
protected
function
formatCompareRate
(
$rate
)
{
$prefix
=
$rate
>
0
?
'+'
:
''
;
return
$prefix
.
round
((
float
)
$rate
,
2
)
.
'%'
;
}
/**
* 大数格式化
*
* 将大数值转换为更易读的格式:亿、万
*
* @param float $value 原始数值
* @param bool $isMoney 是否为金额(决定小数位数)
* @return string 格式化后的字符串
*/
protected
function
formatLargeNumber
(
$value
,
$isMoney
=
false
)
{
$precision
=
$isMoney
?
2
:
0
;
if
(
abs
(
$value
)
>=
100000000
)
{
return
round
(
$value
/
100000000
,
2
)
.
'亿'
;
}
if
(
abs
(
$value
)
>=
10000
)
{
return
round
(
$value
/
10000
,
2
)
.
'万'
;
}
return
number_format
(
$value
,
$precision
,
'.'
,
''
);
}
/**
* 构建缓存键
*
* @param string $timeRange 时间范围标识
* @return string 缓存键名
*/
protected
function
buildCacheKey
(
$timeRange
)
{
return
'shopro:data_stats_center:'
.
$timeRange
;
}
/**
* 获取完整表名(带表前缀)
*
* @param string $table 表名
* @return string 完整表名
*/
protected
function
getFullTableName
(
$table
)
{
return
config
(
'database.prefix'
)
.
$table
;
}
}
?>
\ No newline at end of file
application/command/TrackFailedRetry.php
View file @
8da5c178
...
...
@@ -36,6 +36,7 @@ class TrackFailedRetry extends Command
}
protected
function
execute
(
Input
$input
,
Output
$output
)
{
$this
->
output
=
$output
;
$this
->
customLog
(
'info'
,
'========== ['
.
date
(
'Y-m-d H:i:s'
)
.
'] 开始执行重试失败埋埋点数据任务 =========='
);
...
...
@@ -78,8 +79,24 @@ class TrackFailedRetry extends Command
throw
new
\Exception
(
'数据格式错误'
);
}
$retryPayload
=
$this
->
filterRetryPayload
(
$data
);
if
(
empty
(
$retryPayload
[
'data'
]))
{
Db
::
name
(
'shopro_miniprogram_track_failed'
)
->
where
(
'id'
,
$item
[
'id'
])
->
update
([
'status'
=>
1
,
'error'
=>
'同一操作已入库,跳过重试'
,
'update_time'
=>
time
()
]);
$totalSuccess
++
;
$totalProcessed
++
;
continue
;
}
// 重新推入队列
Queue
::
push
(
'\addons\shopro\job\TrackProcessJob@fire'
,
$
data
[
'data'
]
,
'shopro-high'
);
Queue
::
push
(
'\addons\shopro\job\TrackProcessJob@fire'
,
$
retryPayload
,
'shopro-high'
);
// 更新状态为已处理
Db
::
name
(
'shopro_miniprogram_track_failed'
)
...
...
@@ -114,6 +131,94 @@ class TrackFailedRetry extends Command
$this
->
customLog
(
'info'
,
"重试完成!总计处理:
{
$totalProcessed
}
| 成功:
{
$totalSuccess
}
| 失败:
{
$totalFailed
}
"
);
}
/**
* 过滤已经入库的埋点,仅重试未入库数据
*/
protected
function
filterRetryPayload
(
$payload
)
{
$trackList
=
isset
(
$payload
[
'data'
])
&&
is_array
(
$payload
[
'data'
])
?
$payload
[
'data'
]
:
[];
$clientIp
=
$payload
[
'ip'
]
??
''
;
$jobTimestamp
=
isset
(
$payload
[
'timestamp'
])
?
(
int
)
$payload
[
'timestamp'
]
:
time
();
$pendingList
=
[];
$seen
=
[];
foreach
(
$trackList
as
$trackItem
)
{
$trackRow
=
$this
->
buildTrackRow
(
$trackItem
,
$clientIp
,
$jobTimestamp
);
$dedupKey
=
$this
->
buildTrackDedupKey
(
$trackRow
);
if
(
isset
(
$seen
[
$dedupKey
]))
{
continue
;
}
$seen
[
$dedupKey
]
=
true
;
if
(
$this
->
trackExists
(
$trackRow
))
{
continue
;
}
$pendingList
[]
=
$trackItem
;
}
$payload
[
'data'
]
=
$pendingList
;
return
$payload
;
}
/**
* 构建埋点行数据
*/
protected
function
buildTrackRow
(
$trackItem
,
$clientIp
,
$jobTimestamp
)
{
return
[
'user_id'
=>
$trackItem
[
'user_id'
]
??
0
,
'openid'
=>
$trackItem
[
'openid'
]
??
''
,
'session_id'
=>
$trackItem
[
'session_id'
]
??
''
,
'event_type'
=>
$trackItem
[
'event_type'
]
??
''
,
'event_code'
=>
$trackItem
[
'event_code'
]
??
''
,
'page_path'
=>
$trackItem
[
'page_path'
]
??
''
,
'page_title'
=>
$trackItem
[
'page_title'
]
??
''
,
'params'
=>
!
empty
(
$trackItem
[
'params'
])
?
json_encode
(
$trackItem
[
'params'
])
:
''
,
'ip'
=>
$clientIp
,
'create_time'
=>
isset
(
$trackItem
[
'create_time'
])
?
(
int
)(
$trackItem
[
'create_time'
]
/
1000
)
:
$jobTimestamp
];
}
/**
* 构建埋点判重键
*/
protected
function
buildTrackDedupKey
(
$trackRow
)
{
return
md5
(
implode
(
'|'
,
[
(
string
)
$trackRow
[
'user_id'
],
(
string
)
$trackRow
[
'openid'
],
(
string
)
$trackRow
[
'session_id'
],
(
string
)
$trackRow
[
'event_type'
],
(
string
)
$trackRow
[
'event_code'
],
(
string
)
$trackRow
[
'page_path'
],
(
string
)
$trackRow
[
'page_title'
],
(
string
)
$trackRow
[
'params'
],
(
string
)
$trackRow
[
'ip'
],
(
string
)
$trackRow
[
'create_time'
],
]));
}
/**
* 判断埋点是否已存在
*/
protected
function
trackExists
(
$trackRow
)
{
return
Db
::
name
(
'shopro_miniprogram_track'
)
->
where
(
'user_id'
,
$trackRow
[
'user_id'
])
->
where
(
'openid'
,
$trackRow
[
'openid'
])
->
where
(
'session_id'
,
$trackRow
[
'session_id'
])
->
where
(
'event_type'
,
$trackRow
[
'event_type'
])
->
where
(
'event_code'
,
$trackRow
[
'event_code'
])
->
where
(
'page_path'
,
$trackRow
[
'page_path'
])
->
where
(
'page_title'
,
$trackRow
[
'page_title'
])
->
where
(
'params'
,
$trackRow
[
'params'
])
->
where
(
'ip'
,
$trackRow
[
'ip'
])
->
where
(
'create_time'
,
$trackRow
[
'create_time'
])
->
find
()
?
true
:
false
;
}
}
?>
\ No newline at end of file
?>
public/assets/js/backend/shopro/dashboard.js
View file @
8da5c178
...
...
@@ -54,7 +54,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
},
function
(
ret
,
res
)
{
})
}
function
initChartTotal
(
key
)
{
var
myChart
=
echarts
.
init
(
document
.
getElementById
(
`
${
key
}
Total`
));
var
chartEl
=
document
.
getElementById
(
`
${
key
}
Total`
);
if
(
!
chartEl
)
return
;
var
myChart
=
echarts
.
init
(
chartEl
);
window
.
onresize
=
()
=>
{
myChart
.
resize
()
}
...
...
@@ -370,14 +372,15 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
}
drawX
(
interval
,
kld
);
console
.
log
(
chartOption
,
'chartOption'
)
var
myChart2
=
echarts
.
init
(
document
.
getElementById
(
`chartContent`
));
window
.
onresize
=
()
=>
{
myChart2
.
resize
()
var
chartContentEl
=
document
.
getElementById
(
`chartContent`
);
if
(
chartContentEl
)
{
var
myChart2
=
echarts
.
init
(
chartContentEl
);
window
.
onresize
=
()
=>
{
myChart2
.
resize
()
}
myChart2
.
setOption
(
chartOption
);
}
myChart2
.
setOption
(
chartOption
);
}
else
{
chartOption
.
xAxis
.
data
=
[];
chartOption
.
series
[
0
].
data
=
[];
...
...
@@ -495,13 +498,15 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'moment'], function (
});
})
var
myChart3
=
echarts
.
init
(
document
.
getElementById
(
`rankingContent`
));
window
.
onresize
=
()
=>
{
myChart3
.
resize
()
var
rankingContentEl
=
document
.
getElementById
(
`rankingContent`
);
if
(
rankingContentEl
)
{
var
myChart3
=
echarts
.
init
(
rankingContentEl
);
window
.
onresize
=
()
=>
{
myChart3
.
resize
()
}
myChart3
.
setOption
(
pieOption
);
}
myChart3
.
setOption
(
pieOption
);
return
false
},
function
(
ret
,
res
)
{
})
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment