RegisterHourlyStatsController.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use App\Http\Controllers\Controller;
  4. use App\Http\logic\api\RegisterHourlyStatsLogic;
  5. use Illuminate\Http\Request;
  6. class RegisterHourlyStatsController extends Controller
  7. {
  8. /**
  9. * 获取按小时分布的注册用户、首充付费人数统计
  10. *
  11. * @param Request $request
  12. * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response
  13. */
  14. public function getHourlyStats(Request $request)
  15. {
  16. // 获取日期参数,如果没有提供则使用今天
  17. $date = $request->input('date', date('Y-m-d'));
  18. $channel = (int)$request->input('channel', 104); // 渠道ID,默认102
  19. $format = $request->input('format', 'chart'); // json 或 chart
  20. $logic = new RegisterHourlyStatsLogic();
  21. $result = $logic->getHourlyStats($date, $channel);
  22. if ($result === false) {
  23. if ($format === 'chart') {
  24. return response('<html><body><h3>错误: ' . htmlspecialchars($logic->getError()) . '</h3></body></html>', 400);
  25. }
  26. return apiReturnFail($logic->getError());
  27. }
  28. // 如果请求图表格式,返回 HTML 图表页面
  29. if ($format === 'chart') {
  30. return $this->renderChart($result, $date, $channel);
  31. }
  32. // 默认返回 JSON
  33. return apiReturnSuc($result);
  34. }
  35. /**
  36. * 渲染 HTML 图表页面
  37. *
  38. * @param array $data
  39. * @param string $date
  40. * @param int $channel
  41. * @return \Illuminate\Http\Response
  42. */
  43. private function renderChart($data, $date, $channel = 102)
  44. {
  45. // 准备图表数据
  46. $hours = [];
  47. $registerCounts = [];
  48. $chargeCounts = [];
  49. $chargeRates = [];
  50. foreach ($data as $row) {
  51. $hours[] = $row['hour_display'];
  52. $registerCounts[] = $row['register_count'];
  53. $chargeCounts[] = $row['first_charge_count'];
  54. $chargeRates[] = $row['charge_rate'];
  55. }
  56. $html = '<!DOCTYPE html>
  57. <html>
  58. <head>
  59. <meta charset="UTF-8">
  60. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  61. <title>注册用户按小时统计图表</title>
  62. <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
  63. <style>
  64. * {
  65. box-sizing: border-box;
  66. }
  67. body {
  68. font-family: Arial, "Microsoft YaHei", sans-serif;
  69. margin: 0;
  70. padding: 20px;
  71. background-color: #f5f5f5;
  72. }
  73. .container {
  74. max-width: 1400px;
  75. margin: 0 auto;
  76. background-color: white;
  77. padding: 30px;
  78. border-radius: 8px;
  79. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  80. }
  81. h1 {
  82. color: #333;
  83. margin-bottom: 20px;
  84. text-align: center;
  85. }
  86. .filter-section {
  87. display: flex;
  88. gap: 20px;
  89. align-items: center;
  90. margin-bottom: 30px;
  91. padding: 20px;
  92. background-color: #f9f9f9;
  93. border-radius: 6px;
  94. }
  95. .filter-item {
  96. display: flex;
  97. align-items: center;
  98. gap: 10px;
  99. }
  100. .filter-item label {
  101. font-weight: bold;
  102. color: #555;
  103. }
  104. .filter-item input, .filter-item select {
  105. padding: 8px 12px;
  106. border: 1px solid #ddd;
  107. border-radius: 4px;
  108. font-size: 14px;
  109. }
  110. .filter-item button {
  111. padding: 8px 20px;
  112. background-color: #4CAF50;
  113. color: white;
  114. border: none;
  115. border-radius: 4px;
  116. cursor: pointer;
  117. font-size: 14px;
  118. font-weight: bold;
  119. }
  120. .filter-item button:hover {
  121. background-color: #45a049;
  122. }
  123. .chart-container {
  124. margin-bottom: 40px;
  125. position: relative;
  126. height: 400px;
  127. }
  128. .chart-title {
  129. font-size: 18px;
  130. font-weight: bold;
  131. margin-bottom: 15px;
  132. color: #333;
  133. text-align: center;
  134. }
  135. .info {
  136. color: #666;
  137. margin-bottom: 20px;
  138. font-size: 14px;
  139. text-align: center;
  140. }
  141. </style>
  142. </head>
  143. <body>
  144. <div class="container">
  145. <h1>注册用户按小时统计图表</h1>
  146. <div class="info">
  147. <strong>统计日期:</strong> ' . htmlspecialchars($date) . ' |
  148. <strong>渠道:</strong> ' . htmlspecialchars($channel) . ' |
  149. <strong>统计时间:</strong> ' . date('Y-m-d H:i:s') . '
  150. </div>
  151. <div class="filter-section">
  152. <div class="filter-item">
  153. <label for="dateInput">选择日期:</label>
  154. <input type="date" id="dateInput" value="' . htmlspecialchars($date) . '" max="' . date('Y-m-d') . '">
  155. </div>
  156. <div class="filter-item">
  157. <label for="channelInput">渠道:</label>
  158. <select id="channelInput">
  159. <option value="104"' . ($channel == 104 ? ' selected' : '') . '>104</option>
  160. <option value="114"' . ($channel == 114 ? ' selected' : '') . '>114</option>
  161. </select>
  162. </div>
  163. <div class="filter-item">
  164. <button onclick="loadData()">查询</button>
  165. </div>
  166. </div>
  167. <div class="chart-container">
  168. <div class="chart-title">注册人数 & 新增付费人数(柱状图)</div>
  169. <canvas id="barChart"></canvas>
  170. </div>
  171. <div class="chart-container">
  172. <div class="chart-title">新增付费率(折线图)</div>
  173. <canvas id="lineChart"></canvas>
  174. </div>
  175. </div>
  176. <script>
  177. const hours = ' . json_encode($hours, JSON_UNESCAPED_UNICODE) . ';
  178. const registerCounts = ' . json_encode($registerCounts, JSON_UNESCAPED_UNICODE) . ';
  179. const chargeCounts = ' . json_encode($chargeCounts, JSON_UNESCAPED_UNICODE) . ';
  180. const chargeRates = ' . json_encode($chargeRates, JSON_UNESCAPED_UNICODE) . ';
  181. // 柱状图配置
  182. const barCtx = document.getElementById("barChart").getContext("2d");
  183. const barChart = new Chart(barCtx, {
  184. type: "bar",
  185. data: {
  186. labels: hours,
  187. datasets: [
  188. {
  189. label: "注册人数",
  190. data: registerCounts,
  191. backgroundColor: "rgba(54, 162, 235, 0.6)",
  192. borderColor: "rgba(54, 162, 235, 1)",
  193. borderWidth: 1
  194. },
  195. {
  196. label: "新增付费人数",
  197. data: chargeCounts,
  198. backgroundColor: "rgba(255, 99, 132, 0.6)",
  199. borderColor: "rgba(255, 99, 132, 1)",
  200. borderWidth: 1
  201. }
  202. ]
  203. },
  204. options: {
  205. responsive: true,
  206. maintainAspectRatio: false,
  207. scales: {
  208. y: {
  209. beginAtZero: true,
  210. ticks: {
  211. stepSize: 1
  212. }
  213. }
  214. },
  215. plugins: {
  216. legend: {
  217. display: true,
  218. position: "top"
  219. },
  220. tooltip: {
  221. mode: "index",
  222. intersect: false
  223. }
  224. }
  225. }
  226. });
  227. // 折线图配置
  228. const lineCtx = document.getElementById("lineChart").getContext("2d");
  229. const lineChart = new Chart(lineCtx, {
  230. type: "line",
  231. data: {
  232. labels: hours,
  233. datasets: [
  234. {
  235. label: "新增付费率 (%)",
  236. data: chargeRates,
  237. borderColor: "rgba(75, 192, 192, 1)",
  238. backgroundColor: "rgba(75, 192, 192, 0.2)",
  239. borderWidth: 2,
  240. fill: true,
  241. tension: 0.4,
  242. pointRadius: 4,
  243. pointHoverRadius: 6
  244. }
  245. ]
  246. },
  247. options: {
  248. responsive: true,
  249. maintainAspectRatio: false,
  250. scales: {
  251. y: {
  252. beginAtZero: true,
  253. max: 100,
  254. ticks: {
  255. callback: function(value) {
  256. return value + "%";
  257. }
  258. }
  259. }
  260. },
  261. plugins: {
  262. legend: {
  263. display: true,
  264. position: "top"
  265. },
  266. tooltip: {
  267. callbacks: {
  268. label: function(context) {
  269. return "新增付费率: " + context.parsed.y + "%";
  270. }
  271. }
  272. }
  273. }
  274. }
  275. });
  276. // 加载数据函数
  277. function loadData() {
  278. const date = document.getElementById("dateInput").value;
  279. const channel = document.getElementById("channelInput").value;
  280. if (!date) {
  281. alert("请选择日期");
  282. return;
  283. }
  284. // 构建URL
  285. const url = window.location.pathname + "?date=" + date + "&channel=" + channel + "&format=chart";
  286. // 重新加载页面
  287. window.location.href = url;
  288. }
  289. </script>
  290. </body>
  291. </html>';
  292. return response($html, 200)->header('Content-Type', 'text/html; charset=utf-8');
  293. }
  294. }