diff --git a/app/Http/Admin/Controllers/StatController.php b/app/Http/Admin/Controllers/StatController.php
new file mode 100644
index 0000000000000000000000000000000000000000..cb80569bc8a020166f272377fee1c55d19422300
--- /dev/null
+++ b/app/Http/Admin/Controllers/StatController.php
@@ -0,0 +1,98 @@
+getYearOptions();
+ $months = $statService->getMonthOptions();
+ $items = $statService->hotSales();
+
+ $this->view->pick('stat/hot_sales');
+ $this->view->setVar('years', $years);
+ $this->view->setVar('months', $months);
+ $this->view->setVar('items', $items);
+ }
+
+ /**
+ * @Get("/sales", name="admin.stat.sales")
+ */
+ public function salesAction()
+ {
+ $statService = new StatService();
+
+ $years = $statService->getYearOptions();
+ $months = $statService->getMonthOptions();
+ $data = $statService->sales();
+
+ $this->view->pick('stat/sales');
+ $this->view->setVar('years', $years);
+ $this->view->setVar('months', $months);
+ $this->view->setVar('data', $data);
+ }
+
+ /**
+ * @Get("/refunds", name="admin.stat.refunds")
+ */
+ public function refundsAction()
+ {
+ $statService = new StatService();
+
+ $years = $statService->getYearOptions();
+ $months = $statService->getMonthOptions();
+ $data = $statService->refunds();
+
+ $this->view->pick('stat/refunds');
+ $this->view->setVar('years', $years);
+ $this->view->setVar('months', $months);
+ $this->view->setVar('data', $data);
+ }
+
+ /**
+ * @Get("/users/registered", name="admin.stat.reg_users")
+ */
+ public function registeredUsersAction()
+ {
+ $statService = new StatService();
+
+ $years = $statService->getYearOptions();
+ $months = $statService->getMonthOptions();
+ $data = $statService->registeredUsers();
+
+ $this->view->pick('stat/registered_users');
+ $this->view->setVar('years', $years);
+ $this->view->setVar('months', $months);
+ $this->view->setVar('data', $data);
+ }
+
+ /**
+ * @Get("/users/online", name="admin.stat.online_users")
+ */
+ public function onlineUsersAction()
+ {
+ $statService = new StatService();
+
+ $years = $statService->getYearOptions();
+ $months = $statService->getMonthOptions();
+ $data = $statService->onlineUsers();
+
+ $this->view->pick('stat/online_users');
+ $this->view->setVar('years', $years);
+ $this->view->setVar('months', $months);
+ $this->view->setVar('data', $data);
+ }
+
+}
diff --git a/app/Http/Admin/Services/AuthNode.php b/app/Http/Admin/Services/AuthNode.php
index 44482bb07d0d55c1f116c5ad40e01aaf4d1078a4..86b54761d5e71aa32c666fe097e7df27decae354 100644
--- a/app/Http/Admin/Services/AuthNode.php
+++ b/app/Http/Admin/Services/AuthNode.php
@@ -438,6 +438,43 @@ class AuthNode extends Service
],
],
],
+ [
+ 'id' => '2-7',
+ 'title' => '数据统计',
+ 'type' => 'menu',
+ 'children' => [
+ [
+ 'id' => '2-7-1',
+ 'title' => '热卖商品',
+ 'type' => 'menu',
+ 'route' => 'admin.stat.hot_sales',
+ ],
+ [
+ 'id' => '2-7-2',
+ 'title' => '成交订单',
+ 'type' => 'menu',
+ 'route' => 'admin.stat.sales',
+ ],
+ [
+ 'id' => '2-7-3',
+ 'title' => '售后退款',
+ 'type' => 'menu',
+ 'route' => 'admin.stat.refunds',
+ ],
+ [
+ 'id' => '2-7-4',
+ 'title' => '注册用户',
+ 'type' => 'menu',
+ 'route' => 'admin.stat.reg_users',
+ ],
+ [
+ 'id' => '2-7-5',
+ 'title' => '活跃用户',
+ 'type' => 'menu',
+ 'route' => 'admin.stat.online_users',
+ ],
+ ],
+ ],
],
];
}
diff --git a/app/Http/Admin/Services/Stat.php b/app/Http/Admin/Services/Stat.php
new file mode 100644
index 0000000000000000000000000000000000000000..bacb6858e15027237bb326c79571840d4cdfd89c
--- /dev/null
+++ b/app/Http/Admin/Services/Stat.php
@@ -0,0 +1,405 @@
+request->getQuery('type', 'int', OrderModel::ITEM_COURSE);
+ $year = $this->request->getQuery('year', 'int', date('Y'));
+ $month = $this->request->getQuery('month', 'int', date('m'));
+
+ $prev = $this->getPrevMonth($year, $month);
+
+ return [
+ [
+ 'title' => "{$year}-{$month}",
+ 'sales' => $this->handleHotSales($type, $year, $month),
+ ],
+ [
+ 'title' => "{$prev['year']}-{$prev['month']}",
+ 'sales' => $this->handleHotSales($type, $prev['year'], $prev['month']),
+ ],
+ ];
+ }
+
+ public function sales()
+ {
+ $year = $this->request->getQuery('year', 'int', date('Y'));
+ $month = $this->request->getQuery('month', 'int', date('m'));
+
+ $prev = $this->getPrevMonth($year, $month);
+ $currSales = $this->handleSales($year, $month);
+ $prevSales = $this->handleSales($prev['year'], $prev['month']);
+
+ $items = [];
+
+ foreach (range(1, 31) as $day) {
+ $date = sprintf('%02d', $day);
+ $prevMonth = "{$prev['year']}-{$prev['month']}";
+ $currMonth = "{$year}-{$month}";
+ $items[] = [
+ 'date' => $date,
+ $currMonth => $currSales[$date] ?? 0,
+ $prevMonth => $prevSales[$date] ?? 0,
+ ];
+ }
+
+ return $items;
+ }
+
+ public function refunds()
+ {
+ $year = $this->request->getQuery('year', 'int', date('Y'));
+ $month = $this->request->getQuery('month', 'int', date('m'));
+
+ $prev = $this->getPrevMonth($year, $month);
+ $currRefunds = $this->handleRefunds($year, $month);
+ $prevRefunds = $this->handleRefunds($prev['year'], $prev['month']);
+
+ $items = [];
+
+ foreach (range(1, 31) as $day) {
+ $date = sprintf('%02d', $day);
+ $prevMonth = "{$prev['year']}-{$prev['month']}";
+ $currMonth = "{$year}-{$month}";
+ $items[] = [
+ 'date' => $date,
+ $currMonth => $currRefunds[$date] ?? 0,
+ $prevMonth => $prevRefunds[$date] ?? 0,
+ ];
+ }
+
+ return $items;
+ }
+
+ public function registeredUsers()
+ {
+ $year = $this->request->getQuery('year', 'int', date('Y'));
+ $month = $this->request->getQuery('month', 'int', date('m'));
+
+ $prev = $this->getPrevMonth($year, $month);
+ $currUsers = $this->handleRegisteredUsers($year, $month);
+ $prevUsers = $this->handleRegisteredUsers($prev['year'], $prev['month']);
+
+ $items = [];
+
+ foreach (range(1, 31) as $day) {
+ $date = sprintf('%02d', $day);
+ $prevMonth = "{$prev['year']}-{$prev['month']}";
+ $currMonth = "{$year}-{$month}";
+ $items[] = [
+ 'date' => $date,
+ $currMonth => $currUsers[$date] ?? 0,
+ $prevMonth => $prevUsers[$date] ?? 0,
+ ];
+ }
+
+ return $items;
+ }
+
+ public function onlineUsers()
+ {
+ $year = $this->request->getQuery('year', 'int', date('Y'));
+ $month = $this->request->getQuery('month', 'int', date('m'));
+
+ $prev = $this->getPrevMonth($year, $month);
+ $currUsers = $this->handleOnlineUsers($year, $month);
+ $prevUsers = $this->handleOnlineUsers($prev['year'], $prev['month']);
+
+ $items = [];
+
+ foreach (range(1, 31) as $day) {
+ $date = sprintf('%02d', $day);
+ $prevMonth = "{$prev['year']}-{$prev['month']}";
+ $currMonth = "{$year}-{$month}";
+ $items[] = [
+ 'date' => $date,
+ $currMonth => $currUsers[$date] ?? 0,
+ $prevMonth => $prevUsers[$date] ?? 0,
+ ];
+ }
+
+ return $items;
+ }
+
+ public function getYearOptions()
+ {
+ $end = date('Y');
+
+ $start = $end - 3;
+
+ return range($start, $end);
+ }
+
+ public function getMonthOptions()
+ {
+ $options = [];
+
+ foreach (range(1, 12) as $value) {
+ $options[] = sprintf('%02d', $value);
+ }
+ return $options;
+ }
+
+ protected function isCurrMonth($year, $month)
+ {
+ return date('Y-m') == "{$year}-{$month}";
+ }
+
+ protected function getPrevMonth($year, $month)
+ {
+ $currentMonthTime = strtotime("{$year}-{$month}");
+
+ $prevMonthTime = strtotime('-1 month', $currentMonthTime);
+
+ return [
+ 'year' => date('Y', $prevMonthTime),
+ 'month' => date('m', $prevMonthTime),
+ ];
+ }
+
+ protected function getMonthDates($year, $month)
+ {
+ $startTime = strtotime("{$year}-{$month}-01");
+
+ $days = date('t', $startTime);
+
+ $result = [];
+
+ foreach (range(1, $days) as $day) {
+ $result[] = sprintf('%04d-%02d-%02d', $year, $month, $day);
+ }
+
+ return $result;
+ }
+
+ protected function handleHotSales($type, $year, $month)
+ {
+ $keyName = "stat_hot_sales:{$type}_{$year}_{$month}";
+
+ $cache = $this->getCache();
+
+ $items = $cache->get($keyName);
+
+ if (!$items) {
+
+ $statRepo = new StatRepo();
+
+ $orders = $statRepo->findMonthlyOrders($type, $year, $month);
+
+ $items = [];
+
+ if ($orders->count() > 0) {
+
+ foreach ($orders as $order) {
+ $key = $order->item_id;
+ if (!isset($items[$key])) {
+ $items[$key] = [
+ 'title' => $order->subject,
+ 'total_count' => 1,
+ 'total_amount' => $order->amount,
+ ];
+ } else {
+ $items[$key]['total_count'] += 1;
+ $items[$key]['total_amount'] += $order->amount;
+ }
+ }
+
+ $totalCount = array_column($items, 'total_count');
+
+ array_multisort($totalCount, SORT_DESC, $items);
+ }
+
+ $queryMonth = "{$year}-{$month}";
+
+ $currMonth = date('Y-m');
+
+ if ($queryMonth < $currMonth) {
+ $cache->save($keyName, $items, 7 * 86400);
+ } else {
+ $cache->save($keyName, $items, 2 * 3600);
+ }
+ }
+
+ return $items;
+ }
+
+ protected function handleSales($year, $month)
+ {
+ $keyName = "stat_sales:{$year}_{$month}";
+
+ $redis = $this->getRedis();
+
+ $list = $redis->hGetAll($keyName);
+
+ $statRepo = new StatRepo();
+
+ $currDate = date('Y-m-d');
+ $currDay = date('d');
+
+ if (!$list) {
+ $dates = $this->getMonthDates($year, $month);
+ foreach ($dates as $date) {
+ $key = substr($date, -2);
+ if ($date < $currDate) {
+ $list[$key] = $statRepo->sumDailySales($date);
+ } elseif ($date == $currDate) {
+ $list[$key] = -999;
+ } else {
+ $list[$key] = 0;
+ }
+ }
+ $redis->hMSet($keyName, $list);
+ $redis->expire($keyName, 7 * 86400);
+ }
+
+ foreach ($list as $key => $value) {
+ if ($value < 0) {
+ $list[$key] = $statRepo->sumDailySales("{$year}-{$month}-{$key}");
+ $redis->hSet($keyName, $key, $list[$key]);
+ }
+ }
+
+ if ($this->isCurrMonth($year, $month)) {
+ $list[$currDay] = $statRepo->sumDailySales($currDate);
+ }
+
+ return $list;
+ }
+
+ protected function handleRefunds($year, $month)
+ {
+ $keyName = "stat_refunds:{$year}_{$month}";
+
+ $redis = $this->getRedis();
+
+ $list = $redis->hGetAll($keyName);
+
+ $statRepo = new StatRepo();
+
+ $currDate = date('Y-m-d');
+ $currDay = date('d');
+
+ if (!$list) {
+ $dates = $this->getMonthDates($year, $month);
+ foreach ($dates as $date) {
+ $key = substr($date, -2);
+ if ($date < $currDate) {
+ $list[$key] = $statRepo->sumDailyRefunds($date);
+ } elseif ($date == $currDate) {
+ $list[$key] = -999;
+ } else {
+ $list[$key] = 0;
+ }
+ }
+ $redis->hMSet($keyName, $list);
+ $redis->expire($keyName, 7 * 86400);
+ }
+
+ foreach ($list as $key => $value) {
+ if ($value < 0) {
+ $list[$key] = $statRepo->sumDailyRefunds("{$year}-{$month}-{$key}");
+ $redis->hSet($keyName, $key, $list[$key]);
+ }
+ }
+
+ if ($this->isCurrMonth($year, $month)) {
+ $list[$currDay] = $statRepo->sumDailyRefunds($currDate);
+ }
+
+ return $list;
+ }
+
+ protected function handleRegisteredUsers($year, $month)
+ {
+ $keyName = "stat_reg_users:{$year}_{$month}";
+
+ $redis = $this->getRedis();
+
+ $list = $redis->hGetAll($keyName);
+
+ $statRepo = new StatRepo();
+
+ $currDate = date('Y-m-d');
+ $currDay = date('d');
+
+ if (!$list) {
+ $dates = $this->getMonthDates($year, $month);
+ foreach ($dates as $date) {
+ $key = substr($date, -2);
+ if ($date < $currDate) {
+ $list[$key] = $statRepo->countDailyRegisteredUser($date);
+ } elseif ($date == $currDate) {
+ $list[$key] = -999;
+ } else {
+ $list[$key] = 0;
+ }
+ }
+ $redis->hMSet($keyName, $list);
+ $redis->expire($keyName, 7 * 86400);
+ }
+
+ foreach ($list as $key => $value) {
+ if ($value < 0) {
+ $list[$key] = $statRepo->countDailyRegisteredUser("{$year}-{$month}-{$key}");
+ $redis->hSet($keyName, $key, $list[$key]);
+ }
+ }
+
+ if ($this->isCurrMonth($year, $month)) {
+ $list[$currDay] = $statRepo->countDailyRegisteredUser($currDate);
+ }
+
+ return $list;
+ }
+
+ protected function handleOnlineUsers($year, $month)
+ {
+ $keyName = "stat_online_users:{$year}_{$month}";
+
+ $redis = $this->getRedis();
+
+ $list = $redis->hGetAll($keyName);
+
+ $statRepo = new StatRepo();
+
+ $currDate = date('Y-m-d');
+ $currDay = date('d');
+
+ if (!$list) {
+ $dates = $this->getMonthDates($year, $month);
+ foreach ($dates as $date) {
+ $key = substr($date, -2);
+ if ($date < $currDate) {
+ $list[$key] = $statRepo->countDailyOnlineUser($date);
+ } elseif ($date == $currDate) {
+ $list[$key] = -999;
+ } else {
+ $list[$key] = 0;
+ }
+ }
+ $redis->hMSet($keyName, $list);
+ $redis->expire($keyName, 7 * 86400);
+ }
+
+ foreach ($list as $key => $value) {
+ if ($value < 0) {
+ $list[$key] = $statRepo->countDailyOnlineUser("{$year}-{$month}-{$key}");
+ $redis->hSet($keyName, $key, $list[$key]);
+ }
+ }
+
+ if ($this->isCurrMonth($year, $month)) {
+ $list[$currDay] = $statRepo->countDailyOnlineUser($currDate);
+ }
+
+ return $list;
+ }
+
+}
diff --git a/app/Http/Admin/Views/stat/hot_sales.volt b/app/Http/Admin/Views/stat/hot_sales.volt
new file mode 100644
index 0000000000000000000000000000000000000000..124769851246de04a7e891fcb95d9f2b73f5ae66
--- /dev/null
+++ b/app/Http/Admin/Views/stat/hot_sales.volt
@@ -0,0 +1,101 @@
+{% extends 'templates/main.volt' %}
+
+{% block content %}
+
+ {%- macro show_sales(sales) %}
+
+
+
+
+
+
+
+
+
+ 排序 |
+ 名称 |
+ 数量 |
+ 金额 |
+
+
+
+ {% for sale in sales %}
+
+ {{ loop.index }} |
+ {{ sale.title }} |
+ {{ sale.total_count }} |
+ {{ '¥%0.2f'|format(sale.total_amount) }} |
+
+ {% endfor %}
+
+
+ {%- endmacro %}
+
+ {% set types = {'1':'课程','2':'套餐','3':'赞赏','4':'会员'} %}
+ {% set year = request.get('year','int',date('Y')) %}
+ {% set month = request.get('month','int',date('m')) %}
+ {% set type = request.get('type','int',1) %}
+
+
+
+
+
+
+ {% for item in items %}
+
+
+
+
{{ show_sales(item.sales) }}
+
+
+ {% endfor %}
+
+
+{% endblock %}
+
+{% block inline_css %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/Http/Admin/Views/stat/online_users.volt b/app/Http/Admin/Views/stat/online_users.volt
new file mode 100644
index 0000000000000000000000000000000000000000..68e18b329e6ee5ec3aaf27990fdd87c4bfb330d3
--- /dev/null
+++ b/app/Http/Admin/Views/stat/online_users.volt
@@ -0,0 +1,74 @@
+{% extends 'templates/main.volt' %}
+
+{% block content %}
+
+ {% set year = request.get('year','int',date('Y')) %}
+ {% set month = request.get('month','int',date('m')) %}
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block include_js %}
+
+ {{ js_include('https://cdn.bootcdn.net/ajax/libs/echarts/4.8.0/echarts.min.js', false) }}
+
+{% endblock %}
+
+{% block inline_js %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/Http/Admin/Views/stat/refunds.volt b/app/Http/Admin/Views/stat/refunds.volt
new file mode 100644
index 0000000000000000000000000000000000000000..6c25c75f19196e4c86b23e86d59cba558c6cc079
--- /dev/null
+++ b/app/Http/Admin/Views/stat/refunds.volt
@@ -0,0 +1,74 @@
+{% extends 'templates/main.volt' %}
+
+{% block content %}
+
+ {% set year = request.get('year','int',date('Y')) %}
+ {% set month = request.get('month','int',date('m')) %}
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block include_js %}
+
+ {{ js_include('https://cdn.bootcdn.net/ajax/libs/echarts/4.8.0/echarts.min.js', false) }}
+
+{% endblock %}
+
+{% block inline_js %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/Http/Admin/Views/stat/registered_users.volt b/app/Http/Admin/Views/stat/registered_users.volt
new file mode 100644
index 0000000000000000000000000000000000000000..1fd7cec0ae528e1329acaf9a6d0ecccfcb9d17c2
--- /dev/null
+++ b/app/Http/Admin/Views/stat/registered_users.volt
@@ -0,0 +1,74 @@
+{% extends 'templates/main.volt' %}
+
+{% block content %}
+
+ {% set year = request.get('year','int',date('Y')) %}
+ {% set month = request.get('month','int',date('m')) %}
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block include_js %}
+
+ {{ js_include('https://cdn.bootcdn.net/ajax/libs/echarts/4.8.0/echarts.min.js', false) }}
+
+{% endblock %}
+
+{% block inline_js %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/Http/Admin/Views/stat/sales.volt b/app/Http/Admin/Views/stat/sales.volt
new file mode 100644
index 0000000000000000000000000000000000000000..d2eb18ba9b216d592565c547c1192ac9062cd0d9
--- /dev/null
+++ b/app/Http/Admin/Views/stat/sales.volt
@@ -0,0 +1,74 @@
+{% extends 'templates/main.volt' %}
+
+{% block content %}
+
+ {% set year = request.get('year','int',date('Y')) %}
+ {% set month = request.get('month','int',date('m')) %}
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block include_js %}
+
+ {{ js_include('https://cdn.bootcdn.net/ajax/libs/echarts/4.8.0/echarts.min.js', false) }}
+
+{% endblock %}
+
+{% block inline_js %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/Listeners/User.php b/app/Listeners/User.php
new file mode 100644
index 0000000000000000000000000000000000000000..4d60f7a5dd838d245b8fe92b475d7b789aef0550
--- /dev/null
+++ b/app/Listeners/User.php
@@ -0,0 +1,52 @@
+active_time > 600) {
+
+ $user->active_time = $now;
+
+ $user->update();
+
+ $onlineRepo = new OnlineRepo();
+
+ $online = $onlineRepo->findByUserDate($user->id, date('Y-m-d'));
+
+ if ($online) {
+
+ $online->active_time = $now;
+ $online->client_type = $this->getClientType();
+ $online->client_ip = $this->getClientIp();
+
+ $online->update();
+
+ } else {
+
+ $online = new OnlineModel();
+
+ $online->user_id = $user->id;
+ $online->active_time = $now;
+ $online->client_type = $this->getClientType();
+ $online->client_ip = $this->getClientIp();
+
+ $online->create();
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/Models/Online.php b/app/Models/Online.php
new file mode 100644
index 0000000000000000000000000000000000000000..8d181b09dc64df401b76cf5c307bf7fa2bc04375
--- /dev/null
+++ b/app/Models/Online.php
@@ -0,0 +1,79 @@
+create_time = time();
+ }
+
+ public function beforeUpdate()
+ {
+ $this->update_time = time();
+ }
+
+}
diff --git a/app/Repos/Audit.php b/app/Repos/Audit.php
index 0cd111691fb78bfc3fb569578d0b3bf2480066c8..98dda9ec62149967f5978a23a002061309bf829f 100644
--- a/app/Repos/Audit.php
+++ b/app/Repos/Audit.php
@@ -63,7 +63,7 @@ class Audit extends Repository
}
/**
- * @param string $id
+ * @param int $id
* @return AuditModel|Model|bool
*/
public function findById($id)
diff --git a/app/Repos/Online.php b/app/Repos/Online.php
new file mode 100644
index 0000000000000000000000000000000000000000..1d833cbf7522e4e49af4d88938a21695bdc1b507
--- /dev/null
+++ b/app/Repos/Online.php
@@ -0,0 +1,30 @@
+ 'user_id = ?1 AND active_time BETWEEN ?2 AND ?3',
+ 'bind' => [
+ 1 => $userId,
+ 2 => $activeTime,
+ 3 => $activeTime + 86400,
+ ],
+ ]);
+ }
+
+}
\ No newline at end of file
diff --git a/app/Repos/Stat.php b/app/Repos/Stat.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb5731ceb36075079dddd97060ede81812fb949c
--- /dev/null
+++ b/app/Repos/Stat.php
@@ -0,0 +1,139 @@
+ 'create_time BETWEEN :start_time: AND :end_time:',
+ 'bind' => ['start_time' => $startTime, 'end_time' => $endTime],
+ ]);
+ }
+
+ public function countDailyOnlineUser($date)
+ {
+ $startTime = strtotime($date);
+
+ $endTime = $startTime + 86400;
+
+ return (int)OnlineModel::count([
+ 'conditions' => 'active_time BETWEEN :start_time: AND :end_time:',
+ 'bind' => ['start_time' => $startTime, 'end_time' => $endTime],
+ ]);
+ }
+
+ public function countDailySales($date)
+ {
+ $sql = "SELECT count(*) AS total_count FROM %s AS os JOIN %s AS o ON os.order_id = o.id ";
+
+ $sql .= "WHERE os.status = ?1 AND o.create_time BETWEEN ?2 AND ?3";
+
+ $phql = sprintf($sql, OrderStatusModel::class, OrderModel::class);
+
+ $startTime = strtotime($date);
+
+ $endTime = $startTime + 86400;
+
+ $result = $this->modelsManager->executeQuery($phql, [
+ 1 => OrderModel::STATUS_FINISHED,
+ 2 => $startTime,
+ 3 => $endTime,
+ ]);
+
+ return (float)$result[0]['total_count'];
+ }
+
+ public function countDailyRefunds($date)
+ {
+ $startTime = strtotime($date);
+
+ $endTime = $startTime + 86400;
+
+ return (int)RefundModel::count([
+ 'conditions' => 'status = ?1 AND create_time BETWEEN ?2 AND ?3',
+ 'bind' => [
+ 1 => RefundModel::STATUS_FINISHED,
+ 2 => $startTime,
+ 3 => $endTime,
+ ],
+ ]);
+ }
+
+ public function sumDailySales($date)
+ {
+ $sql = "SELECT sum(o.amount) AS total_amount FROM %s AS os JOIN %s AS o ON os.order_id = o.id ";
+
+ $sql .= "WHERE os.status = ?1 AND o.create_time BETWEEN ?2 AND ?3";
+
+ $phql = sprintf($sql, OrderStatusModel::class, OrderModel::class);
+
+ $startTime = strtotime($date);
+
+ $endTime = $startTime + 86400;
+
+ $result = $this->modelsManager->executeQuery($phql, [
+ 1 => OrderModel::STATUS_FINISHED,
+ 2 => $startTime,
+ 3 => $endTime,
+ ]);
+
+ return (float)$result[0]['total_amount'];
+ }
+
+ public function sumDailyRefunds($date)
+ {
+ $startTime = strtotime($date);
+
+ $endTime = $startTime + 86400;
+
+ return (float)RefundModel::sum([
+ 'column' => 'amount',
+ 'conditions' => 'status = ?1 AND create_time BETWEEN ?2 AND ?3',
+ 'bind' => [
+ 1 => RefundModel::STATUS_FINISHED,
+ 2 => $startTime,
+ 3 => $endTime,
+ ],
+ ]);
+ }
+
+ /**
+ * @param int $type
+ * @param int $year
+ * @param int $month
+ * @return ResultsetInterface|Resultset|OrderModel[]
+ */
+ public function findMonthlyOrders($type, $year, $month)
+ {
+ $startTime = strtotime("{$year}-{$month}");
+
+ $endTime = strtotime('+1 month', $startTime);
+
+ $status = OrderModel::STATUS_FINISHED;
+
+ return $this->modelsManager->createBuilder()
+ ->addFrom(OrderStatusModel::class, 'os')
+ ->join(OrderModel::class, 'os.order_id = o.id', 'o')
+ ->columns('o.*')
+ ->where('o.item_type = :type:', ['type' => $type])
+ ->andWhere('os.status = :status:', ['status' => $status])
+ ->betweenWhere('o.create_time', $startTime, $endTime)
+ ->getQuery()->execute();
+ }
+
+}
diff --git a/app/Traits/Auth.php b/app/Traits/Auth.php
index 57d71e7d69dac749792b6fe1dbfe18f59534952a..1524fa9082b7c5d60d9553aa81b81f617612d7f3 100644
--- a/app/Traits/Auth.php
+++ b/app/Traits/Auth.php
@@ -6,7 +6,8 @@ use App\Models\User as UserModel;
use App\Repos\User as UserRepo;
use App\Services\Auth as AuthService;
use App\Validators\Validator as AppValidator;
-use Phalcon\Di;
+use Phalcon\Di as Di;
+use Phalcon\Events\Manager as EventsManager;
trait Auth
{
@@ -24,7 +25,16 @@ trait Auth
$userRepo = new UserRepo();
- return $userRepo->findById($authUser['id']);
+ $user = $userRepo->findById($authUser['id']);
+
+ /**
+ * @var EventsManager $eventsManager
+ */
+ $eventsManager = Di::getDefault()->getShared('eventsManager');
+
+ $eventsManager->fire('user:online', $this, $user);
+
+ return $user;
}
/**
@@ -40,13 +50,7 @@ trait Auth
$userRepo = new UserRepo();
- $user = $userRepo->findById($authUser['id']);
-
- if (time() - $user->active_time > 600) {
- $user->update(['active_time' => time()]);
- }
-
- return $user;
+ return $userRepo->findById($authUser['id']);
}
/**
diff --git a/config/events.php b/config/events.php
index c4628c05bda5d331349125383ad5253a425dbe6f..f42117792f4d591e82184d1aa72a1c89acc3c305 100644
--- a/config/events.php
+++ b/config/events.php
@@ -1,9 +1,11 @@
Pay::class,
+ 'user' => User::class,
'userDailyCounter' => UserDailyCounter::class,
];
\ No newline at end of file
diff --git a/db/migrations/20200827063842_init_table.php b/db/migrations/20200827063842_init_table.php
index f11f2b42a54f0448b0af646d1d5481fb19b1949c..dc9c703cd1a4ff858d968d05c2a1dafad7b7a48b 100644
--- a/db/migrations/20200827063842_init_table.php
+++ b/db/migrations/20200827063842_init_table.php
@@ -2517,7 +2517,7 @@ class InitTable extends Phinx\Migration\AbstractMigration
->addColumn('client_ip', 'string', [
'null' => false,
'default' => '',
- 'limit' => 30,
+ 'limit' => 64,
'collation' => 'utf8mb4_general_ci',
'encoding' => 'utf8mb4',
'comment' => '终端IP',
diff --git a/db/migrations/20201004095647_create_online_table.php b/db/migrations/20201004095647_create_online_table.php
new file mode 100644
index 0000000000000000000000000000000000000000..9a5e64facba828c0f030c5af1bef0fb05bb47c37
--- /dev/null
+++ b/db/migrations/20201004095647_create_online_table.php
@@ -0,0 +1,107 @@
+table('kg_online', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_general_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => MysqlAdapter::INT_REGULAR,
+ 'identity' => 'enable',
+ 'comment' => '主键编号',
+ ])
+ ->addColumn('user_id', 'integer', [
+ 'null' => false,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_REGULAR,
+ 'comment' => '用户编号',
+ 'after' => 'id',
+ ])
+ ->addColumn('client_type', 'integer', [
+ 'null' => false,
+ 'default' => '1',
+ 'limit' => MysqlAdapter::INT_REGULAR,
+ 'comment' => '终端类型',
+ 'after' => 'user_id',
+ ])
+ ->addColumn('client_ip', 'string', [
+ 'null' => false,
+ 'default' => '',
+ 'limit' => 64,
+ 'collation' => 'utf8mb4_general_ci',
+ 'encoding' => 'utf8mb4',
+ 'comment' => '终端IP',
+ 'after' => 'client_type',
+ ])
+ ->addColumn('active_time', 'integer', [
+ 'null' => false,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_REGULAR,
+ 'comment' => '活跃时间',
+ 'after' => 'client_ip',
+ ])
+ ->addColumn('create_time', 'integer', [
+ 'null' => false,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_REGULAR,
+ 'comment' => '创建时间',
+ 'after' => 'active_time',
+ ])
+ ->addColumn('update_time', 'integer', [
+ 'null' => false,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_REGULAR,
+ 'comment' => '更新时间',
+ 'after' => 'create_time',
+ ])
+ ->addIndex(['active_time'], [
+ 'name' => 'active_time',
+ 'unique' => false,
+ ])
+ ->addIndex(['user_id'], [
+ 'name' => 'user_id',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('kg_task', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_general_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->save();
+ $this->table('kg_trade', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_general_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->changeColumn('channel_sn', 'string', [
+ 'null' => false,
+ 'default' => '',
+ 'limit' => 64,
+ 'collation' => 'utf8mb4_general_ci',
+ 'encoding' => 'utf8mb4',
+ 'comment' => '平台序号',
+ 'after' => 'channel',
+ ])
+ ->save();
+ }
+}
diff --git a/db/migrations/schema.php b/db/migrations/schema.php
index b43fdd2886639c4c3c62a5e8ab752bb7dae0dc36..6e7477b4237c4935ca5065373caf750b60c14b95 100644
--- a/db/migrations/schema.php
+++ b/db/migrations/schema.php
@@ -8822,14 +8822,14 @@ return array(
'COLUMN_DEFAULT' => '',
'IS_NULLABLE' => 'NO',
'DATA_TYPE' => 'varchar',
- 'CHARACTER_MAXIMUM_LENGTH' => '30',
- 'CHARACTER_OCTET_LENGTH' => '120',
+ 'CHARACTER_MAXIMUM_LENGTH' => '64',
+ 'CHARACTER_OCTET_LENGTH' => '256',
'NUMERIC_PRECISION' => NULL,
'NUMERIC_SCALE' => NULL,
'DATETIME_PRECISION' => NULL,
'CHARACTER_SET_NAME' => 'utf8mb4',
'COLLATION_NAME' => 'utf8mb4_general_ci',
- 'COLUMN_TYPE' => 'varchar(30)',
+ 'COLUMN_TYPE' => 'varchar(64)',
'COLUMN_KEY' => '',
'EXTRA' => '',
'PRIVILEGES' => 'select,insert,update,references',
@@ -9514,6 +9514,247 @@ return array(
),
'foreign_keys' => NULL,
),
+ 'kg_online' =>
+ array(
+ 'table' =>
+ array(
+ 'table_name' => 'kg_online',
+ 'engine' => 'InnoDB',
+ 'table_comment' => '',
+ 'table_collation' => 'utf8mb4_general_ci',
+ 'character_set_name' => 'utf8mb4',
+ 'row_format' => 'Dynamic',
+ ),
+ 'columns' =>
+ array(
+ 'id' =>
+ array(
+ 'TABLE_CATALOG' => 'def',
+ 'TABLE_NAME' => 'kg_online',
+ 'COLUMN_NAME' => 'id',
+ 'ORDINAL_POSITION' => '1',
+ 'COLUMN_DEFAULT' => NULL,
+ 'IS_NULLABLE' => 'NO',
+ 'DATA_TYPE' => 'int',
+ 'CHARACTER_MAXIMUM_LENGTH' => NULL,
+ 'CHARACTER_OCTET_LENGTH' => NULL,
+ 'NUMERIC_PRECISION' => '10',
+ 'NUMERIC_SCALE' => '0',
+ 'DATETIME_PRECISION' => NULL,
+ 'CHARACTER_SET_NAME' => NULL,
+ 'COLLATION_NAME' => NULL,
+ 'COLUMN_TYPE' => 'int',
+ 'COLUMN_KEY' => 'PRI',
+ 'EXTRA' => 'auto_increment',
+ 'PRIVILEGES' => 'select,insert,update,references',
+ 'COLUMN_COMMENT' => '主键编号',
+ 'GENERATION_EXPRESSION' => '',
+ 'SRS_ID' => NULL,
+ ),
+ 'user_id' =>
+ array(
+ 'TABLE_CATALOG' => 'def',
+ 'TABLE_NAME' => 'kg_online',
+ 'COLUMN_NAME' => 'user_id',
+ 'ORDINAL_POSITION' => '2',
+ 'COLUMN_DEFAULT' => '0',
+ 'IS_NULLABLE' => 'NO',
+ 'DATA_TYPE' => 'int',
+ 'CHARACTER_MAXIMUM_LENGTH' => NULL,
+ 'CHARACTER_OCTET_LENGTH' => NULL,
+ 'NUMERIC_PRECISION' => '10',
+ 'NUMERIC_SCALE' => '0',
+ 'DATETIME_PRECISION' => NULL,
+ 'CHARACTER_SET_NAME' => NULL,
+ 'COLLATION_NAME' => NULL,
+ 'COLUMN_TYPE' => 'int unsigned',
+ 'COLUMN_KEY' => 'MUL',
+ 'EXTRA' => '',
+ 'PRIVILEGES' => 'select,insert,update,references',
+ 'COLUMN_COMMENT' => '用户编号',
+ 'GENERATION_EXPRESSION' => '',
+ 'SRS_ID' => NULL,
+ ),
+ 'client_type' =>
+ array(
+ 'TABLE_CATALOG' => 'def',
+ 'TABLE_NAME' => 'kg_online',
+ 'COLUMN_NAME' => 'client_type',
+ 'ORDINAL_POSITION' => '3',
+ 'COLUMN_DEFAULT' => '1',
+ 'IS_NULLABLE' => 'NO',
+ 'DATA_TYPE' => 'int',
+ 'CHARACTER_MAXIMUM_LENGTH' => NULL,
+ 'CHARACTER_OCTET_LENGTH' => NULL,
+ 'NUMERIC_PRECISION' => '10',
+ 'NUMERIC_SCALE' => '0',
+ 'DATETIME_PRECISION' => NULL,
+ 'CHARACTER_SET_NAME' => NULL,
+ 'COLLATION_NAME' => NULL,
+ 'COLUMN_TYPE' => 'int unsigned',
+ 'COLUMN_KEY' => '',
+ 'EXTRA' => '',
+ 'PRIVILEGES' => 'select,insert,update,references',
+ 'COLUMN_COMMENT' => '终端类型',
+ 'GENERATION_EXPRESSION' => '',
+ 'SRS_ID' => NULL,
+ ),
+ 'client_ip' =>
+ array(
+ 'TABLE_CATALOG' => 'def',
+ 'TABLE_NAME' => 'kg_online',
+ 'COLUMN_NAME' => 'client_ip',
+ 'ORDINAL_POSITION' => '4',
+ 'COLUMN_DEFAULT' => '',
+ 'IS_NULLABLE' => 'NO',
+ 'DATA_TYPE' => 'varchar',
+ 'CHARACTER_MAXIMUM_LENGTH' => '64',
+ 'CHARACTER_OCTET_LENGTH' => '256',
+ 'NUMERIC_PRECISION' => NULL,
+ 'NUMERIC_SCALE' => NULL,
+ 'DATETIME_PRECISION' => NULL,
+ 'CHARACTER_SET_NAME' => 'utf8mb4',
+ 'COLLATION_NAME' => 'utf8mb4_general_ci',
+ 'COLUMN_TYPE' => 'varchar(64)',
+ 'COLUMN_KEY' => '',
+ 'EXTRA' => '',
+ 'PRIVILEGES' => 'select,insert,update,references',
+ 'COLUMN_COMMENT' => '终端IP',
+ 'GENERATION_EXPRESSION' => '',
+ 'SRS_ID' => NULL,
+ ),
+ 'active_time' =>
+ array(
+ 'TABLE_CATALOG' => 'def',
+ 'TABLE_NAME' => 'kg_online',
+ 'COLUMN_NAME' => 'active_time',
+ 'ORDINAL_POSITION' => '5',
+ 'COLUMN_DEFAULT' => '0',
+ 'IS_NULLABLE' => 'NO',
+ 'DATA_TYPE' => 'int',
+ 'CHARACTER_MAXIMUM_LENGTH' => NULL,
+ 'CHARACTER_OCTET_LENGTH' => NULL,
+ 'NUMERIC_PRECISION' => '10',
+ 'NUMERIC_SCALE' => '0',
+ 'DATETIME_PRECISION' => NULL,
+ 'CHARACTER_SET_NAME' => NULL,
+ 'COLLATION_NAME' => NULL,
+ 'COLUMN_TYPE' => 'int unsigned',
+ 'COLUMN_KEY' => 'MUL',
+ 'EXTRA' => '',
+ 'PRIVILEGES' => 'select,insert,update,references',
+ 'COLUMN_COMMENT' => '活跃时间',
+ 'GENERATION_EXPRESSION' => '',
+ 'SRS_ID' => NULL,
+ ),
+ 'create_time' =>
+ array(
+ 'TABLE_CATALOG' => 'def',
+ 'TABLE_NAME' => 'kg_online',
+ 'COLUMN_NAME' => 'create_time',
+ 'ORDINAL_POSITION' => '6',
+ 'COLUMN_DEFAULT' => '0',
+ 'IS_NULLABLE' => 'NO',
+ 'DATA_TYPE' => 'int',
+ 'CHARACTER_MAXIMUM_LENGTH' => NULL,
+ 'CHARACTER_OCTET_LENGTH' => NULL,
+ 'NUMERIC_PRECISION' => '10',
+ 'NUMERIC_SCALE' => '0',
+ 'DATETIME_PRECISION' => NULL,
+ 'CHARACTER_SET_NAME' => NULL,
+ 'COLLATION_NAME' => NULL,
+ 'COLUMN_TYPE' => 'int unsigned',
+ 'COLUMN_KEY' => '',
+ 'EXTRA' => '',
+ 'PRIVILEGES' => 'select,insert,update,references',
+ 'COLUMN_COMMENT' => '创建时间',
+ 'GENERATION_EXPRESSION' => '',
+ 'SRS_ID' => NULL,
+ ),
+ 'update_time' =>
+ array(
+ 'TABLE_CATALOG' => 'def',
+ 'TABLE_NAME' => 'kg_online',
+ 'COLUMN_NAME' => 'update_time',
+ 'ORDINAL_POSITION' => '7',
+ 'COLUMN_DEFAULT' => '0',
+ 'IS_NULLABLE' => 'NO',
+ 'DATA_TYPE' => 'int',
+ 'CHARACTER_MAXIMUM_LENGTH' => NULL,
+ 'CHARACTER_OCTET_LENGTH' => NULL,
+ 'NUMERIC_PRECISION' => '10',
+ 'NUMERIC_SCALE' => '0',
+ 'DATETIME_PRECISION' => NULL,
+ 'CHARACTER_SET_NAME' => NULL,
+ 'COLLATION_NAME' => NULL,
+ 'COLUMN_TYPE' => 'int unsigned',
+ 'COLUMN_KEY' => '',
+ 'EXTRA' => '',
+ 'PRIVILEGES' => 'select,insert,update,references',
+ 'COLUMN_COMMENT' => '更新时间',
+ 'GENERATION_EXPRESSION' => '',
+ 'SRS_ID' => NULL,
+ ),
+ ),
+ 'indexes' =>
+ array(
+ 'active_time' =>
+ array(
+ 1 =>
+ array(
+ 'Table' => 'kg_online',
+ 'Non_unique' => '1',
+ 'Key_name' => 'active_time',
+ 'Seq_in_index' => '1',
+ 'Column_name' => 'active_time',
+ 'Collation' => 'A',
+ 'Sub_part' => NULL,
+ 'Packed' => NULL,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ ),
+ 'PRIMARY' =>
+ array(
+ 1 =>
+ array(
+ 'Table' => 'kg_online',
+ 'Non_unique' => '0',
+ 'Key_name' => 'PRIMARY',
+ 'Seq_in_index' => '1',
+ 'Column_name' => 'id',
+ 'Collation' => 'A',
+ 'Sub_part' => NULL,
+ 'Packed' => NULL,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ ),
+ 'user_id' =>
+ array(
+ 1 =>
+ array(
+ 'Table' => 'kg_online',
+ 'Non_unique' => '1',
+ 'Key_name' => 'user_id',
+ 'Seq_in_index' => '1',
+ 'Column_name' => 'user_id',
+ 'Collation' => 'A',
+ 'Sub_part' => NULL,
+ 'Packed' => NULL,
+ 'Null' => '',
+ 'Index_type' => 'BTREE',
+ 'Comment' => '',
+ 'Index_comment' => '',
+ ),
+ ),
+ ),
+ 'foreign_keys' => NULL,
+ ),
'kg_order' =>
array(
'table' =>
@@ -9752,14 +9993,14 @@ return array(
'COLUMN_DEFAULT' => '',
'IS_NULLABLE' => 'NO',
'DATA_TYPE' => 'varchar',
- 'CHARACTER_MAXIMUM_LENGTH' => '30',
- 'CHARACTER_OCTET_LENGTH' => '120',
+ 'CHARACTER_MAXIMUM_LENGTH' => '64',
+ 'CHARACTER_OCTET_LENGTH' => '256',
'NUMERIC_PRECISION' => NULL,
'NUMERIC_SCALE' => NULL,
'DATETIME_PRECISION' => NULL,
'CHARACTER_SET_NAME' => 'utf8mb4',
'COLLATION_NAME' => 'utf8mb4_general_ci',
- 'COLUMN_TYPE' => 'varchar(30)',
+ 'COLUMN_TYPE' => 'varchar(64)',
'COLUMN_KEY' => '',
'EXTRA' => '',
'PRIVILEGES' => 'select,insert,update,references',
@@ -12811,7 +13052,7 @@ return array(
'CHARACTER_SET_NAME' => NULL,
'COLLATION_NAME' => NULL,
'COLUMN_TYPE' => 'int',
- 'COLUMN_KEY' => 'MUL',
+ 'COLUMN_KEY' => '',
'EXTRA' => '',
'PRIVILEGES' => 'select,insert,update,references',
'COLUMN_COMMENT' => '',
@@ -13429,14 +13670,14 @@ return array(
'COLUMN_DEFAULT' => '',
'IS_NULLABLE' => 'NO',
'DATA_TYPE' => 'varchar',
- 'CHARACTER_MAXIMUM_LENGTH' => '32',
- 'CHARACTER_OCTET_LENGTH' => '128',
+ 'CHARACTER_MAXIMUM_LENGTH' => '64',
+ 'CHARACTER_OCTET_LENGTH' => '256',
'NUMERIC_PRECISION' => NULL,
'NUMERIC_SCALE' => NULL,
'DATETIME_PRECISION' => NULL,
'CHARACTER_SET_NAME' => 'utf8mb4',
'COLLATION_NAME' => 'utf8mb4_general_ci',
- 'COLUMN_TYPE' => 'varchar(32)',
+ 'COLUMN_TYPE' => 'varchar(64)',
'COLUMN_KEY' => '',
'EXTRA' => '',
'PRIVILEGES' => 'select,insert,update,references',
diff --git a/public/static/admin/css/common.css b/public/static/admin/css/common.css
index 270217cc8abbef0bf751cea2943ab485e2fde6d6..23231fefd2d120c4e1f3a39c35e7a1ca1fcce4e1 100644
--- a/public/static/admin/css/common.css
+++ b/public/static/admin/css/common.css
@@ -258,3 +258,20 @@ img.kg-qrcode {
font-size: 20px;
color: rgb(0, 150, 136);
}
+
+.kg-search-form {
+ margin-bottom: 30px;
+}
+
+.kg-search-form .layui-form-label {
+ width: auto;
+}
+
+.kg-search-form .layui-input-inline:last-child {
+ margin-left: 10px;
+}
+
+.kg-chart {
+ width: 100%;
+ height: 480px;
+}