statistics_customers.php 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472
  1. <?php
  2. /**
  3. * 客户统计分析模块
  4. *
  5. * 包含与客户相关的数据分析功能
  6. */
  7. require_once 'statistics_utils.php';
  8. /**
  9. * 获取客户类型分布
  10. *
  11. * @param mysqli $conn 数据库连接
  12. * @return mysqli_result 客户类型分布数据结果集
  13. */
  14. function getCustomerTypeDistribution($conn) {
  15. $sql = "SELECT
  16. ct.businessType,
  17. COUNT(c.id) as customer_count
  18. FROM customer c
  19. JOIN clienttype ct ON c.cs_type = ct.id
  20. GROUP BY c.cs_type";
  21. return $conn->query($sql);
  22. }
  23. /**
  24. * 获取成交阶段分布
  25. *
  26. * @param mysqli $conn 数据库连接
  27. * @return mysqli_result 成交阶段分布数据结果集
  28. */
  29. function getDealStageDistribution($conn) {
  30. $sql = "SELECT
  31. cs_deal,
  32. CASE
  33. WHEN cs_deal = 1 THEN '背景调查'
  34. WHEN cs_deal = 2 THEN '明确需求'
  35. WHEN cs_deal = 3 THEN '已成交'
  36. ELSE '其他'
  37. END as stage_name,
  38. COUNT(id) as customer_count
  39. FROM customer
  40. GROUP BY cs_deal";
  41. return $conn->query($sql);
  42. }
  43. /**
  44. * 获取客户增长趋势
  45. *
  46. * @param mysqli $conn 数据库连接
  47. * @param int $months 获取多少个月的数据,默认12个月
  48. * @return mysqli_result 客户增长趋势数据结果集
  49. */
  50. function getCustomerGrowthTrend($conn, $months = 12) {
  51. $sql = "SELECT
  52. DATE_FORMAT(cs_addtime, '%Y-%m') as month,
  53. COUNT(id) as new_customers
  54. FROM customer
  55. WHERE cs_addtime >= DATE_SUB(CURDATE(), INTERVAL ? MONTH)
  56. GROUP BY DATE_FORMAT(cs_addtime, '%Y-%m')
  57. ORDER BY month";
  58. $stmt = $conn->prepare($sql);
  59. $stmt->bind_param("i", $months);
  60. $stmt->execute();
  61. return $stmt->get_result();
  62. }
  63. /**
  64. * 获取新老客户订单分析
  65. *
  66. * @param mysqli $conn 数据库连接
  67. * @param string $start_date 开始日期
  68. * @param string $end_date 结束日期
  69. * @return array 新老客户订单分析数据
  70. */
  71. function getNewVsReturningCustomerOrders($conn, $start_date, $end_date) {
  72. // 获取选定日期范围内的订单
  73. $sql = "SELECT
  74. o.customer_id,
  75. COUNT(o.id) as order_count,
  76. SUM(o.total_amount) as total_amount,
  77. MIN(o.order_date) as first_order_date,
  78. MAX(c.cs_addtime) as customer_addtime
  79. FROM orders o
  80. JOIN customer c ON o.customer_id = c.id
  81. WHERE o.order_date BETWEEN ? AND ?
  82. GROUP BY o.customer_id";
  83. $stmt = $conn->prepare($sql);
  84. $stmt->bind_param("ss", $start_date, $end_date);
  85. $stmt->execute();
  86. $result = $stmt->get_result();
  87. $new_customers = 0;
  88. $returning_customers = 0;
  89. $new_customer_amount = 0;
  90. $returning_customer_amount = 0;
  91. while ($row = $result->fetch_assoc()) {
  92. // 查找之前是否有订单
  93. $prev_sql = "SELECT id FROM orders
  94. WHERE customer_id = ?
  95. AND order_date < ?
  96. LIMIT 1";
  97. $prev_stmt = $conn->prepare($prev_sql);
  98. $prev_stmt->bind_param("is", $row['customer_id'], $start_date);
  99. $prev_stmt->execute();
  100. $prev_result = $prev_stmt->get_result();
  101. if ($prev_result->num_rows > 0) {
  102. // 老客户
  103. $returning_customers++;
  104. $returning_customer_amount += $row['total_amount'];
  105. } else {
  106. // 新客户
  107. $new_customers++;
  108. $new_customer_amount += $row['total_amount'];
  109. }
  110. }
  111. return [
  112. 'new_customers' => $new_customers,
  113. 'returning_customers' => $returning_customers,
  114. 'new_customer_amount' => $new_customer_amount,
  115. 'returning_customer_amount' => $returning_customer_amount,
  116. 'total_customers' => $new_customers + $returning_customers,
  117. 'total_amount' => $new_customer_amount + $returning_customer_amount
  118. ];
  119. }
  120. /**
  121. * 渲染客户类型分布图
  122. *
  123. * @param array $type_labels 类型标签
  124. * @param array $type_data 类型数据
  125. * @return void
  126. */
  127. function renderCustomerTypeChart($type_labels, $type_data) {
  128. ?>
  129. <div class="chart-container">
  130. <div class="chart-header">
  131. <h2 class="chart-title">客户类型分布</h2>
  132. </div>
  133. <canvas id="customerTypeChart"></canvas>
  134. </div>
  135. <script>
  136. // 客户类型分布图
  137. var customerTypeCtx = document.getElementById('customerTypeChart').getContext('2d');
  138. var customerTypeChart = new Chart(customerTypeCtx, {
  139. type: 'doughnut',
  140. data: {
  141. labels: <?php echo json_encode($type_labels); ?>,
  142. datasets: [{
  143. data: <?php echo json_encode($type_data); ?>,
  144. backgroundColor: [
  145. 'rgba(54, 162, 235, 0.7)',
  146. 'rgba(255, 99, 132, 0.7)',
  147. 'rgba(255, 206, 86, 0.7)',
  148. 'rgba(75, 192, 192, 0.7)',
  149. 'rgba(153, 102, 255, 0.7)'
  150. ],
  151. borderWidth: 1
  152. }]
  153. },
  154. options: {
  155. responsive: true,
  156. plugins: {
  157. legend: {
  158. position: 'right',
  159. }
  160. }
  161. }
  162. });
  163. </script>
  164. <?php
  165. }
  166. /**
  167. * 渲染成交阶段分布图
  168. *
  169. * @param array $stage_labels 阶段标签
  170. * @param array $stage_data 阶段数据
  171. * @return void
  172. */
  173. function renderDealStageChart($stage_labels, $stage_data) {
  174. ?>
  175. <div class="chart-container">
  176. <div class="chart-header">
  177. <h2 class="chart-title">成交阶段分布</h2>
  178. </div>
  179. <canvas id="dealStageChart"></canvas>
  180. </div>
  181. <script>
  182. // 成交阶段分布图
  183. var dealStageCtx = document.getElementById('dealStageChart').getContext('2d');
  184. var dealStageChart = new Chart(dealStageCtx, {
  185. type: 'bar',
  186. data: {
  187. labels: <?php echo json_encode($stage_labels); ?>,
  188. datasets: [{
  189. label: '客户数量',
  190. data: <?php echo json_encode($stage_data); ?>,
  191. backgroundColor: [
  192. 'rgba(255, 206, 86, 0.7)',
  193. 'rgba(54, 162, 235, 0.7)',
  194. 'rgba(255, 99, 132, 0.7)'
  195. ],
  196. borderWidth: 1
  197. }]
  198. },
  199. options: {
  200. responsive: true,
  201. scales: {
  202. y: {
  203. beginAtZero: true,
  204. title: {
  205. display: true,
  206. text: '客户数量'
  207. }
  208. }
  209. }
  210. }
  211. });
  212. </script>
  213. <?php
  214. }
  215. /**
  216. * 渲染客户增长趋势图
  217. *
  218. * @param array $growth_labels 增长标签
  219. * @param array $growth_data 增长数据
  220. * @return void
  221. */
  222. function renderCustomerGrowthChart($growth_labels, $growth_data) {
  223. ?>
  224. <div class="chart-container">
  225. <div class="chart-header">
  226. <h2 class="chart-title">客户增长趋势</h2>
  227. </div>
  228. <canvas id="customerGrowthChart"></canvas>
  229. </div>
  230. <script>
  231. // 客户增长趋势图
  232. var customerGrowthCtx = document.getElementById('customerGrowthChart').getContext('2d');
  233. var customerGrowthChart = new Chart(customerGrowthCtx, {
  234. type: 'line',
  235. data: {
  236. labels: <?php echo json_encode($growth_labels); ?>,
  237. datasets: [{
  238. label: '新增客户',
  239. data: <?php echo json_encode($growth_data); ?>,
  240. backgroundColor: 'rgba(75, 192, 192, 0.2)',
  241. borderColor: 'rgba(75, 192, 192, 1)',
  242. borderWidth: 2,
  243. tension: 0.1
  244. }]
  245. },
  246. options: {
  247. responsive: true,
  248. scales: {
  249. y: {
  250. beginAtZero: true,
  251. title: {
  252. display: true,
  253. text: '客户数量'
  254. }
  255. }
  256. }
  257. }
  258. });
  259. </script>
  260. <?php
  261. }
  262. /**
  263. * 渲染新老客户分析图
  264. *
  265. * @param array $new_vs_returning 新老客户数据
  266. * @return void
  267. */
  268. function renderNewVsReturningCustomersChart($new_vs_returning) {
  269. ?>
  270. <div class="chart-container">
  271. <div class="chart-header">
  272. <h2 class="chart-title">新老客户分析</h2>
  273. </div>
  274. <style>
  275. .pie-charts-container {
  276. display: flex;
  277. flex-direction: row;
  278. justify-content: space-between;
  279. margin-bottom: 20px;
  280. }
  281. .pie-chart-wrapper {
  282. flex: 0 0 48%;
  283. max-width: 48%;
  284. }
  285. .customer-stats-summary {
  286. margin-top: 20px;
  287. padding: 15px;
  288. background-color: #f9f9f9;
  289. border-radius: 5px;
  290. }
  291. .stats-row {
  292. display: flex;
  293. margin-bottom: 15px;
  294. }
  295. .stat-item {
  296. flex: 1;
  297. padding: 0 10px;
  298. }
  299. .stat-label {
  300. display: block;
  301. font-weight: bold;
  302. margin-bottom: 5px;
  303. color: #555;
  304. }
  305. .stat-value {
  306. font-size: 16px;
  307. color: #333;
  308. }
  309. </style>
  310. <div class="pie-charts-container">
  311. <div class="pie-chart-wrapper">
  312. <h3 style="text-align: center; margin-bottom: 15px;">客户数量分布</h3>
  313. <canvas id="newVsReturningCustomersChart"></canvas>
  314. </div>
  315. <div class="pie-chart-wrapper">
  316. <h3 style="text-align: center; margin-bottom: 15px;">销售额分布</h3>
  317. <canvas id="newVsReturningAmountChart"></canvas>
  318. </div>
  319. </div>
  320. <div class="customer-stats-summary">
  321. <div class="stats-row">
  322. <div class="stat-item">
  323. <span class="stat-label">总客户数:</span>
  324. <span class="stat-value"><?php echo number_format($new_vs_returning['total_customers']); ?></span>
  325. </div>
  326. <div class="stat-item">
  327. <span class="stat-label">新客户:</span>
  328. <span class="stat-value"><?php echo number_format($new_vs_returning['new_customers']); ?>
  329. (<?php echo ($new_vs_returning['total_customers'] > 0) ? number_format(($new_vs_returning['new_customers'] / $new_vs_returning['total_customers']) * 100, 1) : '0'; ?>%)</span>
  330. </div>
  331. <div class="stat-item">
  332. <span class="stat-label">老客户:</span>
  333. <span class="stat-value"><?php echo number_format($new_vs_returning['returning_customers']); ?>
  334. (<?php echo ($new_vs_returning['total_customers'] > 0) ? number_format(($new_vs_returning['returning_customers'] / $new_vs_returning['total_customers']) * 100, 1) : '0'; ?>%)</span>
  335. </div>
  336. </div>
  337. <div class="stats-row">
  338. <div class="stat-item">
  339. <span class="stat-label">总销售额:</span>
  340. <span class="stat-value">¥<?php echo number_format($new_vs_returning['total_amount'], 2); ?></span>
  341. </div>
  342. <div class="stat-item">
  343. <span class="stat-label">新客户销售额:</span>
  344. <span class="stat-value">¥<?php echo number_format($new_vs_returning['new_customer_amount'], 2); ?>
  345. (<?php echo ($new_vs_returning['total_amount'] > 0) ? number_format(($new_vs_returning['new_customer_amount'] / $new_vs_returning['total_amount']) * 100, 1) : '0'; ?>%)</span>
  346. </div>
  347. <div class="stat-item">
  348. <span class="stat-label">老客户销售额:</span>
  349. <span class="stat-value">¥<?php echo number_format($new_vs_returning['returning_customer_amount'], 2); ?>
  350. (<?php echo ($new_vs_returning['total_amount'] > 0) ? number_format(($new_vs_returning['returning_customer_amount'] / $new_vs_returning['total_amount']) * 100, 1) : '0'; ?>%)</span>
  351. </div>
  352. </div>
  353. </div>
  354. </div>
  355. <script>
  356. // 新老客户数量图
  357. var newVsReturningCtx = document.getElementById('newVsReturningCustomersChart').getContext('2d');
  358. var newVsReturningChart = new Chart(newVsReturningCtx, {
  359. type: 'pie',
  360. data: {
  361. labels: ['新客户', '老客户'],
  362. datasets: [{
  363. data: [
  364. <?php echo $new_vs_returning['new_customers']; ?>,
  365. <?php echo $new_vs_returning['returning_customers']; ?>
  366. ],
  367. backgroundColor: [
  368. 'rgba(54, 162, 235, 0.7)',
  369. 'rgba(255, 99, 132, 0.7)'
  370. ],
  371. borderWidth: 1
  372. }]
  373. },
  374. options: {
  375. responsive: true,
  376. maintainAspectRatio: true,
  377. plugins: {
  378. legend: {
  379. position: 'bottom',
  380. }
  381. }
  382. }
  383. });
  384. // 新老客户销售额图
  385. var amountCtx = document.getElementById('newVsReturningAmountChart').getContext('2d');
  386. var amountChart = new Chart(amountCtx, {
  387. type: 'pie',
  388. data: {
  389. labels: ['新客户销售额', '老客户销售额'],
  390. datasets: [{
  391. data: [
  392. <?php echo $new_vs_returning['new_customer_amount']; ?>,
  393. <?php echo $new_vs_returning['returning_customer_amount']; ?>
  394. ],
  395. backgroundColor: [
  396. 'rgba(54, 162, 235, 0.7)',
  397. 'rgba(255, 99, 132, 0.7)'
  398. ],
  399. borderWidth: 1
  400. }]
  401. },
  402. options: {
  403. responsive: true,
  404. maintainAspectRatio: true,
  405. plugins: {
  406. legend: {
  407. position: 'bottom',
  408. }
  409. }
  410. }
  411. });
  412. </script>
  413. <?php
  414. }
  415. /**
  416. * 获取客户总数
  417. *
  418. * @param mysqli $conn 数据库连接
  419. * @param array|int $employee_filter 业务员ID或ID数组,用于过滤数据 (可选)
  420. * @return int 客户总数
  421. */
  422. function getTotalCustomers($conn, $employee_filter = null) {
  423. $sql = "SELECT COUNT(id) as total FROM customer";
  424. // 如果有业务员过滤
  425. if ($employee_filter !== null) {
  426. if (is_array($employee_filter) && !empty($employee_filter)) {
  427. // 处理数组形式的业务员ID列表
  428. $emp_ids = array();
  429. foreach ($employee_filter as $emp_id) {
  430. if (is_numeric($emp_id)) {
  431. $emp_ids[] = intval($emp_id);
  432. }
  433. }
  434. if (!empty($emp_ids)) {
  435. $emp_ids_str = implode(',', $emp_ids);
  436. $sql .= " WHERE cs_belong IN ($emp_ids_str)";
  437. }
  438. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  439. // 处理单个业务员ID
  440. $employee_filter = intval($employee_filter);
  441. $sql .= " WHERE cs_belong = $employee_filter";
  442. }
  443. }
  444. $result = $conn->query($sql);
  445. $row = $result->fetch_assoc();
  446. return $row['total'];
  447. }
  448. /**
  449. * 获取指定时间段内新增客户数
  450. *
  451. * @param mysqli $conn 数据库连接
  452. * @param string $start_date 开始日期
  453. * @param string $end_date 结束日期
  454. * @param array|int $employee_filter 业务员ID或ID数组,用于过滤数据 (可选)
  455. * @return int 新增客户数
  456. */
  457. function getNewCustomers($conn, $start_date, $end_date, $employee_filter = null) {
  458. // 使用 mysqli_real_escape_string 防止 SQL 注入
  459. $start_date = $conn->real_escape_string($start_date);
  460. $end_date = $conn->real_escape_string($end_date);
  461. $sql = "SELECT COUNT(id) as new_count
  462. FROM customer
  463. WHERE cs_addtime BETWEEN '$start_date' AND '$end_date'";
  464. // 如果有业务员过滤
  465. if ($employee_filter !== null) {
  466. if (is_array($employee_filter) && !empty($employee_filter)) {
  467. // 处理数组形式的业务员ID列表
  468. $emp_ids = array();
  469. foreach ($employee_filter as $emp_id) {
  470. if (is_numeric($emp_id)) {
  471. $emp_ids[] = intval($emp_id);
  472. }
  473. }
  474. if (!empty($emp_ids)) {
  475. $emp_ids_str = implode(',', $emp_ids);
  476. $sql .= " AND cs_belong IN ($emp_ids_str)";
  477. }
  478. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  479. // 处理单个业务员ID
  480. $employee_filter = intval($employee_filter);
  481. $sql .= " AND cs_belong = $employee_filter";
  482. }
  483. }
  484. $result = $conn->query($sql);
  485. $row = $result->fetch_assoc();
  486. return $row['new_count'];
  487. }
  488. /**
  489. * 计算平均客户价值(客户平均订单金额)
  490. *
  491. * @param mysqli $conn 数据库连接
  492. * @param string $start_date 开始日期
  493. * @param string $end_date 结束日期
  494. * @param array|int $employee_filter 业务员ID或ID数组,用于过滤数据 (可选)
  495. * @return float 平均客户价值
  496. */
  497. function getAverageCustomerValue($conn, $start_date, $end_date, $employee_filter = null) {
  498. // 使用 mysqli_real_escape_string 防止 SQL 注入
  499. $start_date = $conn->real_escape_string($start_date);
  500. $end_date = $conn->real_escape_string($end_date);
  501. $sql = "SELECT AVG(customer_value) as avg_value FROM (
  502. SELECT
  503. o.customer_id,
  504. SUM(o.total_amount) as customer_value
  505. FROM orders o
  506. JOIN customer c ON o.customer_id = c.id
  507. WHERE o.order_date BETWEEN '$start_date' AND '$end_date'";
  508. // 如果有业务员过滤
  509. if ($employee_filter !== null) {
  510. if (is_array($employee_filter) && !empty($employee_filter)) {
  511. // 处理数组形式的业务员ID列表
  512. $emp_ids = array();
  513. foreach ($employee_filter as $emp_id) {
  514. if (is_numeric($emp_id)) {
  515. $emp_ids[] = intval($emp_id);
  516. }
  517. }
  518. if (!empty($emp_ids)) {
  519. $emp_ids_str = implode(',', $emp_ids);
  520. $sql .= " AND c.cs_belong IN ($emp_ids_str)";
  521. }
  522. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  523. // 处理单个业务员ID
  524. $employee_filter = intval($employee_filter);
  525. $sql .= " AND c.cs_belong = $employee_filter";
  526. }
  527. }
  528. $sql .= " GROUP BY o.customer_id) as customer_values";
  529. $result = $conn->query($sql);
  530. $row = $result->fetch_assoc();
  531. return $row['avg_value'] ? $row['avg_value'] : 0;
  532. }
  533. /**
  534. * 计算客户留存率
  535. *
  536. * @param mysqli $conn 数据库连接
  537. * @param string $start_date 开始日期
  538. * @param string $end_date 结束日期
  539. * @param array|int $employee_filter 业务员ID或ID数组,用于过滤数据 (可选)
  540. * @return array 客户留存率数据
  541. */
  542. function getCustomerRetentionRate($conn, $start_date, $end_date, $employee_filter = null) {
  543. // 使用 mysqli_real_escape_string 防止 SQL 注入
  544. $start_date = $conn->real_escape_string($start_date);
  545. $end_date = $conn->real_escape_string($end_date);
  546. // 获取之前时间段的客户
  547. $previous_start = date('Y-m-d', strtotime('-1 year', strtotime($start_date)));
  548. $previous_end = date('Y-m-d', strtotime('-1 day', strtotime($start_date)));
  549. // 之前时间段的客户ID
  550. $prev_sql = "SELECT DISTINCT o.customer_id
  551. FROM orders o
  552. JOIN customer c ON o.customer_id = c.id
  553. WHERE o.order_date BETWEEN '$previous_start' AND '$previous_end'";
  554. // 如果有业务员过滤
  555. if ($employee_filter !== null) {
  556. if (is_array($employee_filter) && !empty($employee_filter)) {
  557. // 处理数组形式的业务员ID列表
  558. $emp_ids = array();
  559. foreach ($employee_filter as $emp_id) {
  560. if (is_numeric($emp_id)) {
  561. $emp_ids[] = intval($emp_id);
  562. }
  563. }
  564. if (!empty($emp_ids)) {
  565. $emp_ids_str = implode(',', $emp_ids);
  566. $prev_sql .= " AND c.cs_belong IN ($emp_ids_str)";
  567. }
  568. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  569. // 处理单个业务员ID
  570. $employee_filter = intval($employee_filter);
  571. $prev_sql .= " AND c.cs_belong = $employee_filter";
  572. }
  573. }
  574. $prev_result = $conn->query($prev_sql);
  575. $previous_customers = [];
  576. while ($row = $prev_result->fetch_assoc()) {
  577. $previous_customers[] = $row['customer_id'];
  578. }
  579. $previous_count = count($previous_customers);
  580. // 如果没有之前的客户,返回0
  581. if ($previous_count == 0) {
  582. return [
  583. 'retained_count' => 0,
  584. 'total_previous' => 0,
  585. 'retention_rate' => 0
  586. ];
  587. }
  588. // 查询当前时间段内,之前客户中再次购买的客户数
  589. $current_sql = "SELECT COUNT(DISTINCT o.customer_id) as retained_count
  590. FROM orders o
  591. JOIN customer c ON o.customer_id = c.id
  592. WHERE o.order_date BETWEEN '$start_date' AND '$end_date'
  593. AND o.customer_id IN (" . implode(',', $previous_customers) . ")";
  594. // 如果有业务员过滤
  595. if ($employee_filter !== null) {
  596. if (is_array($employee_filter) && !empty($employee_filter)) {
  597. // 处理数组形式的业务员ID列表
  598. $emp_ids = array();
  599. foreach ($employee_filter as $emp_id) {
  600. if (is_numeric($emp_id)) {
  601. $emp_ids[] = intval($emp_id);
  602. }
  603. }
  604. if (!empty($emp_ids)) {
  605. $emp_ids_str = implode(',', $emp_ids);
  606. $current_sql .= " AND c.cs_belong IN ($emp_ids_str)";
  607. }
  608. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  609. // 处理单个业务员ID
  610. $employee_filter = intval($employee_filter);
  611. $current_sql .= " AND c.cs_belong = $employee_filter";
  612. }
  613. }
  614. $current_result = $conn->query($current_sql);
  615. $row = $current_result->fetch_assoc();
  616. $retained_count = $row['retained_count'];
  617. $retention_rate = ($retained_count / $previous_count) * 100;
  618. return [
  619. 'retained_count' => $retained_count,
  620. 'total_previous' => $previous_count,
  621. 'retention_rate' => $retention_rate
  622. ];
  623. }
  624. /**
  625. * 计算下单转换率
  626. *
  627. * @param mysqli $conn 数据库连接
  628. * @param string $start_date 开始日期
  629. * @param string $end_date 结束日期
  630. * @param array|int $employee_filter 业务员ID或ID数组,用于过滤数据 (可选)
  631. * @return array 下单转换率数据
  632. */
  633. function getOrderConversionRate($conn, $start_date, $end_date, $employee_filter = null) {
  634. // 使用 mysqli_real_escape_string 防止 SQL 注入
  635. $start_date = $conn->real_escape_string($start_date);
  636. $end_date = $conn->real_escape_string($end_date);
  637. // 获取指定时间段内总客户数
  638. $total_sql = "SELECT COUNT(DISTINCT id) as total_count FROM customer WHERE cs_addtime <= '$end_date'";
  639. // 如果有业务员过滤
  640. if ($employee_filter !== null) {
  641. if (is_array($employee_filter) && !empty($employee_filter)) {
  642. // 处理数组形式的业务员ID列表
  643. $emp_ids = array();
  644. foreach ($employee_filter as $emp_id) {
  645. if (is_numeric($emp_id)) {
  646. $emp_ids[] = intval($emp_id);
  647. }
  648. }
  649. if (!empty($emp_ids)) {
  650. $emp_ids_str = implode(',', $emp_ids);
  651. $total_sql .= " AND cs_belong IN ($emp_ids_str)";
  652. }
  653. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  654. // 处理单个业务员ID
  655. $employee_filter = intval($employee_filter);
  656. $total_sql .= " AND cs_belong = $employee_filter";
  657. }
  658. }
  659. $total_result = $conn->query($total_sql);
  660. $total_row = $total_result->fetch_assoc();
  661. $total_customers = $total_row['total_count'];
  662. // 获取有订单的客户数
  663. $order_sql = "SELECT COUNT(DISTINCT o.customer_id) as order_count
  664. FROM orders o
  665. JOIN customer c ON o.customer_id = c.id
  666. WHERE o.order_date BETWEEN '$start_date' AND '$end_date'";
  667. // 如果有业务员过滤
  668. if ($employee_filter !== null) {
  669. if (is_array($employee_filter) && !empty($employee_filter)) {
  670. // 处理数组形式的业务员ID列表
  671. $emp_ids = array();
  672. foreach ($employee_filter as $emp_id) {
  673. if (is_numeric($emp_id)) {
  674. $emp_ids[] = intval($emp_id);
  675. }
  676. }
  677. if (!empty($emp_ids)) {
  678. $emp_ids_str = implode(',', $emp_ids);
  679. $order_sql .= " AND c.cs_belong IN ($emp_ids_str)";
  680. }
  681. } else if (is_numeric($employee_filter) && $employee_filter > 0) {
  682. // 处理单个业务员ID
  683. $employee_filter = intval($employee_filter);
  684. $order_sql .= " AND c.cs_belong = $employee_filter";
  685. }
  686. }
  687. $order_result = $conn->query($order_sql);
  688. $order_row = $order_result->fetch_assoc();
  689. $customers_with_orders = $order_row['order_count'];
  690. // 计算转换率
  691. $conversion_rate = ($total_customers > 0) ? ($customers_with_orders / $total_customers) * 100 : 0;
  692. return [
  693. 'total_customers' => $total_customers,
  694. 'customers_with_orders' => $customers_with_orders,
  695. 'conversion_rate' => $conversion_rate
  696. ];
  697. }
  698. /**
  699. * 渲染关键指标仪表板
  700. *
  701. * @param array $kpi_data 关键指标数据
  702. * @return void
  703. */
  704. function renderKeyMetricsCard($kpi_data) {
  705. ?>
  706. <div class="stats-card-container">
  707. <div class="stats-card">
  708. <div class="stats-card-header">
  709. <h3>客户总数</h3>
  710. </div>
  711. <div class="stats-card-body">
  712. <div class="stats-card-value"><?php echo number_format($kpi_data['total_customers']); ?></div>
  713. </div>
  714. </div>
  715. <div class="stats-card">
  716. <div class="stats-card-header">
  717. <h3>新增客户</h3>
  718. </div>
  719. <div class="stats-card-body">
  720. <div class="stats-card-value"><?php echo number_format($kpi_data['new_customers']); ?></div>
  721. </div>
  722. </div>
  723. <div class="stats-card">
  724. <div class="stats-card-header">
  725. <h3>平均客户价值</h3>
  726. </div>
  727. <div class="stats-card-body">
  728. <div class="stats-card-value">¥<?php echo number_format($kpi_data['avg_customer_value'], 2); ?></div>
  729. </div>
  730. </div>
  731. <div class="stats-card">
  732. <div class="stats-card-header">
  733. <h3>客户留存率</h3>
  734. </div>
  735. <div class="stats-card-body">
  736. <div class="stats-card-value"><?php echo number_format($kpi_data['retention_rate'], 1); ?>%</div>
  737. <div class="stats-card-subtitle"><?php echo number_format($kpi_data['retained_count']); ?> / <?php echo number_format($kpi_data['total_previous']); ?></div>
  738. </div>
  739. </div>
  740. <div class="stats-card">
  741. <div class="stats-card-header">
  742. <h3>下单转换率</h3>
  743. </div>
  744. <div class="stats-card-body">
  745. <div class="stats-card-value"><?php echo number_format($kpi_data['conversion_rate'], 1); ?>%</div>
  746. <div class="stats-card-subtitle"><?php echo number_format($kpi_data['customers_with_orders']); ?> / <?php echo number_format($kpi_data['total_customers']); ?></div>
  747. </div>
  748. </div>
  749. </div>
  750. <?php
  751. }
  752. /**
  753. * 获取客户价值分布数据
  754. *
  755. * @param mysqli $conn 数据库连接
  756. * @param string $start_date 开始日期
  757. * @param string $end_date 结束日期
  758. * @return array 客户价值分布数据
  759. */
  760. function getCustomerValueDistribution($conn, $start_date, $end_date) {
  761. $sql = "SELECT
  762. value_segment,
  763. COUNT(customer_id) as customer_count,
  764. SUM(total_amount) as total_amount
  765. FROM (
  766. SELECT
  767. o.customer_id,
  768. SUM(o.total_amount) as total_amount,
  769. CASE
  770. WHEN SUM(o.total_amount) > 100000 THEN '高价值客户(>10万)'
  771. WHEN SUM(o.total_amount) > 50000 THEN '中高价值客户(5-10万)'
  772. WHEN SUM(o.total_amount) > 10000 THEN '中价值客户(1-5万)'
  773. WHEN SUM(o.total_amount) > 5000 THEN '低价值客户(5千-1万)'
  774. ELSE '微价值客户(<5千)'
  775. END as value_segment
  776. FROM orders o
  777. WHERE o.order_date BETWEEN ? AND ?
  778. GROUP BY o.customer_id
  779. ) as customer_value
  780. GROUP BY value_segment
  781. ORDER BY
  782. CASE value_segment
  783. WHEN '高价值客户(>10万)' THEN 1
  784. WHEN '中高价值客户(5-10万)' THEN 2
  785. WHEN '中价值客户(1-5万)' THEN 3
  786. WHEN '低价值客户(5千-1万)' THEN 4
  787. ELSE 5
  788. END";
  789. $stmt = $conn->prepare($sql);
  790. $stmt->bind_param("ss", $start_date, $end_date);
  791. $stmt->execute();
  792. $result = $stmt->get_result();
  793. $value_segments = [];
  794. $customer_counts = [];
  795. $total_amounts = [];
  796. $total_customers = 0;
  797. while ($row = $result->fetch_assoc()) {
  798. $value_segments[] = $row['value_segment'];
  799. $customer_counts[] = $row['customer_count'];
  800. $total_amounts[] = $row['total_amount'];
  801. $total_customers += $row['customer_count'];
  802. }
  803. return [
  804. 'segments' => $value_segments,
  805. 'counts' => $customer_counts,
  806. 'amounts' => $total_amounts,
  807. 'total_customers' => $total_customers
  808. ];
  809. }
  810. /**
  811. * 获取客户活跃度分析数据
  812. *
  813. * @param mysqli $conn 数据库连接
  814. * @param string $end_date 截止日期
  815. * @return array 客户活跃度分析数据
  816. */
  817. function getCustomerActivityAnalysis($conn, $end_date) {
  818. $sql = "SELECT
  819. activity_level,
  820. COUNT(*) as customer_count
  821. FROM (
  822. SELECT
  823. o.customer_id,
  824. CASE
  825. WHEN DATEDIFF(?, MAX(o.order_date)) <= 30 THEN '活跃客户(30天内)'
  826. WHEN DATEDIFF(?, MAX(o.order_date)) <= 90 THEN '一般活跃(90天内)'
  827. WHEN DATEDIFF(?, MAX(o.order_date)) <= 180 THEN '低活跃(180天内)'
  828. WHEN DATEDIFF(?, MAX(o.order_date)) <= 365 THEN '沉睡客户(1年内)'
  829. ELSE '流失客户(超过1年)'
  830. END as activity_level
  831. FROM orders o
  832. GROUP BY o.customer_id
  833. ) as customer_activity
  834. GROUP BY activity_level
  835. ORDER BY
  836. CASE activity_level
  837. WHEN '活跃客户(30天内)' THEN 1
  838. WHEN '一般活跃(90天内)' THEN 2
  839. WHEN '低活跃(180天内)' THEN 3
  840. WHEN '沉睡客户(1年内)' THEN 4
  841. ELSE 5
  842. END";
  843. $stmt = $conn->prepare($sql);
  844. $end_date_formatted = date('Y-m-d', strtotime($end_date));
  845. $stmt->bind_param("ssss", $end_date_formatted, $end_date_formatted, $end_date_formatted, $end_date_formatted);
  846. $stmt->execute();
  847. $result = $stmt->get_result();
  848. $activity_levels = [];
  849. $customer_counts = [];
  850. while ($row = $result->fetch_assoc()) {
  851. $activity_levels[] = $row['activity_level'];
  852. $customer_counts[] = $row['customer_count'];
  853. }
  854. return [
  855. 'levels' => $activity_levels,
  856. 'counts' => $customer_counts
  857. ];
  858. }
  859. /**
  860. * 获取客户流失风险分析数据
  861. *
  862. * @param mysqli $conn 数据库连接
  863. * @param string $end_date 截止日期
  864. * @return array 客户流失风险分析数据
  865. */
  866. function getCustomerChurnRiskAnalysis($conn, $end_date) {
  867. $sql = "SELECT
  868. risk_level,
  869. COUNT(*) as customer_count
  870. FROM (
  871. SELECT
  872. c.id,
  873. CASE
  874. WHEN last_order_date IS NULL THEN '从未购买'
  875. WHEN DATEDIFF(?, last_order_date) <= 90 THEN '低风险(90天内)'
  876. WHEN DATEDIFF(?, last_order_date) <= 180 THEN '中风险(90-180天)'
  877. WHEN DATEDIFF(?, last_order_date) <= 365 THEN '高风险(180-365天)'
  878. ELSE '极高风险(超过1年)'
  879. END as risk_level
  880. FROM customer c
  881. LEFT JOIN (
  882. SELECT customer_id, MAX(order_date) as last_order_date
  883. FROM orders
  884. GROUP BY customer_id
  885. ) o ON c.id = o.customer_id
  886. ) as customer_risk
  887. GROUP BY risk_level
  888. ORDER BY
  889. CASE risk_level
  890. WHEN '低风险(90天内)' THEN 1
  891. WHEN '中风险(90-180天)' THEN 2
  892. WHEN '高风险(180-365天)' THEN 3
  893. WHEN '极高风险(超过1年)' THEN 4
  894. WHEN '从未购买' THEN 5
  895. END";
  896. $stmt = $conn->prepare($sql);
  897. $end_date_formatted = date('Y-m-d', strtotime($end_date));
  898. $stmt->bind_param("sss", $end_date_formatted, $end_date_formatted, $end_date_formatted);
  899. $stmt->execute();
  900. $result = $stmt->get_result();
  901. $risk_levels = [];
  902. $customer_counts = [];
  903. while ($row = $result->fetch_assoc()) {
  904. $risk_levels[] = $row['risk_level'];
  905. $customer_counts[] = $row['customer_count'];
  906. }
  907. return [
  908. 'levels' => $risk_levels,
  909. 'counts' => $customer_counts
  910. ];
  911. }
  912. /**
  913. * 获取客户来源分析数据
  914. *
  915. * @param mysqli $conn 数据库连接
  916. * @return array 客户来源分析数据
  917. */
  918. function getCustomerSourceAnalysis($conn) {
  919. // 假设cs_from字段代表客户来源,需要根据实际情况调整SQL
  920. $sql = "SELECT
  921. source,
  922. COUNT(*) as customer_count
  923. FROM (
  924. SELECT
  925. id,
  926. CASE
  927. WHEN cs_from = 1 THEN '网站注册'
  928. WHEN cs_from = 2 THEN '销售开发'
  929. WHEN cs_from = 3 THEN '广告引流'
  930. WHEN cs_from = 4 THEN '展会获取'
  931. WHEN cs_from = 5 THEN '客户推荐'
  932. ELSE '其他来源'
  933. END as source
  934. FROM customer
  935. ) as customer_source
  936. GROUP BY source
  937. ORDER BY customer_count DESC";
  938. $result = $conn->query($sql);
  939. $sources = [];
  940. $counts = [];
  941. while ($row = $result->fetch_assoc()) {
  942. $sources[] = $row['source'];
  943. $counts[] = $row['customer_count'];
  944. }
  945. return [
  946. 'sources' => $sources,
  947. 'counts' => $counts
  948. ];
  949. }
  950. /**
  951. * 渲染客户价值分布图表
  952. *
  953. * @param array $value_data 客户价值分布数据
  954. * @return void
  955. */
  956. function renderCustomerValueCharts($value_data) {
  957. ?>
  958. <div class="chart-container">
  959. <div class="chart-header">
  960. <h2 class="chart-title">客户价值分布</h2>
  961. </div>
  962. <div class="chart-row" style="display: flex; width: 100%; margin: 0 -10px;">
  963. <div class="chart-column" style="flex: 0 0 50%; max-width: 50%; padding: 0 10px; box-sizing: border-box;">
  964. <h3 style="text-align: center; margin-bottom: 15px;">客户价值分布(柱状图)</h3>
  965. <canvas id="customerValueBarChart"></canvas>
  966. </div>
  967. <div class="chart-column" style="flex: 0 0 50%; max-width: 50%; padding: 0 10px; box-sizing: border-box;">
  968. <h3 style="text-align: center; margin-bottom: 15px;">客户价值分布(饼图)</h3>
  969. <canvas id="customerValuePieChart"></canvas>
  970. </div>
  971. </div>
  972. <div class="customer-stats-summary">
  973. <div class="stats-row">
  974. <?php foreach ($value_data['segments'] as $index => $segment): ?>
  975. <div class="stat-item">
  976. <span class="stat-label"><?php echo $segment; ?>:</span>
  977. <span class="stat-value"><?php echo number_format($value_data['counts'][$index]); ?>
  978. (<?php echo ($value_data['total_customers'] > 0) ?
  979. number_format(($value_data['counts'][$index] / $value_data['total_customers']) * 100, 1) : '0'; ?>%)</span>
  980. <span class="stat-sub-value">¥<?php echo number_format($value_data['amounts'][$index], 2); ?></span>
  981. </div>
  982. <?php endforeach; ?>
  983. </div>
  984. </div>
  985. </div>
  986. <script>
  987. // 客户价值分布柱状图
  988. var valueBarCtx = document.getElementById('customerValueBarChart').getContext('2d');
  989. var valueBarChart = new Chart(valueBarCtx, {
  990. type: 'bar',
  991. data: {
  992. labels: <?php echo json_encode($value_data['segments']); ?>,
  993. datasets: [{
  994. label: '客户数量',
  995. data: <?php echo json_encode($value_data['counts']); ?>,
  996. backgroundColor: [
  997. 'rgba(54, 162, 235, 0.7)',
  998. 'rgba(75, 192, 192, 0.7)',
  999. 'rgba(255, 206, 86, 0.7)',
  1000. 'rgba(255, 99, 132, 0.7)',
  1001. 'rgba(153, 102, 255, 0.7)'
  1002. ],
  1003. borderWidth: 1
  1004. }]
  1005. },
  1006. options: {
  1007. responsive: true,
  1008. scales: {
  1009. y: {
  1010. beginAtZero: true,
  1011. title: {
  1012. display: true,
  1013. text: '客户数量'
  1014. }
  1015. }
  1016. }
  1017. }
  1018. });
  1019. // 客户价值分布饼图
  1020. var valuePieCtx = document.getElementById('customerValuePieChart').getContext('2d');
  1021. var valuePieChart = new Chart(valuePieCtx, {
  1022. type: 'pie',
  1023. data: {
  1024. labels: <?php echo json_encode($value_data['segments']); ?>,
  1025. datasets: [{
  1026. data: <?php echo json_encode($value_data['counts']); ?>,
  1027. backgroundColor: [
  1028. 'rgba(54, 162, 235, 0.7)',
  1029. 'rgba(75, 192, 192, 0.7)',
  1030. 'rgba(255, 206, 86, 0.7)',
  1031. 'rgba(255, 99, 132, 0.7)',
  1032. 'rgba(153, 102, 255, 0.7)'
  1033. ],
  1034. borderWidth: 1
  1035. }]
  1036. },
  1037. options: {
  1038. responsive: true
  1039. }
  1040. });
  1041. </script>
  1042. <?php
  1043. }
  1044. /**
  1045. * 渲染客户活跃度分析图表
  1046. *
  1047. * @param array $activity_data 客户活跃度数据
  1048. * @return void
  1049. */
  1050. function renderCustomerActivityChart($activity_data) {
  1051. ?>
  1052. <div class="chart-container">
  1053. <div class="chart-header">
  1054. <h2 class="chart-title">客户活跃度分析</h2>
  1055. </div>
  1056. <canvas id="customerActivityChart"></canvas>
  1057. <div class="customer-stats-summary">
  1058. <div class="stats-row">
  1059. <?php
  1060. $total_customers = array_sum($activity_data['counts']);
  1061. foreach ($activity_data['levels'] as $index => $level):
  1062. ?>
  1063. <div class="stat-item">
  1064. <span class="stat-label"><?php echo $level; ?>:</span>
  1065. <span class="stat-value"><?php echo number_format($activity_data['counts'][$index]); ?>
  1066. (<?php echo ($total_customers > 0) ?
  1067. number_format(($activity_data['counts'][$index] / $total_customers) * 100, 1) : '0'; ?>%)</span>
  1068. </div>
  1069. <?php endforeach; ?>
  1070. </div>
  1071. </div>
  1072. </div>
  1073. <script>
  1074. // 客户活跃度分析图
  1075. var activityCtx = document.getElementById('customerActivityChart').getContext('2d');
  1076. var activityChart = new Chart(activityCtx, {
  1077. type: 'bar',
  1078. data: {
  1079. labels: <?php echo json_encode($activity_data['levels']); ?>,
  1080. datasets: [{
  1081. label: '客户数量',
  1082. data: <?php echo json_encode($activity_data['counts']); ?>,
  1083. backgroundColor: [
  1084. 'rgba(54, 162, 235, 0.7)',
  1085. 'rgba(75, 192, 192, 0.7)',
  1086. 'rgba(255, 206, 86, 0.7)',
  1087. 'rgba(255, 99, 132, 0.7)',
  1088. 'rgba(153, 102, 255, 0.7)'
  1089. ],
  1090. borderWidth: 1
  1091. }]
  1092. },
  1093. options: {
  1094. responsive: true,
  1095. scales: {
  1096. y: {
  1097. beginAtZero: true,
  1098. title: {
  1099. display: true,
  1100. text: '客户数量'
  1101. }
  1102. }
  1103. }
  1104. }
  1105. });
  1106. </script>
  1107. <?php
  1108. }
  1109. /**
  1110. * 渲染客户流失风险分析图表
  1111. *
  1112. * @param array $risk_data 客户流失风险数据
  1113. * @return void
  1114. */
  1115. function renderCustomerChurnRiskChart($risk_data) {
  1116. ?>
  1117. <div class="chart-container">
  1118. <div class="chart-header">
  1119. <h2 class="chart-title">客户流失风险分析</h2>
  1120. </div>
  1121. <canvas id="customerRiskChart"></canvas>
  1122. <div class="customer-stats-summary">
  1123. <div class="stats-row">
  1124. <?php
  1125. $total_customers = array_sum($risk_data['counts']);
  1126. foreach ($risk_data['levels'] as $index => $level):
  1127. ?>
  1128. <div class="stat-item">
  1129. <span class="stat-label"><?php echo $level; ?>:</span>
  1130. <span class="stat-value"><?php echo number_format($risk_data['counts'][$index]); ?>
  1131. (<?php echo ($total_customers > 0) ?
  1132. number_format(($risk_data['counts'][$index] / $total_customers) * 100, 1) : '0'; ?>%)</span>
  1133. </div>
  1134. <?php endforeach; ?>
  1135. </div>
  1136. </div>
  1137. </div>
  1138. <script>
  1139. // 客户流失风险分析图
  1140. var riskCtx = document.getElementById('customerRiskChart').getContext('2d');
  1141. var riskChart = new Chart(riskCtx, {
  1142. type: 'doughnut',
  1143. data: {
  1144. labels: <?php echo json_encode($risk_data['levels']); ?>,
  1145. datasets: [{
  1146. data: <?php echo json_encode($risk_data['counts']); ?>,
  1147. backgroundColor: [
  1148. 'rgba(54, 162, 235, 0.7)',
  1149. 'rgba(75, 192, 192, 0.7)',
  1150. 'rgba(255, 206, 86, 0.7)',
  1151. 'rgba(255, 99, 132, 0.7)',
  1152. 'rgba(153, 102, 255, 0.7)'
  1153. ],
  1154. borderWidth: 1
  1155. }]
  1156. },
  1157. options: {
  1158. responsive: true,
  1159. plugins: {
  1160. legend: {
  1161. position: 'right',
  1162. }
  1163. }
  1164. }
  1165. });
  1166. </script>
  1167. <?php
  1168. }
  1169. /**
  1170. * 渲染客户来源分析图表
  1171. *
  1172. * @param array $source_data 客户来源数据
  1173. * @return void
  1174. */
  1175. function renderCustomerSourceChart($source_data) {
  1176. ?>
  1177. <div class="chart-container">
  1178. <div class="chart-header">
  1179. <h2 class="chart-title">客户来源分析</h2>
  1180. </div>
  1181. <canvas id="customerSourceChart"></canvas>
  1182. <div class="customer-stats-summary">
  1183. <div class="stats-row">
  1184. <?php
  1185. $total_customers = array_sum($source_data['counts']);
  1186. foreach ($source_data['sources'] as $index => $source):
  1187. ?>
  1188. <div class="stat-item">
  1189. <span class="stat-label"><?php echo $source; ?>:</span>
  1190. <span class="stat-value"><?php echo number_format($source_data['counts'][$index]); ?>
  1191. (<?php echo ($total_customers > 0) ?
  1192. number_format(($source_data['counts'][$index] / $total_customers) * 100, 1) : '0'; ?>%)</span>
  1193. </div>
  1194. <?php endforeach; ?>
  1195. </div>
  1196. </div>
  1197. </div>
  1198. <script>
  1199. // 客户来源分析图
  1200. var sourceCtx = document.getElementById('customerSourceChart').getContext('2d');
  1201. var sourceChart = new Chart(sourceCtx, {
  1202. type: 'pie',
  1203. data: {
  1204. labels: <?php echo json_encode($source_data['sources']); ?>,
  1205. datasets: [{
  1206. data: <?php echo json_encode($source_data['counts']); ?>,
  1207. backgroundColor: [
  1208. 'rgba(54, 162, 235, 0.7)',
  1209. 'rgba(75, 192, 192, 0.7)',
  1210. 'rgba(255, 206, 86, 0.7)',
  1211. 'rgba(255, 99, 132, 0.7)',
  1212. 'rgba(153, 102, 255, 0.7)',
  1213. 'rgba(255, 159, 64, 0.7)'
  1214. ],
  1215. borderWidth: 1
  1216. }]
  1217. },
  1218. options: {
  1219. responsive: true,
  1220. plugins: {
  1221. legend: {
  1222. position: 'right',
  1223. }
  1224. }
  1225. }
  1226. });
  1227. </script>
  1228. <?php
  1229. }
  1230. /**
  1231. * 获取客户转化漏斗数据
  1232. *
  1233. * @param mysqli $conn 数据库连接
  1234. * @param string $start_date 开始日期
  1235. * @param string $end_date 结束日期
  1236. * @return array 客户转化漏斗数据
  1237. */
  1238. function getCustomerConversionFunnel($conn, $start_date, $end_date) {
  1239. // 获取总客户数(潜在客户)
  1240. $total_sql = "SELECT COUNT(id) as total FROM customer";
  1241. $total_result = $conn->query($total_sql);
  1242. $total_row = $total_result->fetch_assoc();
  1243. $total_customers = $total_row['total'];
  1244. // 获取明确需求的客户数
  1245. $needs_sql = "SELECT COUNT(id) as needs_count FROM customer WHERE cs_deal = 2";
  1246. $needs_result = $conn->query($needs_sql);
  1247. $needs_row = $needs_result->fetch_assoc();
  1248. $needs_customers = $needs_row['needs_count'];
  1249. // 获取已成交客户数
  1250. $deal_sql = "SELECT COUNT(id) as deal_count FROM customer WHERE cs_deal = 3";
  1251. $deal_result = $conn->query($deal_sql);
  1252. $deal_row = $deal_result->fetch_assoc();
  1253. $deal_customers = $deal_row['deal_count'];
  1254. // 获取有订单的客户数
  1255. $order_sql = "SELECT COUNT(DISTINCT customer_id) as order_count FROM orders WHERE order_date BETWEEN ? AND ?";
  1256. $order_stmt = $conn->prepare($order_sql);
  1257. $order_stmt->bind_param("ss", $start_date, $end_date);
  1258. $order_stmt->execute();
  1259. $order_result = $order_stmt->get_result();
  1260. $order_row = $order_result->fetch_assoc();
  1261. $order_customers = $order_row['order_count'];
  1262. // 获取复购客户数(多次下单)
  1263. $repeat_sql = "SELECT COUNT(customer_id) as repeat_count FROM (
  1264. SELECT customer_id, COUNT(id) as order_count
  1265. FROM orders
  1266. WHERE order_date BETWEEN ? AND ?
  1267. GROUP BY customer_id
  1268. HAVING order_count > 1
  1269. ) as repeat_customers";
  1270. $repeat_stmt = $conn->prepare($repeat_sql);
  1271. $repeat_stmt->bind_param("ss", $start_date, $end_date);
  1272. $repeat_stmt->execute();
  1273. $repeat_result = $repeat_stmt->get_result();
  1274. $repeat_row = $repeat_result->fetch_assoc();
  1275. $repeat_customers = $repeat_row['repeat_count'];
  1276. return [
  1277. 'stages' => ['潜在客户', '明确需求', '已成交', '有效订单', '复购客户'],
  1278. 'counts' => [$total_customers, $needs_customers, $deal_customers, $order_customers, $repeat_customers]
  1279. ];
  1280. }
  1281. /**
  1282. * 渲染客户转化漏斗图表
  1283. *
  1284. * @param array $funnel_data 客户转化漏斗数据
  1285. * @return void
  1286. */
  1287. function renderCustomerFunnelChart($funnel_data) {
  1288. ?>
  1289. <div class="chart-container">
  1290. <div class="chart-header">
  1291. <h2 class="chart-title">客户转化漏斗</h2>
  1292. </div>
  1293. <canvas id="customerFunnelChart" style="max-height: 400px;"></canvas>
  1294. <div class="customer-stats-summary">
  1295. <div class="stats-row">
  1296. <?php
  1297. foreach ($funnel_data['stages'] as $index => $stage):
  1298. $current_count = $funnel_data['counts'][$index];
  1299. $prev_count = $index > 0 ? $funnel_data['counts'][$index-1] : $current_count;
  1300. $conversion_rate = $prev_count > 0 ? ($current_count / $prev_count) * 100 : 0;
  1301. ?>
  1302. <div class="stat-item">
  1303. <span class="stat-label"><?php echo $stage; ?>:</span>
  1304. <span class="stat-value"><?php echo number_format($current_count); ?></span>
  1305. <?php if ($index > 0): ?>
  1306. <span class="stat-conversion">
  1307. 转化率: <?php echo number_format($conversion_rate, 1); ?>%
  1308. </span>
  1309. <?php endif; ?>
  1310. </div>
  1311. <?php endforeach; ?>
  1312. </div>
  1313. </div>
  1314. </div>
  1315. <script>
  1316. // 客户转化漏斗图
  1317. var funnelCtx = document.getElementById('customerFunnelChart').getContext('2d');
  1318. var funnelChart = new Chart(funnelCtx, {
  1319. type: 'bar',
  1320. data: {
  1321. labels: <?php echo json_encode($funnel_data['stages']); ?>,
  1322. datasets: [{
  1323. label: '客户数量',
  1324. data: <?php echo json_encode($funnel_data['counts']); ?>,
  1325. backgroundColor: [
  1326. 'rgba(54, 162, 235, 0.7)',
  1327. 'rgba(75, 192, 192, 0.7)',
  1328. 'rgba(255, 206, 86, 0.7)',
  1329. 'rgba(255, 99, 132, 0.7)',
  1330. 'rgba(153, 102, 255, 0.7)'
  1331. ],
  1332. borderWidth: 1
  1333. }]
  1334. },
  1335. options: {
  1336. indexAxis: 'y',
  1337. responsive: true,
  1338. scales: {
  1339. x: {
  1340. beginAtZero: true,
  1341. title: {
  1342. display: true,
  1343. text: '客户数量'
  1344. }
  1345. }
  1346. },
  1347. plugins: {
  1348. tooltip: {
  1349. callbacks: {
  1350. afterLabel: function(context) {
  1351. var index = context.dataIndex;
  1352. if (index > 0) {
  1353. var currentValue = context.parsed.x;
  1354. var previousValue = context.dataset.data[index-1];
  1355. var conversionRate = previousValue > 0 ? (currentValue / previousValue * 100).toFixed(1) : 0;
  1356. return '转化率: ' + conversionRate + '%';
  1357. }
  1358. return '';
  1359. }
  1360. }
  1361. }
  1362. }
  1363. }
  1364. });
  1365. </script>
  1366. <?php
  1367. }