statistics_customers.php 53 KB

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