statistics_region.php 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441
  1. <?php
  2. /**
  3. * 地区统计分析模块
  4. *
  5. * 包含与地区相关的数据分析功能
  6. */
  7. require_once 'statistics_utils.php';
  8. /**
  9. * 获取客户国家分布
  10. *
  11. * @param mysqli $conn 数据库连接
  12. * @param array|int|null $employee_filter 业务员ID或ID数组,用于过滤数据
  13. * @return mysqli_result 客户国家分布数据结果集
  14. */
  15. function getCustomerCountryDistribution($conn, $employee_filter = null) {
  16. $employee_condition = "";
  17. // 如果有业务员过滤条件
  18. if ($employee_filter !== null) {
  19. if (is_array($employee_filter) && !empty($employee_filter)) {
  20. // 处理数组形式的业务员ID列表
  21. $emp_ids = [];
  22. foreach ($employee_filter as $emp_id) {
  23. if (is_numeric($emp_id)) {
  24. $emp_ids[] = intval($emp_id);
  25. }
  26. }
  27. if (!empty($emp_ids)) {
  28. $emp_ids_str = implode(',', $emp_ids);
  29. $employee_condition = " WHERE cu.cs_belong IN ($emp_ids_str)";
  30. }
  31. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  32. // 处理单个业务员ID
  33. $employee_filter = intval($employee_filter);
  34. $employee_condition = " WHERE cu.cs_belong = $employee_filter";
  35. }
  36. }
  37. $sql = "SELECT
  38. c.countryName,
  39. COUNT(cu.id) as customer_count
  40. FROM customer cu
  41. JOIN country c ON cu.cs_country = c.id
  42. $employee_condition
  43. GROUP BY cu.cs_country
  44. ORDER BY customer_count DESC
  45. LIMIT 10";
  46. return $conn->query($sql);
  47. }
  48. /**
  49. * 获取不同地区的订单数量
  50. *
  51. * @param mysqli $conn 数据库连接
  52. * @param string $start_date 开始日期
  53. * @param string $end_date 结束日期
  54. * @param array|int|null $employee_filter 业务员ID或ID数组,用于过滤数据
  55. * @return mysqli_result 地区订单数据结果集
  56. */
  57. function getOrdersByRegion($conn, $start_date, $end_date, $employee_filter = null) {
  58. $employee_condition = "";
  59. $params = [$start_date, $end_date];
  60. $types = "ss";
  61. // 如果有业务员过滤条件
  62. if ($employee_filter !== null) {
  63. if (is_array($employee_filter) && !empty($employee_filter)) {
  64. // 处理数组形式的业务员ID列表
  65. $emp_ids = [];
  66. foreach ($employee_filter as $emp_id) {
  67. if (is_numeric($emp_id)) {
  68. $emp_ids[] = intval($emp_id);
  69. }
  70. }
  71. if (!empty($emp_ids)) {
  72. $emp_ids_str = implode(',', $emp_ids);
  73. $employee_condition = " AND cu.cs_belong IN ($emp_ids_str)";
  74. }
  75. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  76. // 处理单个业务员ID
  77. $employee_filter = intval($employee_filter);
  78. $employee_condition = " AND cu.cs_belong = $employee_filter";
  79. }
  80. }
  81. $sql = "SELECT
  82. c.countryName,
  83. COUNT(DISTINCT o.id) as order_count,
  84. SUM(o.total_amount) as total_amount,
  85. (SELECT SUM(oi_sub.quantity)
  86. FROM order_items oi_sub
  87. JOIN orders o_sub ON oi_sub.order_id = o_sub.id
  88. JOIN customer cu_sub ON o_sub.customer_id = cu_sub.id
  89. WHERE o_sub.order_date BETWEEN ? AND ?
  90. AND cu_sub.cs_country = cu.cs_country
  91. $employee_condition) as total_quantity
  92. FROM orders o
  93. JOIN customer cu ON o.customer_id = cu.id
  94. JOIN country c ON cu.cs_country = c.id
  95. WHERE o.order_date BETWEEN ? AND ?
  96. $employee_condition
  97. GROUP BY cu.cs_country
  98. ORDER BY total_quantity DESC
  99. LIMIT 10";
  100. // 添加额外的参数用于子查询
  101. $params = array_merge([$start_date, $end_date], $params);
  102. $types = "ss" . $types;
  103. $stmt = $conn->prepare($sql);
  104. $stmt->bind_param($types, ...$params);
  105. $stmt->execute();
  106. return $stmt->get_result();
  107. }
  108. /**
  109. * 获取地区销售同比环比数据
  110. *
  111. * @param mysqli $conn 数据库连接
  112. * @param string $current_start 当前周期开始日期
  113. * @param string $current_end 当前周期结束日期
  114. * @param array|int|null $employee_filter 业务员ID或ID数组,用于过滤数据
  115. * @return array 地区销售同比环比数据
  116. */
  117. function getRegionSalesComparison($conn, $current_start, $current_end, $employee_filter = null) {
  118. // 计算上一个相同时长的周期
  119. $current_start_date = new DateTime($current_start);
  120. $current_end_date = new DateTime($current_end);
  121. $interval = $current_start_date->diff($current_end_date);
  122. $prev_end_date = clone $current_start_date;
  123. $prev_end_date->modify('-1 day');
  124. $prev_start_date = clone $prev_end_date;
  125. $prev_start_date->sub($interval);
  126. $prev_start = $prev_start_date->format('Y-m-d');
  127. $prev_end = $prev_end_date->format('Y-m-d') . ' 23:59:59';
  128. // 如果有业务员过滤条件
  129. $employee_condition = "";
  130. if ($employee_filter !== null) {
  131. if (is_array($employee_filter) && !empty($employee_filter)) {
  132. // 处理数组形式的业务员ID列表
  133. $emp_ids = [];
  134. foreach ($employee_filter as $emp_id) {
  135. if (is_numeric($emp_id)) {
  136. $emp_ids[] = intval($emp_id);
  137. }
  138. }
  139. if (!empty($emp_ids)) {
  140. $emp_ids_str = implode(',', $emp_ids);
  141. $employee_condition = " AND cu.cs_belong IN ($emp_ids_str)";
  142. }
  143. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  144. // 处理单个业务员ID
  145. $employee_filter = intval($employee_filter);
  146. $employee_condition = " AND cu.cs_belong = $employee_filter";
  147. }
  148. }
  149. // 获取当前周期数据
  150. $sql = "SELECT
  151. c.countryName,
  152. COUNT(o.id) as order_count,
  153. SUM(o.total_amount) as total_amount
  154. FROM orders o
  155. JOIN customer cu ON o.customer_id = cu.id
  156. JOIN country c ON cu.cs_country = c.id
  157. WHERE o.order_date BETWEEN ? AND ?
  158. $employee_condition
  159. GROUP BY cu.cs_country
  160. ORDER BY total_amount DESC
  161. LIMIT 5";
  162. $stmt = $conn->prepare($sql);
  163. $stmt->bind_param("ss", $current_start, $current_end);
  164. $stmt->execute();
  165. $current_result = $stmt->get_result();
  166. $current_data = [];
  167. while ($row = $current_result->fetch_assoc()) {
  168. $current_data[$row['countryName']] = [
  169. 'order_count' => $row['order_count'],
  170. 'total_amount' => $row['total_amount']
  171. ];
  172. }
  173. // 获取上一个周期数据
  174. $stmt = $conn->prepare($sql);
  175. $stmt->bind_param("ss", $prev_start, $prev_end);
  176. $stmt->execute();
  177. $prev_result = $stmt->get_result();
  178. $prev_data = [];
  179. while ($row = $prev_result->fetch_assoc()) {
  180. $prev_data[$row['countryName']] = [
  181. 'order_count' => $row['order_count'],
  182. 'total_amount' => $row['total_amount']
  183. ];
  184. }
  185. // 计算同比变化
  186. $comparison_data = [];
  187. foreach ($current_data as $country => $current) {
  188. $prev = $prev_data[$country] ?? ['order_count' => 0, 'total_amount' => 0];
  189. $order_growth = $prev['order_count'] > 0
  190. ? (($current['order_count'] - $prev['order_count']) / $prev['order_count']) * 100
  191. : 100;
  192. $amount_growth = $prev['total_amount'] > 0
  193. ? (($current['total_amount'] - $prev['total_amount']) / $prev['total_amount']) * 100
  194. : 100;
  195. $comparison_data[] = [
  196. 'countryName' => $country,
  197. 'current_orders' => $current['order_count'],
  198. 'prev_orders' => $prev['order_count'],
  199. 'order_growth' => $order_growth,
  200. 'current_amount' => $current['total_amount'],
  201. 'prev_amount' => $prev['total_amount'],
  202. 'amount_growth' => $amount_growth
  203. ];
  204. }
  205. return $comparison_data;
  206. }
  207. /**
  208. * 渲染客户国家分布图
  209. *
  210. * @param array $country_labels 国家标签
  211. * @param array $country_data 国家数据
  212. * @return void
  213. */
  214. function renderCustomerCountryDistributionChart($country_labels, $country_data) {
  215. ?>
  216. <div class="chart-container">
  217. <div class="chart-header">
  218. <h2 class="chart-title">客户国家分布</h2>
  219. </div>
  220. <canvas id="countryDistributionChart"></canvas>
  221. </div>
  222. <script>
  223. // 客户国家分布图
  224. var countryDistributionCtx = document.getElementById('countryDistributionChart').getContext('2d');
  225. var countryDistributionChart = new Chart(countryDistributionCtx, {
  226. type: 'pie',
  227. data: {
  228. labels: <?php echo json_encode($country_labels); ?>,
  229. datasets: [{
  230. data: <?php echo json_encode($country_data); ?>,
  231. backgroundColor: [
  232. 'rgba(255, 99, 132, 0.7)',
  233. 'rgba(54, 162, 235, 0.7)',
  234. 'rgba(255, 206, 86, 0.7)',
  235. 'rgba(75, 192, 192, 0.7)',
  236. 'rgba(153, 102, 255, 0.7)',
  237. 'rgba(255, 159, 64, 0.7)',
  238. 'rgba(199, 199, 199, 0.7)',
  239. 'rgba(83, 102, 255, 0.7)',
  240. 'rgba(40, 159, 64, 0.7)',
  241. 'rgba(210, 199, 199, 0.7)'
  242. ],
  243. borderWidth: 1
  244. }]
  245. },
  246. options: {
  247. responsive: true,
  248. plugins: {
  249. legend: {
  250. position: 'right',
  251. }
  252. }
  253. }
  254. });
  255. </script>
  256. <?php
  257. }
  258. /**
  259. * 渲染地区订单分析图
  260. *
  261. * @param array $region_labels 地区标签
  262. * @param array $region_orders 地区订单数量
  263. * @param array $region_quantities 地区产品数量
  264. * @return void
  265. */
  266. function renderRegionOrdersChart($region_labels, $region_orders, $region_quantities) {
  267. ?>
  268. <div class="chart-container">
  269. <div class="chart-header">
  270. <h2 class="chart-title">地区订单分析</h2>
  271. </div>
  272. <canvas id="regionOrdersChart"></canvas>
  273. </div>
  274. <script>
  275. // 地区订单分析图
  276. var regionOrdersCtx = document.getElementById('regionOrdersChart').getContext('2d');
  277. var regionOrdersChart = new Chart(regionOrdersCtx, {
  278. type: 'bar',
  279. data: {
  280. labels: <?php echo json_encode($region_labels); ?>,
  281. datasets: [
  282. {
  283. label: '订单数量',
  284. data: <?php echo json_encode($region_orders); ?>,
  285. backgroundColor: 'rgba(54, 162, 235, 0.6)',
  286. borderColor: 'rgba(54, 162, 235, 1)',
  287. borderWidth: 1,
  288. yAxisID: 'y-orders'
  289. },
  290. {
  291. label: '产品订购数量',
  292. data: <?php echo json_encode($region_quantities); ?>,
  293. backgroundColor: 'rgba(255, 99, 132, 0.6)',
  294. borderColor: 'rgba(255, 99, 132, 1)',
  295. borderWidth: 1,
  296. yAxisID: 'y-quantity'
  297. }
  298. ]
  299. },
  300. options: {
  301. responsive: true,
  302. scales: {
  303. x: {
  304. title: {
  305. display: true,
  306. text: '地区'
  307. }
  308. },
  309. 'y-orders': {
  310. type: 'linear',
  311. position: 'left',
  312. title: {
  313. display: true,
  314. text: '订单数量'
  315. },
  316. beginAtZero: true
  317. },
  318. 'y-quantity': {
  319. type: 'linear',
  320. position: 'right',
  321. title: {
  322. display: true,
  323. text: '产品订购数量'
  324. },
  325. beginAtZero: true,
  326. grid: {
  327. drawOnChartArea: false
  328. }
  329. }
  330. }
  331. }
  332. });
  333. </script>
  334. <?php
  335. }
  336. /**
  337. * 渲染地区销售同比环比表格
  338. *
  339. * @param array $comparison_data 比较数据
  340. * @return void
  341. */
  342. function renderRegionSalesComparisonTable($comparison_data) {
  343. ?>
  344. <div class="chart-container">
  345. <div class="chart-header">
  346. <h2 class="chart-title">地区销售同比分析</h2>
  347. </div>
  348. <table class="data-table">
  349. <thead>
  350. <tr>
  351. <th>国家/地区</th>
  352. <th>当前订单数</th>
  353. <th>上期订单数</th>
  354. <th>订单增长率</th>
  355. <th>当前销售额</th>
  356. <th>上期销售额</th>
  357. <th>销售额增长率</th>
  358. </tr>
  359. </thead>
  360. <tbody>
  361. <?php foreach ($comparison_data as $row): ?>
  362. <tr>
  363. <td><?php echo htmlspecialchars($row['countryName']); ?></td>
  364. <td><?php echo number_format($row['current_orders']); ?></td>
  365. <td><?php echo number_format($row['prev_orders']); ?></td>
  366. <td class="<?php echo $row['order_growth'] >= 0 ? 'positive' : 'negative'; ?>">
  367. <?php echo number_format($row['order_growth'], 2); ?>%
  368. </td>
  369. <td>¥<?php echo number_format($row['current_amount'], 2); ?></td>
  370. <td>¥<?php echo number_format($row['prev_amount'], 2); ?></td>
  371. <td class="<?php echo $row['amount_growth'] >= 0 ? 'positive' : 'negative'; ?>">
  372. <?php echo number_format($row['amount_growth'], 2); ?>%
  373. </td>
  374. </tr>
  375. <?php endforeach; ?>
  376. </tbody>
  377. </table>
  378. </div>
  379. <?php
  380. }
  381. /**
  382. * 获取地区总销售额及增长率
  383. *
  384. * @param mysqli $conn 数据库连接
  385. * @param string $start_date 开始日期
  386. * @param string $end_date 结束日期
  387. * @param array|int|null $employee_filter 业务员ID或ID数组,用于过滤数据
  388. * @return array 总销售额和增长率
  389. */
  390. function getRegionTotalSales($conn, $start_date, $end_date, $employee_filter = null) {
  391. // 如果有业务员过滤条件
  392. $employee_condition = "";
  393. if ($employee_filter !== null) {
  394. if (is_array($employee_filter) && !empty($employee_filter)) {
  395. // 处理数组形式的业务员ID列表
  396. $emp_ids = [];
  397. foreach ($employee_filter as $emp_id) {
  398. if (is_numeric($emp_id)) {
  399. $emp_ids[] = intval($emp_id);
  400. }
  401. }
  402. if (!empty($emp_ids)) {
  403. $emp_ids_str = implode(',', $emp_ids);
  404. $employee_condition = " AND cu.cs_belong IN ($emp_ids_str)";
  405. }
  406. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  407. // 处理单个业务员ID
  408. $employee_filter = intval($employee_filter);
  409. $employee_condition = " AND cu.cs_belong = $employee_filter";
  410. }
  411. }
  412. // 计算当前周期销售额
  413. $sql = "SELECT SUM(o.total_amount) as total_amount
  414. FROM orders o
  415. JOIN customer cu ON o.customer_id = cu.id
  416. WHERE o.order_date BETWEEN ? AND ?
  417. $employee_condition";
  418. $stmt = $conn->prepare($sql);
  419. $stmt->bind_param("ss", $start_date, $end_date);
  420. $stmt->execute();
  421. $result = $stmt->get_result();
  422. $row = $result->fetch_assoc();
  423. $current_amount = $row['total_amount'] ?? 0;
  424. // 计算上一个相同时长的周期
  425. $current_start_date = new DateTime($start_date);
  426. $current_end_date = new DateTime($end_date);
  427. $interval = $current_start_date->diff($current_end_date);
  428. $prev_end_date = clone $current_start_date;
  429. $prev_end_date->modify('-1 day');
  430. $prev_start_date = clone $prev_end_date;
  431. $prev_start_date->sub($interval);
  432. $prev_start = $prev_start_date->format('Y-m-d');
  433. $prev_end = $prev_end_date->format('Y-m-d') . ' 23:59:59';
  434. // 获取上一周期销售额
  435. $stmt = $conn->prepare($sql);
  436. $stmt->bind_param("ss", $prev_start, $prev_end);
  437. $stmt->execute();
  438. $result = $stmt->get_result();
  439. $row = $result->fetch_assoc();
  440. $prev_amount = $row['total_amount'] ?? 0;
  441. // 计算增长率
  442. $growth = 0;
  443. if ($prev_amount > 0) {
  444. $growth = (($current_amount - $prev_amount) / $prev_amount) * 100;
  445. }
  446. return [
  447. 'total_amount' => $current_amount,
  448. 'growth' => $growth
  449. ];
  450. }
  451. /**
  452. * 获取活跃国家数
  453. *
  454. * @param mysqli $conn 数据库连接
  455. * @param string $start_date 开始日期
  456. * @param string $end_date 结束日期
  457. * @param array|int|null $employee_filter 业务员ID或ID数组,用于过滤数据
  458. * @return array 活跃国家信息
  459. */
  460. function getActiveCountries($conn, $start_date, $end_date, $employee_filter = null) {
  461. // 如果有业务员过滤条件
  462. $employee_condition = "";
  463. if ($employee_filter !== null) {
  464. if (is_array($employee_filter) && !empty($employee_filter)) {
  465. // 处理数组形式的业务员ID列表
  466. $emp_ids = [];
  467. foreach ($employee_filter as $emp_id) {
  468. if (is_numeric($emp_id)) {
  469. $emp_ids[] = intval($emp_id);
  470. }
  471. }
  472. if (!empty($emp_ids)) {
  473. $emp_ids_str = implode(',', $emp_ids);
  474. $employee_condition = " AND cu.cs_belong IN ($emp_ids_str)";
  475. }
  476. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  477. // 处理单个业务员ID
  478. $employee_filter = intval($employee_filter);
  479. $employee_condition = " AND cu.cs_belong = $employee_filter";
  480. }
  481. }
  482. $sql = "SELECT COUNT(DISTINCT cu.cs_country) as country_count
  483. FROM orders o
  484. JOIN customer cu ON o.customer_id = cu.id
  485. WHERE o.order_date BETWEEN ? AND ?
  486. $employee_condition";
  487. $stmt = $conn->prepare($sql);
  488. $stmt->bind_param("ss", $start_date, $end_date);
  489. $stmt->execute();
  490. $result = $stmt->get_result();
  491. $row = $result->fetch_assoc();
  492. return [
  493. 'count' => $row['country_count'] ?? 0
  494. ];
  495. }
  496. /**
  497. * 获取各地区平均订单金额
  498. *
  499. * @param mysqli $conn 数据库连接
  500. * @param string $start_date 开始日期
  501. * @param string $end_date 结束日期
  502. * @param array|int|null $employee_filter 业务员ID或ID数组,用于过滤数据
  503. * @return array 各地区平均订单金额数据
  504. */
  505. function getAverageOrderByRegion($conn, $start_date, $end_date, $employee_filter = null) {
  506. // 如果有业务员过滤条件
  507. $employee_condition = "";
  508. if ($employee_filter !== null) {
  509. if (is_array($employee_filter) && !empty($employee_filter)) {
  510. // 处理数组形式的业务员ID列表
  511. $emp_ids = [];
  512. foreach ($employee_filter as $emp_id) {
  513. if (is_numeric($emp_id)) {
  514. $emp_ids[] = intval($emp_id);
  515. }
  516. }
  517. if (!empty($emp_ids)) {
  518. $emp_ids_str = implode(',', $emp_ids);
  519. $employee_condition = " AND cu.cs_belong IN ($emp_ids_str)";
  520. }
  521. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  522. // 处理单个业务员ID
  523. $employee_filter = intval($employee_filter);
  524. $employee_condition = " AND cu.cs_belong = $employee_filter";
  525. }
  526. }
  527. $sql = "SELECT
  528. c.countryName,
  529. AVG(o.total_amount) as avg_amount,
  530. COUNT(o.id) as order_count
  531. FROM orders o
  532. JOIN customer cu ON o.customer_id = cu.id
  533. JOIN country c ON cu.cs_country = c.id
  534. WHERE o.order_date BETWEEN ? AND ?
  535. $employee_condition
  536. GROUP BY cu.cs_country
  537. HAVING order_count >= 5
  538. ORDER BY avg_amount DESC
  539. LIMIT 10";
  540. $stmt = $conn->prepare($sql);
  541. $stmt->bind_param("ss", $start_date, $end_date);
  542. $stmt->execute();
  543. $result = $stmt->get_result();
  544. $regions = [];
  545. $total_avg = 0;
  546. $total_orders = 0;
  547. while ($row = $result->fetch_assoc()) {
  548. $regions[] = [
  549. 'countryName' => $row['countryName'],
  550. 'avg_amount' => $row['avg_amount'],
  551. 'order_count' => $row['order_count']
  552. ];
  553. $total_avg += $row['avg_amount'] * $row['order_count'];
  554. $total_orders += $row['order_count'];
  555. }
  556. // 计算全球平均订单金额
  557. $global_avg = $total_orders > 0 ? $total_avg / $total_orders : 0;
  558. return [
  559. 'regions' => $regions,
  560. 'global_avg' => $global_avg
  561. ];
  562. }
  563. /**
  564. * 获取各地区产品类别偏好
  565. *
  566. * @param mysqli $conn 数据库连接
  567. * @param string $start_date 开始日期
  568. * @param string $end_date 结束日期
  569. * @param array|int|null $employee_filter 业务员ID或ID数组,用于过滤数据
  570. * @return array 各地区产品类别偏好数据
  571. */
  572. function getRegionCategoryPreferences($conn, $start_date, $end_date, $employee_filter = null) {
  573. // 如果有业务员过滤条件
  574. $employee_condition = "";
  575. if ($employee_filter !== null) {
  576. if (is_array($employee_filter) && !empty($employee_filter)) {
  577. // 处理数组形式的业务员ID列表
  578. $emp_ids = [];
  579. foreach ($employee_filter as $emp_id) {
  580. if (is_numeric($emp_id)) {
  581. $emp_ids[] = intval($emp_id);
  582. }
  583. }
  584. if (!empty($emp_ids)) {
  585. $emp_ids_str = implode(',', $emp_ids);
  586. $employee_condition = " AND cu.cs_belong IN ($emp_ids_str)";
  587. }
  588. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  589. // 处理单个业务员ID
  590. $employee_filter = intval($employee_filter);
  591. $employee_condition = " AND cu.cs_belong = $employee_filter";
  592. }
  593. }
  594. $sql = "SELECT
  595. c.countryName,
  596. pc.name as category_name,
  597. SUM(oi.quantity) as total_quantity
  598. FROM orders o
  599. JOIN customer cu ON o.customer_id = cu.id
  600. JOIN country c ON cu.cs_country = c.id
  601. JOIN order_items oi ON o.id = oi.order_id
  602. JOIN products p ON oi.product_id = p.id
  603. JOIN product_categories pc ON p.category_id = pc.id
  604. WHERE o.order_date BETWEEN ? AND ?
  605. $employee_condition
  606. GROUP BY cu.cs_country, p.category_id
  607. ORDER BY c.countryName, total_quantity DESC";
  608. $stmt = $conn->prepare($sql);
  609. $stmt->bind_param("ss", $start_date, $end_date);
  610. $stmt->execute();
  611. $result = $stmt->get_result();
  612. $preferences = [];
  613. $current_country = '';
  614. $country_data = [];
  615. while ($row = $result->fetch_assoc()) {
  616. if ($current_country != $row['countryName']) {
  617. if (!empty($current_country)) {
  618. $preferences[$current_country] = $country_data;
  619. }
  620. $current_country = $row['countryName'];
  621. $country_data = [];
  622. }
  623. $country_data[] = [
  624. 'category' => $row['category_name'],
  625. 'quantity' => $row['total_quantity']
  626. ];
  627. }
  628. // 添加最后一个国家的数据
  629. if (!empty($current_country)) {
  630. $preferences[$current_country] = $country_data;
  631. }
  632. // 只保留前5个主要市场
  633. $top_markets = array_slice(array_keys($preferences), 0, 5);
  634. $filtered_preferences = [];
  635. foreach ($top_markets as $market) {
  636. $filtered_preferences[$market] = array_slice($preferences[$market], 0, 5);
  637. }
  638. return $filtered_preferences;
  639. }
  640. /**
  641. * 获取地区销售增长趋势
  642. *
  643. * @param mysqli $conn 数据库连接
  644. * @param string $start_date 开始日期
  645. * @param string $end_date 结束日期
  646. * @param string $period 时间粒度 (day/week/month)
  647. * @param array|int|null $employee_filter 业务员ID或ID数组,用于过滤数据
  648. * @return array 地区销售增长趋势数据
  649. */
  650. function getRegionGrowthTrends($conn, $start_date, $end_date, $period = 'month', $employee_filter = null) {
  651. $period_format = getPeriodFormat($period);
  652. // 如果有业务员过滤条件
  653. $employee_condition = "";
  654. if ($employee_filter !== null) {
  655. if (is_array($employee_filter) && !empty($employee_filter)) {
  656. // 处理数组形式的业务员ID列表
  657. $emp_ids = [];
  658. foreach ($employee_filter as $emp_id) {
  659. if (is_numeric($emp_id)) {
  660. $emp_ids[] = intval($emp_id);
  661. }
  662. }
  663. if (!empty($emp_ids)) {
  664. $emp_ids_str = implode(',', $emp_ids);
  665. $employee_condition = " AND cu.cs_belong IN ($emp_ids_str)";
  666. }
  667. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  668. // 处理单个业务员ID
  669. $employee_filter = intval($employee_filter);
  670. $employee_condition = " AND cu.cs_belong = $employee_filter";
  671. }
  672. }
  673. $sql = "SELECT
  674. c.countryName,
  675. DATE_FORMAT(o.order_date, ?) as time_period,
  676. SUM(o.total_amount) as total_amount
  677. FROM orders o
  678. JOIN customer cu ON o.customer_id = cu.id
  679. JOIN country c ON cu.cs_country = c.id
  680. WHERE o.order_date BETWEEN ? AND ?
  681. $employee_condition
  682. GROUP BY cu.cs_country, time_period
  683. ORDER BY c.countryName, time_period";
  684. $stmt = $conn->prepare($sql);
  685. $stmt->bind_param("sss", $period_format, $start_date, $end_date);
  686. $stmt->execute();
  687. $result = $stmt->get_result();
  688. $trends = [];
  689. $time_periods = [];
  690. while ($row = $result->fetch_assoc()) {
  691. if (!in_array($row['time_period'], $time_periods)) {
  692. $time_periods[] = $row['time_period'];
  693. }
  694. if (!isset($trends[$row['countryName']])) {
  695. $trends[$row['countryName']] = [];
  696. }
  697. $trends[$row['countryName']][$row['time_period']] = $row['total_amount'];
  698. }
  699. // 只保留前5个主要市场
  700. $top_markets = array_slice(array_keys($trends), 0, 5);
  701. $filtered_trends = [];
  702. foreach ($top_markets as $market) {
  703. $filtered_trends[$market] = $trends[$market];
  704. }
  705. return [
  706. 'time_periods' => $time_periods,
  707. 'trends' => $filtered_trends
  708. ];
  709. }
  710. /**
  711. * 获取地区季节性销售分析
  712. *
  713. * @param mysqli $conn 数据库连接
  714. * @param array|int|null $employee_filter 业务员ID或ID数组,用于过滤数据
  715. * @return array 地区季节性销售分析数据
  716. */
  717. function getRegionSeasonalAnalysis($conn, $employee_filter = null) {
  718. // 如果有业务员过滤条件
  719. $employee_condition = "";
  720. if ($employee_filter !== null) {
  721. if (is_array($employee_filter) && !empty($employee_filter)) {
  722. // 处理数组形式的业务员ID列表
  723. $emp_ids = [];
  724. foreach ($employee_filter as $emp_id) {
  725. if (is_numeric($emp_id)) {
  726. $emp_ids[] = intval($emp_id);
  727. }
  728. }
  729. if (!empty($emp_ids)) {
  730. $emp_ids_str = implode(',', $emp_ids);
  731. $employee_condition = " AND cu.cs_belong IN ($emp_ids_str)";
  732. }
  733. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  734. // 处理单个业务员ID
  735. $employee_filter = intval($employee_filter);
  736. $employee_condition = " AND cu.cs_belong = $employee_filter";
  737. }
  738. }
  739. $sql = "SELECT
  740. c.countryName,
  741. MONTH(o.order_date) as month,
  742. SUM(o.total_amount) as total_amount
  743. FROM orders o
  744. JOIN customer cu ON o.customer_id = cu.id
  745. JOIN country c ON cu.cs_country = c.id
  746. WHERE o.order_date >= DATE_SUB(CURDATE(), INTERVAL 2 YEAR)
  747. $employee_condition
  748. GROUP BY cu.cs_country, month
  749. ORDER BY c.countryName, month";
  750. $result = $conn->query($sql);
  751. $seasonal = [];
  752. $months = range(1, 12);
  753. while ($row = $result->fetch_assoc()) {
  754. if (!isset($seasonal[$row['countryName']])) {
  755. $seasonal[$row['countryName']] = array_fill(1, 12, 0);
  756. }
  757. $seasonal[$row['countryName']][$row['month']] += $row['total_amount'];
  758. }
  759. // 只保留前5个主要市场
  760. $top_markets = array_slice(array_keys($seasonal), 0, 5);
  761. $filtered_seasonal = [];
  762. foreach ($top_markets as $market) {
  763. $filtered_seasonal[$market] = array_values($seasonal[$market]);
  764. }
  765. return [
  766. 'months' => ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
  767. 'data' => $filtered_seasonal
  768. ];
  769. }
  770. /**
  771. * 获取地区销售预测数据
  772. *
  773. * @param mysqli $conn 数据库连接
  774. * @param string $start_date 开始日期
  775. * @param string $end_date 结束日期
  776. * @param array|int|null $employee_filter 业务员ID或ID数组,用于过滤数据
  777. * @return array 地区销售预测数据
  778. */
  779. function getRegionSalesForecast($conn, $start_date, $end_date, $employee_filter = null) {
  780. // 如果有业务员过滤条件
  781. $employee_condition = "";
  782. if ($employee_filter !== null) {
  783. if (is_array($employee_filter) && !empty($employee_filter)) {
  784. // 处理数组形式的业务员ID列表
  785. $emp_ids = [];
  786. foreach ($employee_filter as $emp_id) {
  787. if (is_numeric($emp_id)) {
  788. $emp_ids[] = intval($emp_id);
  789. }
  790. }
  791. if (!empty($emp_ids)) {
  792. $emp_ids_str = implode(',', $emp_ids);
  793. $employee_condition = " AND cu.cs_belong IN ($emp_ids_str)";
  794. }
  795. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  796. // 处理单个业务员ID
  797. $employee_filter = intval($employee_filter);
  798. $employee_condition = " AND cu.cs_belong = $employee_filter";
  799. }
  800. }
  801. // 获取过去12个月的销售数据作为基础
  802. $sql = "SELECT
  803. c.countryName,
  804. MONTH(o.order_date) as month,
  805. YEAR(o.order_date) as year,
  806. SUM(o.total_amount) as total_amount
  807. FROM orders o
  808. JOIN customer cu ON o.customer_id = cu.id
  809. JOIN country c ON cu.cs_country = c.id
  810. WHERE o.order_date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
  811. $employee_condition
  812. GROUP BY cu.cs_country, year, month
  813. ORDER BY c.countryName, year, month";
  814. $result = $conn->query($sql);
  815. $historical = [];
  816. while ($row = $result->fetch_assoc()) {
  817. $period = $row['year'] . '-' . str_pad($row['month'], 2, '0', STR_PAD_LEFT);
  818. if (!isset($historical[$row['countryName']])) {
  819. $historical[$row['countryName']] = [];
  820. }
  821. $historical[$row['countryName']][$period] = $row['total_amount'];
  822. }
  823. // 生成未来6个月的预测
  824. $forecast = [];
  825. $periods = [];
  826. $current_month = (int)date('m');
  827. $current_year = (int)date('Y');
  828. // 收集所有历史数据点时间
  829. $all_periods = [];
  830. foreach ($historical as $country => $data) {
  831. foreach (array_keys($data) as $period) {
  832. $all_periods[$period] = true;
  833. }
  834. }
  835. $all_periods = array_keys($all_periods);
  836. sort($all_periods);
  837. // 生成未来6个月的预测点
  838. $future_periods = [];
  839. for ($i = 1; $i <= 6; $i++) {
  840. $forecast_month = ($current_month + $i) % 12;
  841. $forecast_month = $forecast_month == 0 ? 12 : $forecast_month;
  842. $forecast_year = $current_year + floor(($current_month + $i - 1) / 12);
  843. $period = $forecast_year . '-' . str_pad($forecast_month, 2, '0', STR_PAD_LEFT);
  844. $future_periods[] = $period;
  845. }
  846. // 合并历史和预测时间点
  847. $all_chart_periods = array_merge($all_periods, $future_periods);
  848. // 只选取主要的5个市场做预测
  849. $top_markets = array_slice(array_keys($historical), 0, 5);
  850. $forecast_data = [];
  851. foreach ($top_markets as $market) {
  852. $forecast_data[$market] = [];
  853. // 为每个市场生成简单的预测
  854. // 这里使用简单的线性增长预测
  855. // 实际应用中可以采用更复杂的时间序列预测算法
  856. // 计算过去几个月的平均增长率
  857. $growth_rate = 0.05; // 默认月度增长率为5%
  858. if (count($historical[$market]) >= 2) {
  859. $values = array_values($historical[$market]);
  860. $start_val = array_shift($values);
  861. $end_val = array_pop($values);
  862. if ($start_val > 0) {
  863. $periods_count = count($historical[$market]) - 1;
  864. $total_growth = ($end_val / $start_val) - 1;
  865. $growth_rate = pow(1 + $total_growth, 1 / $periods_count) - 1;
  866. // 限制增长率在合理范围内
  867. $growth_rate = max(-0.2, min(0.2, $growth_rate));
  868. }
  869. }
  870. // 对于历史数据,直接使用实际值
  871. foreach ($all_periods as $period) {
  872. $forecast_data[$market][$period] = [
  873. 'value' => $historical[$market][$period] ?? null,
  874. 'is_forecast' => false
  875. ];
  876. }
  877. // 对于预测数据,基于最后一个历史数据点和增长率计算
  878. $last_period = end($all_periods);
  879. $last_value = $historical[$market][$last_period] ?? array_values($historical[$market])[count($historical[$market])-1];
  880. foreach ($future_periods as $i => $period) {
  881. $forecast_value = $last_value * pow(1 + $growth_rate, $i + 1);
  882. $forecast_data[$market][$period] = [
  883. 'value' => $forecast_value,
  884. 'is_forecast' => true
  885. ];
  886. }
  887. }
  888. return [
  889. 'periods' => $all_chart_periods,
  890. 'forecast' => $forecast_data
  891. ];
  892. }
  893. /**
  894. * 渲染平均订单金额分析图表
  895. *
  896. * @param array $region_data 地区平均订单金额数据
  897. * @return void
  898. */
  899. function renderAverageOrderByRegionChart($region_data) {
  900. $region_labels = [];
  901. $avg_amounts = [];
  902. $order_counts = [];
  903. foreach ($region_data as $region) {
  904. $region_labels[] = $region['countryName'];
  905. $avg_amounts[] = $region['avg_amount'];
  906. $order_counts[] = $region['order_count'];
  907. }
  908. ?>
  909. <div class="chart-header">
  910. <h2 class="chart-title">地区平均订单金额分析</h2>
  911. </div>
  912. <canvas id="avgOrderChart"></canvas>
  913. <script>
  914. // 地区平均订单金额分析图
  915. var avgOrderCtx = document.getElementById('avgOrderChart').getContext('2d');
  916. var avgOrderChart = new Chart(avgOrderCtx, {
  917. type: 'bar',
  918. data: {
  919. labels: <?php echo json_encode($region_labels); ?>,
  920. datasets: [
  921. {
  922. label: '平均订单金额',
  923. data: <?php echo json_encode($avg_amounts); ?>,
  924. backgroundColor: 'rgba(75, 192, 192, 0.6)',
  925. borderColor: 'rgba(75, 192, 192, 1)',
  926. borderWidth: 1,
  927. yAxisID: 'y-amount'
  928. },
  929. {
  930. type: 'line',
  931. label: '订单数量',
  932. data: <?php echo json_encode($order_counts); ?>,
  933. backgroundColor: 'rgba(255, 159, 64, 0.6)',
  934. borderColor: 'rgba(255, 159, 64, 1)',
  935. borderWidth: 2,
  936. fill: false,
  937. yAxisID: 'y-count'
  938. }
  939. ]
  940. },
  941. options: {
  942. responsive: true,
  943. scales: {
  944. x: {
  945. title: {
  946. display: true,
  947. text: '地区'
  948. }
  949. },
  950. 'y-amount': {
  951. type: 'linear',
  952. position: 'left',
  953. title: {
  954. display: true,
  955. text: '平均订单金额'
  956. }
  957. },
  958. 'y-count': {
  959. type: 'linear',
  960. position: 'right',
  961. title: {
  962. display: true,
  963. text: '订单数量'
  964. },
  965. grid: {
  966. drawOnChartArea: false
  967. }
  968. }
  969. }
  970. }
  971. });
  972. </script>
  973. <?php
  974. }
  975. /**
  976. * 渲染地区产品类别偏好图表
  977. *
  978. * @param array $preferences 地区产品类别偏好数据
  979. * @return void
  980. */
  981. function renderRegionCategoryPreferencesChart($preferences) {
  982. ?>
  983. <div class="chart-header">
  984. <h2 class="chart-title">各地区产品类别偏好</h2>
  985. </div>
  986. <div class="grid-row">
  987. <?php foreach ($preferences as $country => $categories): ?>
  988. <?php
  989. $category_labels = [];
  990. $category_data = [];
  991. foreach ($categories as $cat) {
  992. $category_labels[] = $cat['category'];
  993. $category_data[] = $cat['quantity'];
  994. }
  995. ?>
  996. <div class="grid-column">
  997. <div class="subchart-container">
  998. <h3 class="subchart-title"><?php echo $country; ?></h3>
  999. <canvas id="categoryChart<?php echo md5($country); ?>"></canvas>
  1000. </div>
  1001. </div>
  1002. <script>
  1003. // <?php echo $country; ?> 产品类别偏好
  1004. var categoryCtx<?php echo md5($country); ?> = document.getElementById('categoryChart<?php echo md5($country); ?>').getContext('2d');
  1005. var categoryChart<?php echo md5($country); ?> = new Chart(categoryCtx<?php echo md5($country); ?>, {
  1006. type: 'doughnut',
  1007. data: {
  1008. labels: <?php echo json_encode($category_labels); ?>,
  1009. datasets: [{
  1010. data: <?php echo json_encode($category_data); ?>,
  1011. backgroundColor: [
  1012. 'rgba(255, 99, 132, 0.7)',
  1013. 'rgba(54, 162, 235, 0.7)',
  1014. 'rgba(255, 206, 86, 0.7)',
  1015. 'rgba(75, 192, 192, 0.7)',
  1016. 'rgba(153, 102, 255, 0.7)'
  1017. ],
  1018. borderWidth: 1
  1019. }]
  1020. },
  1021. options: {
  1022. responsive: true,
  1023. plugins: {
  1024. legend: {
  1025. position: 'right',
  1026. }
  1027. }
  1028. }
  1029. });
  1030. </script>
  1031. <?php endforeach; ?>
  1032. </div>
  1033. <?php
  1034. }
  1035. /**
  1036. * 渲染地区销售增长趋势图表
  1037. *
  1038. * @param array $growth_data 地区销售增长趋势数据
  1039. * @return void
  1040. */
  1041. function renderRegionGrowthTrendsChart($growth_data) {
  1042. $time_periods = $growth_data['time_periods'];
  1043. $trends = $growth_data['trends'];
  1044. $datasets = [];
  1045. $colors = [
  1046. ['rgba(255, 99, 132, 0.6)', 'rgba(255, 99, 132, 1)'],
  1047. ['rgba(54, 162, 235, 0.6)', 'rgba(54, 162, 235, 1)'],
  1048. ['rgba(255, 206, 86, 0.6)', 'rgba(255, 206, 86, 1)'],
  1049. ['rgba(75, 192, 192, 0.6)', 'rgba(75, 192, 192, 1)'],
  1050. ['rgba(153, 102, 255, 0.6)', 'rgba(153, 102, 255, 1)']
  1051. ];
  1052. $i = 0;
  1053. foreach ($trends as $country => $data) {
  1054. $dataset = [
  1055. 'label' => $country,
  1056. 'data' => [],
  1057. 'backgroundColor' => $colors[$i % count($colors)][0],
  1058. 'borderColor' => $colors[$i % count($colors)][1],
  1059. 'borderWidth' => 2,
  1060. 'fill' => false,
  1061. 'tension' => 0.1
  1062. ];
  1063. foreach ($time_periods as $period) {
  1064. $dataset['data'][] = $data[$period] ?? null;
  1065. }
  1066. $datasets[] = $dataset;
  1067. $i++;
  1068. }
  1069. ?>
  1070. <div class="chart-header">
  1071. <h2 class="chart-title">地区销售增长趋势</h2>
  1072. </div>
  1073. <canvas id="growthTrendsChart"></canvas>
  1074. <script>
  1075. // 地区销售增长趋势图
  1076. var growthTrendsCtx = document.getElementById('growthTrendsChart').getContext('2d');
  1077. var growthTrendsChart = new Chart(growthTrendsCtx, {
  1078. type: 'line',
  1079. data: {
  1080. labels: <?php echo json_encode($time_periods); ?>,
  1081. datasets: <?php echo json_encode($datasets); ?>
  1082. },
  1083. options: {
  1084. responsive: true,
  1085. scales: {
  1086. x: {
  1087. title: {
  1088. display: true,
  1089. text: '时间段'
  1090. }
  1091. },
  1092. y: {
  1093. title: {
  1094. display: true,
  1095. text: '销售额'
  1096. }
  1097. }
  1098. }
  1099. }
  1100. });
  1101. </script>
  1102. <?php
  1103. }
  1104. /**
  1105. * 渲染地区季节性分析图表
  1106. *
  1107. * @param array $seasonal_data 地区季节性分析数据
  1108. * @return void
  1109. */
  1110. function renderRegionSeasonalAnalysisChart($seasonal_data) {
  1111. $months = $seasonal_data['months'];
  1112. $data = $seasonal_data['data'];
  1113. $datasets = [];
  1114. $colors = [
  1115. ['rgba(255, 99, 132, 0.6)', 'rgba(255, 99, 132, 1)'],
  1116. ['rgba(54, 162, 235, 0.6)', 'rgba(54, 162, 235, 1)'],
  1117. ['rgba(255, 206, 86, 0.6)', 'rgba(255, 206, 86, 1)'],
  1118. ['rgba(75, 192, 192, 0.6)', 'rgba(75, 192, 192, 1)'],
  1119. ['rgba(153, 102, 255, 0.6)', 'rgba(153, 102, 255, 1)']
  1120. ];
  1121. $i = 0;
  1122. foreach ($data as $country => $values) {
  1123. $datasets[] = [
  1124. 'label' => $country,
  1125. 'data' => $values,
  1126. 'backgroundColor' => $colors[$i % count($colors)][0],
  1127. 'borderColor' => $colors[$i % count($colors)][1],
  1128. 'borderWidth' => 2,
  1129. 'fill' => false,
  1130. 'tension' => 0.1
  1131. ];
  1132. $i++;
  1133. }
  1134. ?>
  1135. <div class="chart-header">
  1136. <h2 class="chart-title">地区季节性销售分析</h2>
  1137. </div>
  1138. <canvas id="seasonalAnalysisChart"></canvas>
  1139. <script>
  1140. // 地区季节性销售分析图
  1141. var seasonalCtx = document.getElementById('seasonalAnalysisChart').getContext('2d');
  1142. var seasonalChart = new Chart(seasonalCtx, {
  1143. type: 'line',
  1144. data: {
  1145. labels: <?php echo json_encode($months); ?>,
  1146. datasets: <?php echo json_encode($datasets); ?>
  1147. },
  1148. options: {
  1149. responsive: true,
  1150. scales: {
  1151. x: {
  1152. title: {
  1153. display: true,
  1154. text: '月份'
  1155. }
  1156. },
  1157. y: {
  1158. title: {
  1159. display: true,
  1160. text: '销售额'
  1161. }
  1162. }
  1163. }
  1164. }
  1165. });
  1166. </script>
  1167. <?php
  1168. }
  1169. /**
  1170. * 渲染地区销售预测图表
  1171. *
  1172. * @param array $forecast_data 地区销售预测数据
  1173. * @return void
  1174. */
  1175. function renderRegionSalesForecastChart($forecast_data) {
  1176. $periods = $forecast_data['periods'];
  1177. $forecast = $forecast_data['forecast'];
  1178. $datasets = [];
  1179. $colors = [
  1180. ['rgba(255, 99, 132, 0.6)', 'rgba(255, 99, 132, 1)'],
  1181. ['rgba(54, 162, 235, 0.6)', 'rgba(54, 162, 235, 1)'],
  1182. ['rgba(255, 206, 86, 0.6)', 'rgba(255, 206, 86, 1)'],
  1183. ['rgba(75, 192, 192, 0.6)', 'rgba(75, 192, 192, 1)'],
  1184. ['rgba(153, 102, 255, 0.6)', 'rgba(153, 102, 255, 1)']
  1185. ];
  1186. $i = 0;
  1187. foreach ($forecast as $country => $data) {
  1188. $historical_data = [];
  1189. $forecast_data = [];
  1190. foreach ($periods as $period) {
  1191. if (isset($data[$period])) {
  1192. if ($data[$period]['is_forecast']) {
  1193. $historical_data[] = null;
  1194. $forecast_data[] = $data[$period]['value'];
  1195. } else {
  1196. $historical_data[] = $data[$period]['value'];
  1197. $forecast_data[] = null;
  1198. }
  1199. } else {
  1200. $historical_data[] = null;
  1201. $forecast_data[] = null;
  1202. }
  1203. }
  1204. $datasets[] = [
  1205. 'label' => $country . ' (历史)',
  1206. 'data' => $historical_data,
  1207. 'backgroundColor' => $colors[$i % count($colors)][0],
  1208. 'borderColor' => $colors[$i % count($colors)][1],
  1209. 'borderWidth' => 2,
  1210. 'fill' => false
  1211. ];
  1212. $datasets[] = [
  1213. 'label' => $country . ' (预测)',
  1214. 'data' => $forecast_data,
  1215. 'backgroundColor' => $colors[$i % count($colors)][0],
  1216. 'borderColor' => $colors[$i % count($colors)][1],
  1217. 'borderWidth' => 2,
  1218. 'borderDash' => [5, 5],
  1219. 'fill' => false
  1220. ];
  1221. $i++;
  1222. }
  1223. ?>
  1224. <div class="chart-header">
  1225. <h2 class="chart-title">地区销售预测 (未来6个月)</h2>
  1226. </div>
  1227. <canvas id="forecastChart"></canvas>
  1228. <script>
  1229. // 地区销售预测图
  1230. var forecastCtx = document.getElementById('forecastChart').getContext('2d');
  1231. var forecastChart = new Chart(forecastCtx, {
  1232. type: 'line',
  1233. data: {
  1234. labels: <?php echo json_encode($periods); ?>,
  1235. datasets: <?php echo json_encode($datasets); ?>
  1236. },
  1237. options: {
  1238. responsive: true,
  1239. scales: {
  1240. x: {
  1241. title: {
  1242. display: true,
  1243. text: '时间段'
  1244. }
  1245. },
  1246. y: {
  1247. title: {
  1248. display: true,
  1249. text: '销售额'
  1250. }
  1251. }
  1252. },
  1253. plugins: {
  1254. tooltip: {
  1255. callbacks: {
  1256. title: function(tooltipItems) {
  1257. return tooltipItems[0].label;
  1258. }
  1259. }
  1260. }
  1261. }
  1262. }
  1263. });
  1264. </script>
  1265. <?php
  1266. }
  1267. /**
  1268. * 渲染热门地区表格
  1269. *
  1270. * @param array $region_labels 地区标签
  1271. * @param array $region_order_counts 地区订单数量
  1272. * @param array $region_quantities 地区产品数量
  1273. * @param array $region_amounts 地区销售金额
  1274. * @return void
  1275. */
  1276. function renderTopRegionsTable($region_labels, $region_order_counts, $region_quantities, $region_amounts) {
  1277. ?>
  1278. <table class="data-table">
  1279. <thead>
  1280. <tr>
  1281. <th>排名</th>
  1282. <th>国家/地区</th>
  1283. <th>订单数</th>
  1284. <th>产品数量</th>
  1285. <th>销售金额</th>
  1286. <th>平均订单金额</th>
  1287. </tr>
  1288. </thead>
  1289. <tbody>
  1290. <?php for ($i = 0; $i < count($region_labels); $i++): ?>
  1291. <tr>
  1292. <td><?php echo $i + 1; ?></td>
  1293. <td><?php echo htmlspecialchars($region_labels[$i]); ?></td>
  1294. <td><?php echo number_format($region_order_counts[$i]); ?></td>
  1295. <td><?php echo number_format($region_quantities[$i]); ?></td>
  1296. <td>¥<?php echo number_format($region_amounts[$i], 2); ?></td>
  1297. <td>¥<?php echo number_format($region_order_counts[$i] > 0 ? $region_amounts[$i] / $region_order_counts[$i] : 0, 2); ?></td>
  1298. </tr>
  1299. <?php endfor; ?>
  1300. </tbody>
  1301. </table>
  1302. <?php
  1303. }