statistics_region.php 48 KB

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