statistics_order_warnings.php 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138
  1. <?php
  2. /**
  3. * 订单预警系统 - 监控订单异常情况
  4. */
  5. require_once 'conn.php';
  6. require_once 'statistics_utils.php';
  7. // 检查登录状态
  8. if (!isset($_SESSION['employee_id'])) {
  9. checkLogin();
  10. }
  11. // 获取日期范围参数
  12. $date_params = getDateRangeParams();
  13. $current_start_date = $date_params['start_date_sql'];
  14. $current_end_date = $date_params['end_date_sql'];
  15. $date_range = $date_params['date_range'];
  16. // 获取选中的业务员ID
  17. $selected_employee = isset($_GET['employee_id']) ? intval($_GET['employee_id']) : 0;
  18. // 获取所有业务员列表
  19. $sql_employees = "SELECT id, em_user FROM employee ORDER BY em_user";
  20. $employees_result = $conn->query($sql_employees);
  21. // 计算上一个时间段范围(用于比较)
  22. $previous_start_date = '';
  23. $previous_end_date = '';
  24. // 根据当前选择的日期范围,计算上一个对比时段
  25. if ($date_range == 'current_month') {
  26. // 上个月
  27. $previous_start_date = date('Y-m-01', strtotime('-1 month', strtotime($current_start_date)));
  28. $previous_end_date = date('Y-m-t', strtotime('-1 month', strtotime($current_end_date)));
  29. } elseif ($date_range == 'last_month') {
  30. // 上上个月
  31. $previous_start_date = date('Y-m-01', strtotime('-2 month', strtotime($current_start_date)));
  32. $previous_end_date = date('Y-m-t', strtotime('-2 month', strtotime($current_end_date)));
  33. } elseif ($date_range == 'current_year') {
  34. // 去年同期
  35. $previous_start_date = date('Y-01-01', strtotime('-1 year', strtotime($current_start_date)));
  36. $previous_end_date = date('Y-12-31', strtotime('-1 year', strtotime($current_end_date)));
  37. } elseif ($date_range == 'last_30_days' || $date_range == 'last_90_days' || $date_range == 'custom') {
  38. // 上一个同长度周期
  39. $date_diff = (strtotime($current_end_date) - strtotime($current_start_date)) / (60 * 60 * 24);
  40. $previous_end_date = date('Y-m-d', strtotime('-1 day', strtotime($current_start_date)));
  41. $previous_start_date = date('Y-m-d', strtotime("-{$date_diff} day", strtotime($previous_end_date)));
  42. }
  43. // 阈值设置(可以移到数据库或配置文件中)
  44. $order_amount_decrease_threshold = -15; // 订单金额下降超过15%触发预警
  45. $repurchase_cycle_max_threshold = 90; // 复购周期超过90天触发预警
  46. $repurchase_cycle_min_threshold = 0.5; // 复购周期小于正常值的50%触发预警(异常频繁购买)
  47. $inactive_threshold = 60; // 60天未购买视为不活跃客户
  48. $normal_repurchase_days = 30; // 正常复购周期参考值(天)
  49. // 页面头部
  50. include('statistics_header.php');
  51. ?>
  52. <div class="container">
  53. <div class="page-header">
  54. <h1 class="page-title">订单预警系统</h1>
  55. <p class="page-description">监控订单异常情况,提前预警潜在问题</p>
  56. </div>
  57. <!-- 日期筛选 -->
  58. <div class="filter-form">
  59. <form method="get" class="filter-form-inline">
  60. <div class="form-group">
  61. <label for="date_range">分析周期</label>
  62. <select class="form-control" id="date_range" name="date_range" onchange="toggleCustomDates()">
  63. <option value="current_month" <?php echo $date_range == 'current_month' ? 'selected' : ''; ?>>本月</option>
  64. <option value="last_month" <?php echo $date_range == 'last_month' ? 'selected' : ''; ?>>上月</option>
  65. <option value="current_year" <?php echo $date_range == 'current_year' ? 'selected' : ''; ?>>今年</option>
  66. <option value="last_30_days" <?php echo $date_range == 'last_30_days' ? 'selected' : ''; ?>>最近30天</option>
  67. <option value="last_90_days" <?php echo $date_range == 'last_90_days' ? 'selected' : ''; ?>>最近90天</option>
  68. <option value="custom" <?php echo $date_range == 'custom' ? 'selected' : ''; ?>>自定义日期范围</option>
  69. </select>
  70. </div>
  71. <div class="form-group custom-date-inputs" id="custom_start_date" style="display: <?php echo $date_range == 'custom' ? 'inline-block' : 'none'; ?>">
  72. <label for="start_date">开始日期</label>
  73. <input type="date" class="form-control" id="start_date" name="start_date" value="<?php echo $date_params['custom_start']; ?>">
  74. </div>
  75. <div class="form-group custom-date-inputs" id="custom_end_date" style="display: <?php echo $date_range == 'custom' ? 'inline-block' : 'none'; ?>">
  76. <label for="end_date">结束日期</label>
  77. <input type="date" class="form-control" id="end_date" name="end_date" value="<?php echo $date_params['custom_end']; ?>">
  78. </div>
  79. <div class="form-group">
  80. <label for="employee_id">业务员</label>
  81. <select class="form-control" id="employee_id" name="employee_id">
  82. <option value="0">全部业务员</option>
  83. <?php while ($emp = $employees_result->fetch_assoc()): ?>
  84. <option value="<?php echo $emp['id']; ?>" <?php echo $selected_employee == $emp['id'] ? 'selected' : ''; ?>><?php echo htmlspecialchars($emp['em_user']); ?></option>
  85. <?php endwhile; ?>
  86. </select>
  87. </div>
  88. <div class="form-group">
  89. <button type="submit" class="form-btn form-btn-primary">应用筛选</button>
  90. </div>
  91. </form>
  92. </div>
  93. <!-- 预警概览 -->
  94. <div class="warnings-overview">
  95. <div class="row">
  96. <?php
  97. // 获取总预警数
  98. $sql_total_warnings = "SELECT
  99. (SELECT COUNT(*) FROM customer WHERE cs_deal = 3) as total_customers,
  100. (SELECT COUNT(DISTINCT customer_id) FROM orders WHERE order_date BETWEEN ? AND ?) as active_customers";
  101. $stmt = $conn->prepare($sql_total_warnings);
  102. $stmt->bind_param("ss", $current_start_date, $current_end_date);
  103. $stmt->execute();
  104. $result = $stmt->get_result();
  105. $warning_count = $result->fetch_assoc();
  106. // 获取订单金额下降的客户数
  107. $decreasing_amount_count = getDecreasingOrderAmountCustomers($conn, $current_start_date, $current_end_date, $previous_start_date, $previous_end_date, $order_amount_decrease_threshold, true, $selected_employee);
  108. // 获取复购周期异常的客户数
  109. $abnormal_cycle_count = getAbnormalRepurchaseCycleCustomers($conn, $current_start_date, $current_end_date, $repurchase_cycle_max_threshold, $repurchase_cycle_min_threshold, $normal_repurchase_days, true, $selected_employee);
  110. // 获取长期不活跃客户数
  111. $inactive_customers_count = getInactiveCustomers($conn, $current_end_date, $inactive_threshold, true, 1, 10, $selected_employee);
  112. ?>
  113. <div class="col-md-3">
  114. <div class="stat-card warning">
  115. <h3>订单金额下降客户</h3>
  116. <div class="stat-value"><?php echo $decreasing_amount_count; ?></div>
  117. <div class="stat-desc">金额下降超过<?php echo abs($order_amount_decrease_threshold); ?>%</div>
  118. </div>
  119. </div>
  120. <div class="col-md-3">
  121. <div class="stat-card warning">
  122. <h3>复购周期异常客户</h3>
  123. <div class="stat-value"><?php echo $abnormal_cycle_count; ?></div>
  124. <div class="stat-desc">周期异常或不规律</div>
  125. </div>
  126. </div>
  127. <div class="col-md-3">
  128. <div class="stat-card danger">
  129. <h3>长期不活跃客户</h3>
  130. <div class="stat-value"><?php echo $inactive_customers_count; ?></div>
  131. <div class="stat-desc"><?php echo $inactive_threshold; ?>天以上未下单</div>
  132. </div>
  133. </div>
  134. <div class="col-md-3">
  135. <div class="stat-card info">
  136. <h3>客户活跃率</h3>
  137. <div class="stat-value">
  138. <?php
  139. $active_rate = ($warning_count['total_customers'] > 0) ?
  140. round(($warning_count['active_customers'] / $warning_count['total_customers']) * 100, 1) : 0;
  141. echo $active_rate . '%';
  142. ?>
  143. </div>
  144. <div class="stat-desc">选定周期内下单客户占比</div>
  145. </div>
  146. </div>
  147. </div>
  148. </div>
  149. <!-- 订单金额下降客户列表 -->
  150. <div class="warning-section">
  151. <div class="section-header">
  152. <h2>订单金额下降客户</h2>
  153. <p>与上一周期相比,订单金额显著下降的客户</p>
  154. </div>
  155. <table class="data-table">
  156. <thead>
  157. <tr>
  158. <th>客户名称</th>
  159. <th>本期订单金额</th>
  160. <th>上期订单金额</th>
  161. <th>变化百分比</th>
  162. <th>最近订单日期</th>
  163. <th>业务员</th>
  164. <th>操作</th>
  165. </tr>
  166. </thead>
  167. <tbody>
  168. <?php
  169. $decreasing_customers = getDecreasingOrderAmountCustomers(
  170. $conn,
  171. $current_start_date,
  172. $current_end_date,
  173. $previous_start_date,
  174. $previous_end_date,
  175. $order_amount_decrease_threshold,
  176. false,
  177. $selected_employee
  178. );
  179. while ($customer = $decreasing_customers->fetch_assoc()) {
  180. $change_percent = round((($customer['current_amount'] - $customer['previous_amount']) / $customer['previous_amount']) * 100, 1);
  181. $change_class = $change_percent < -20 ? 'text-danger' : 'text-warning';
  182. echo "<tr>";
  183. echo "<td>" . htmlspecialchars($customer['cs_company']) . "</td>";
  184. echo "<td>¥" . number_format($customer['current_amount'], 2) . "</td>";
  185. echo "<td>¥" . number_format($customer['previous_amount'], 2) . "</td>";
  186. echo "<td class='{$change_class}'>" . $change_percent . "%</td>";
  187. echo "<td>" . $customer['last_order_date'] . "</td>";
  188. echo "<td>" . htmlspecialchars($customer['em_user']) . "</td>";
  189. echo "<td><a href='customer_detail.php?id=" . $customer['id'] . "&from_warning=1' class='action-btn action-btn-view'>查看</a></td>";
  190. echo "</tr>";
  191. }
  192. if ($decreasing_customers->num_rows == 0) {
  193. echo "<tr><td colspan='7' class='text-center'>没有发现订单金额下降的客户</td></tr>";
  194. }
  195. ?>
  196. </tbody>
  197. </table>
  198. </div>
  199. <!-- 复购周期异常客户列表 -->
  200. <div class="warning-section">
  201. <div class="section-header">
  202. <h2>复购周期异常客户</h2>
  203. <p>复购周期异常延长或缩短的客户</p>
  204. </div>
  205. <table class="data-table">
  206. <thead>
  207. <tr>
  208. <th>客户名称</th>
  209. <th>平均复购周期(天)</th>
  210. <th>最近复购周期(天)</th>
  211. <th>偏离正常值</th>
  212. <th>最近订单日期</th>
  213. <th>订单总数</th>
  214. <th>业务员</th>
  215. <th>操作</th>
  216. </tr>
  217. </thead>
  218. <tbody>
  219. <?php
  220. $abnormal_customers = getAbnormalRepurchaseCycleCustomers(
  221. $conn,
  222. $current_start_date,
  223. $current_end_date,
  224. $repurchase_cycle_max_threshold,
  225. $repurchase_cycle_min_threshold,
  226. $normal_repurchase_days,
  227. false,
  228. $selected_employee
  229. );
  230. while ($customer = $abnormal_customers->fetch_assoc()) {
  231. $deviation = round((($customer['recent_cycle'] - $normal_repurchase_days) / $normal_repurchase_days) * 100, 1);
  232. $deviation_text = $deviation > 0 ? "+{$deviation}%" : "{$deviation}%";
  233. $deviation_class = $deviation > 50 ? 'text-danger' : ($deviation < -30 ? 'text-warning' : 'text-info');
  234. echo "<tr>";
  235. echo "<td>" . htmlspecialchars($customer['cs_company']) . "</td>";
  236. echo "<td>" . round($customer['avg_cycle'], 1) . "</td>";
  237. echo "<td>" . round($customer['recent_cycle'], 1) . "</td>";
  238. echo "<td class='{$deviation_class}'>" . $deviation_text . "</td>";
  239. echo "<td>" . $customer['last_order_date'] . "</td>";
  240. echo "<td>" . $customer['order_count'] . "</td>";
  241. echo "<td>" . htmlspecialchars($customer['em_user']) . "</td>";
  242. echo "<td><a href='customer_detail.php?id=" . $customer['id'] . "&from_warning=1' class='action-btn action-btn-view'>查看</a></td>";
  243. echo "</tr>";
  244. }
  245. if ($abnormal_customers->num_rows == 0) {
  246. echo "<tr><td colspan='8' class='text-center'>没有发现复购周期异常的客户</td></tr>";
  247. }
  248. ?>
  249. </tbody>
  250. </table>
  251. </div>
  252. <!-- 长期不活跃客户列表 -->
  253. <div class="warning-section" id="inactive-customers">
  254. <div class="section-header">
  255. <h2>长期不活跃客户</h2>
  256. <p>超过<?php echo $inactive_threshold; ?>天未下单的客户</p>
  257. </div>
  258. <table class="data-table">
  259. <thead>
  260. <tr>
  261. <th>客户编码</th>
  262. <th>客户名称</th>
  263. <th>最后订单日期</th>
  264. <th>不活跃天数</th>
  265. <th>历史订单数</th>
  266. <th>历史订单总额</th>
  267. <th>业务员</th>
  268. <th>操作</th>
  269. </tr>
  270. </thead>
  271. <tbody>
  272. <?php
  273. // 获取分页参数
  274. $page = isset($_GET['inactive_page']) ? intval($_GET['inactive_page']) : 1;
  275. $page_size = 10; // 每页显示10条记录
  276. // 获取总记录数
  277. $total_inactive = getInactiveCustomers($conn, $current_end_date, $inactive_threshold, true, 1, 10, $selected_employee);
  278. // 计算总页数
  279. $total_pages = ceil($total_inactive / $page_size);
  280. // 确保页码合法
  281. if ($page < 1) $page = 1;
  282. if ($page > $total_pages && $total_pages > 0) $page = $total_pages;
  283. // 获取当页数据
  284. $inactive_customers = getInactiveCustomers($conn, $current_end_date, $inactive_threshold, false, $page, $page_size, $selected_employee);
  285. while ($customer = $inactive_customers->fetch_assoc()) {
  286. $inactive_days = $customer['inactive_days'];
  287. $inactive_class = $inactive_days > 90 ? 'text-danger' : 'text-warning';
  288. echo "<tr>";
  289. echo "<td title='{$customer['cs_code']}'>" . htmlspecialchars($customer['cs_code']) . "</td>";
  290. echo "<td>" . htmlspecialchars($customer['cs_company'] ?: '未填写') . "</td>";
  291. echo "<td>" . ($customer['last_order_date'] ? $customer['last_order_date'] : '从未下单') . "</td>";
  292. echo "<td class='{$inactive_class}'>" . $inactive_days . "</td>";
  293. echo "<td>" . $customer['order_count'] . "</td>";
  294. echo "<td>¥" . number_format($customer['total_amount'], 2) . "</td>";
  295. echo "<td>" . htmlspecialchars($customer['em_user']) . "</td>";
  296. echo "<td><a href='customer_detail.php?id=" . $customer['id'] . "&from_warning=1' class='action-btn action-btn-view'>查看</a></td>";
  297. echo "</tr>";
  298. }
  299. if ($inactive_customers->num_rows == 0) {
  300. echo "<tr><td colspan='8' class='text-center'>没有发现长期不活跃的客户</td></tr>";
  301. }
  302. ?>
  303. </tbody>
  304. </table>
  305. <!-- 分页控件 -->
  306. <?php if ($total_pages > 1): ?>
  307. <div class="pagination-container">
  308. <ul class="pagination">
  309. <?php
  310. // 生成分页链接的基础URL
  311. $base_url = '?';
  312. foreach ($_GET as $key => $value) {
  313. if ($key != 'inactive_page') {
  314. $base_url .= $key . '=' . urlencode($value) . '&';
  315. }
  316. }
  317. // 上一页链接
  318. if ($page > 1) {
  319. echo "<li class='pager-item'><a class='pager-link' href='{$base_url}inactive_page=" . ($page - 1) . "#inactive-customers'>上一页</a></li>";
  320. } else {
  321. echo "<li class='pager-item disabled'><a class='pager-link' href='#inactive-customers'>上一页</a></li>";
  322. }
  323. // 页码链接
  324. $start_page = max(1, $page - 2);
  325. $end_page = min($total_pages, $page + 2);
  326. if ($start_page > 1) {
  327. echo "<li class='pager-item'><a class='pager-link' href='{$base_url}inactive_page=1#inactive-customers'>1</a></li>";
  328. if ($start_page > 2) {
  329. echo "<li class='pager-item disabled'><a class='pager-link' href='#inactive-customers'>...</a></li>";
  330. }
  331. }
  332. for ($i = $start_page; $i <= $end_page; $i++) {
  333. if ($i == $page) {
  334. echo "<li class='pager-item active'><a class='pager-link' href='#inactive-customers'>{$i}</a></li>";
  335. } else {
  336. echo "<li class='pager-item'><a class='pager-link' href='{$base_url}inactive_page={$i}#inactive-customers'>{$i}</a></li>";
  337. }
  338. }
  339. if ($end_page < $total_pages) {
  340. if ($end_page < $total_pages - 1) {
  341. echo "<li class='pager-item disabled'><a class='pager-link' href='#inactive-customers'>...</a></li>";
  342. }
  343. echo "<li class='pager-item'><a class='pager-link' href='{$base_url}inactive_page={$total_pages}#inactive-customers'>{$total_pages}</a></li>";
  344. }
  345. // 下一页链接
  346. if ($page < $total_pages) {
  347. echo "<li class='pager-item'><a class='pager-link' href='{$base_url}inactive_page=" . ($page + 1) . "#inactive-customers'>下一页</a></li>";
  348. } else {
  349. echo "<li class='pager-item disabled'><a class='pager-link' href='#inactive-customers'>下一页</a></li>";
  350. }
  351. ?>
  352. </ul>
  353. <div class="pagination-info">
  354. 共 <?php echo $total_inactive; ?> 条记录,当前显示第 <?php echo $page; ?> 页,共 <?php echo $total_pages; ?> 页
  355. </div>
  356. </div>
  357. <?php endif; ?>
  358. </div>
  359. <!-- 总体订单趋势图 -->
  360. <div class="chart-section">
  361. <div class="section-header">
  362. <h2>总体订单趋势</h2>
  363. <p>最近12个月的订单数量和金额趋势</p>
  364. </div>
  365. <div class="chart-container">
  366. <canvas id="orderTrendChart"></canvas>
  367. </div>
  368. <?php
  369. // 获取最近12个月的订单趋势数据
  370. $sql_trend = "SELECT
  371. DATE_FORMAT(order_date, '%Y-%m') as month,
  372. COUNT(*) as order_count,
  373. SUM(total_amount) as total_amount
  374. FROM orders
  375. WHERE order_date >= DATE_SUB(?, INTERVAL 11 MONTH)
  376. GROUP BY DATE_FORMAT(order_date, '%Y-%m')
  377. ORDER BY month";
  378. $stmt = $conn->prepare($sql_trend);
  379. $stmt->bind_param("s", $current_end_date);
  380. $stmt->execute();
  381. $trend_result = $stmt->get_result();
  382. $months = [];
  383. $order_counts = [];
  384. $order_amounts = [];
  385. while ($row = $trend_result->fetch_assoc()) {
  386. $months[] = $row['month'];
  387. $order_counts[] = $row['order_count'];
  388. $order_amounts[] = $row['total_amount'];
  389. }
  390. // 转为JSON格式,用于JavaScript图表
  391. $months_json = json_encode($months);
  392. $order_counts_json = json_encode($order_counts);
  393. $order_amounts_json = json_encode($order_amounts);
  394. ?>
  395. </div>
  396. </div>
  397. <style>
  398. .container {
  399. padding: 20px;
  400. max-width: 1280px; /* 稍微扩大容器宽度 */
  401. margin: 0 auto;
  402. }
  403. .page-header {
  404. margin-bottom: 30px;
  405. }
  406. .page-title {
  407. font-size: 24px;
  408. margin-top: 0;
  409. margin-bottom: 5px;
  410. }
  411. .page-description {
  412. color: #666;
  413. margin-bottom: 0;
  414. }
  415. .filter-form {
  416. background-color: #f8f9fa;
  417. border-radius: 4px;
  418. padding: 20px;
  419. margin-bottom: 30px;
  420. box-shadow: 0 1px 2px rgba(0,0,0,0.05);
  421. }
  422. .filter-form-inline {
  423. display: flex;
  424. flex-wrap: wrap;
  425. gap: 15px;
  426. align-items: end;
  427. }
  428. .warnings-overview {
  429. margin-bottom: 30px;
  430. }
  431. .warnings-overview .row {
  432. display: flex;
  433. flex-wrap: wrap;
  434. margin: 0 -10px;
  435. }
  436. .warnings-overview .col-md-3 {
  437. padding: 0 10px;
  438. width: 25%;
  439. flex: 0 0 25%;
  440. box-sizing: border-box;
  441. }
  442. @media (max-width: 992px) {
  443. .warnings-overview .col-md-3 {
  444. width: 50%;
  445. flex: 0 0 50%;
  446. }
  447. }
  448. @media (max-width: 576px) {
  449. .warnings-overview .col-md-3 {
  450. width: 100%;
  451. flex: 0 0 100%;
  452. }
  453. }
  454. .stat-card {
  455. border-radius: 8px;
  456. padding: 20px;
  457. color: #fff;
  458. height: 100%;
  459. margin-bottom: 20px;
  460. box-shadow: 0 3px 6px rgba(0,0,0,0.16);
  461. text-align: center;
  462. transition: transform 0.3s ease;
  463. }
  464. .stat-card:hover {
  465. transform: translateY(-5px);
  466. }
  467. .stat-card h3 {
  468. margin-top: 0;
  469. font-size: 16px;
  470. font-weight: 500;
  471. margin-bottom: 10px;
  472. }
  473. .stat-value {
  474. font-size: 32px;
  475. font-weight: 600;
  476. margin-bottom: 10px;
  477. }
  478. .stat-desc {
  479. font-size: 15px;
  480. opacity: 0.8;
  481. }
  482. .stat-card.warning {
  483. background-color: #ff9800;
  484. }
  485. .stat-card.danger {
  486. background-color: #f44336;
  487. }
  488. .stat-card.info {
  489. background-color: #2196f3;
  490. }
  491. .stat-card.success {
  492. background-color: #4caf50;
  493. }
  494. .warning-section {
  495. background-color: white;
  496. border-radius: 8px;
  497. padding: 25px;
  498. margin-bottom: 35px;
  499. box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  500. }
  501. .section-header {
  502. margin-bottom: 20px;
  503. }
  504. .section-header h2 {
  505. font-size: 20px;
  506. margin-top: 0;
  507. margin-bottom: 5px;
  508. }
  509. .section-header p {
  510. color: #666;
  511. font-size: 15px;
  512. margin-bottom: 15px;
  513. }
  514. .chart-section {
  515. background-color: white;
  516. border-radius: 4px;
  517. padding: 20px;
  518. margin-bottom: 30px;
  519. box-shadow: 0 1px 2px rgba(0,0,0,0.05);
  520. }
  521. .chart-container {
  522. height: 350px;
  523. }
  524. .text-danger {
  525. color: #f44336;
  526. }
  527. .text-warning {
  528. color: #ff9800;
  529. }
  530. .text-info {
  531. color: #2196f3;
  532. }
  533. /* 调整表格和内容样式 */
  534. .data-table {
  535. margin-bottom: 20px;
  536. width: 100%;
  537. font-size: 14px; /* 增加表格字体大小 */
  538. border-collapse: separate;
  539. border-spacing: 0;
  540. table-layout: fixed; /* 固定表格布局 */
  541. }
  542. .data-table th {
  543. font-size: 15px; /* 表头字体大小 */
  544. padding: 12px 15px;
  545. background-color: #f5f5f5;
  546. font-weight: 600;
  547. }
  548. .data-table td {
  549. padding: 12px 15px;
  550. vertical-align: middle;
  551. white-space: nowrap; /* 防止文本换行 */
  552. overflow: hidden; /* 溢出隐藏 */
  553. text-overflow: ellipsis; /* 文本溢出显示省略号 */
  554. }
  555. /* 为长期不活跃客户表格设置列宽 */
  556. #inactive-customers .data-table th:nth-child(1), /* 客户编码 */
  557. #inactive-customers .data-table td:nth-child(1) {
  558. width: 12%;
  559. }
  560. #inactive-customers .data-table th:nth-child(2), /* 客户名称 */
  561. #inactive-customers .data-table td:nth-child(2) {
  562. width: 20%;
  563. }
  564. #inactive-customers .data-table th:nth-child(3), /* 最后订单日期 */
  565. #inactive-customers .data-table td:nth-child(3) {
  566. width: 12%;
  567. }
  568. #inactive-customers .data-table th:nth-child(4), /* 不活跃天数 */
  569. #inactive-customers .data-table td:nth-child(4) {
  570. width: 10%;
  571. }
  572. #inactive-customers .data-table th:nth-child(5), /* 历史订单数 */
  573. #inactive-customers .data-table td:nth-child(5) {
  574. width: 10%;
  575. }
  576. #inactive-customers .data-table th:nth-child(6), /* 历史订单总额 */
  577. #inactive-customers .data-table td:nth-child(6) {
  578. width: 12%;
  579. }
  580. #inactive-customers .data-table th:nth-child(7), /* 业务员 */
  581. #inactive-customers .data-table td:nth-child(7) {
  582. width: 12%;
  583. }
  584. #inactive-customers .data-table th:nth-child(8), /* 操作 */
  585. #inactive-customers .data-table td:nth-child(8) {
  586. width: 12%;
  587. }
  588. .data-table td:last-child {
  589. min-width: 80px; /* 确保操作列有足够宽度 */
  590. text-align: center;
  591. }
  592. .data-table tr:hover {
  593. background-color: #f9f9f9;
  594. }
  595. .action-btn {
  596. padding: 5px 10px;
  597. margin: 0 3px;
  598. min-width: 60px;
  599. display: inline-block;
  600. text-align: center;
  601. border-radius: 4px;
  602. font-size: 13px;
  603. transition: all 0.3s ease;
  604. }
  605. .action-btn-view {
  606. background-color: #2196f3;
  607. color: white;
  608. border: none;
  609. }
  610. .action-btn-view:hover {
  611. background-color: #0d8aee;
  612. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  613. }
  614. /* 为所有客户列表表格添加共同样式 */
  615. .warning-section .data-table {
  616. border: 1px solid #eee;
  617. border-radius: 4px;
  618. box-shadow: 0 1px 3px rgba(0,0,0,0.05);
  619. }
  620. .warning-section .data-table th:first-child,
  621. .warning-section .data-table td:first-child {
  622. padding-left: 20px; /* 左侧留出更多间距 */
  623. }
  624. /* 表格内数字列的对齐方式 */
  625. .warning-section .data-table td:nth-child(3),
  626. .warning-section .data-table td:nth-child(4),
  627. .warning-section .data-table td:nth-child(5) {
  628. text-align: left;
  629. }
  630. /* 调整整体字体大小 */
  631. body {
  632. font-size: 14px;
  633. }
  634. /* 分页样式 */
  635. .pagination-container {
  636. margin-top: 25px;
  637. display: flex;
  638. flex-direction: column;
  639. align-items: center;
  640. }
  641. .pagination {
  642. display: flex;
  643. list-style: none;
  644. padding: 0;
  645. margin-bottom: 15px;
  646. }
  647. .pagination .pager-item {
  648. margin: 0 2px;
  649. }
  650. .pagination .pager-link {
  651. padding: 8px 16px;
  652. border-radius: 4px;
  653. margin: 0 3px;
  654. font-weight: 500;
  655. border: 1px solid #ddd;
  656. background-color: #fff;
  657. color: #2196f3;
  658. text-decoration: none;
  659. display: inline-block;
  660. }
  661. .pagination .pager-item.active .pager-link {
  662. background-color: #2196f3;
  663. color: white;
  664. border-color: #2196f3;
  665. }
  666. .pagination .pager-item.disabled .pager-link {
  667. color: #999;
  668. cursor: not-allowed;
  669. background-color: #f5f5f5;
  670. }
  671. .pagination-info {
  672. margin-top: 10px;
  673. font-size: 14px;
  674. color: #666;
  675. }
  676. .form-btn {
  677. padding: 8px 15px;
  678. border-radius: 4px;
  679. cursor: pointer;
  680. font-size: 14px;
  681. border: none;
  682. transition: all 0.3s ease;
  683. }
  684. .form-btn-primary {
  685. background-color: #2196f3;
  686. color: white;
  687. }
  688. .form-btn-primary:hover {
  689. background-color: #0d8aee;
  690. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  691. }
  692. /* 添加斑马纹和悬停效果 */
  693. .data-table tr:nth-child(even) {
  694. background-color: #f8f8f8;
  695. }
  696. .data-table tbody tr:hover {
  697. background-color: #f0f7ff;
  698. }
  699. </style>
  700. <script>
  701. function toggleCustomDates() {
  702. const dateRange = document.getElementById('date_range').value;
  703. const customDateInputs = document.querySelectorAll('.custom-date-inputs');
  704. if (dateRange === 'custom') {
  705. customDateInputs.forEach(el => el.style.display = 'inline-block');
  706. } else {
  707. customDateInputs.forEach(el => el.style.display = 'none');
  708. }
  709. }
  710. // 订单趋势图
  711. document.addEventListener('DOMContentLoaded', function() {
  712. const ctx = document.getElementById('orderTrendChart').getContext('2d');
  713. const months = <?php echo $months_json; ?>;
  714. const orderCounts = <?php echo $order_counts_json; ?>;
  715. const orderAmounts = <?php echo $order_amounts_json; ?>;
  716. const chart = new Chart(ctx, {
  717. type: 'line',
  718. data: {
  719. labels: months,
  720. datasets: [
  721. {
  722. label: '订单数量',
  723. data: orderCounts,
  724. backgroundColor: 'rgba(54, 162, 235, 0.2)',
  725. borderColor: 'rgba(54, 162, 235, 1)',
  726. borderWidth: 2,
  727. yAxisID: 'y-axis-1'
  728. },
  729. {
  730. label: '订单金额',
  731. data: orderAmounts,
  732. backgroundColor: 'rgba(255, 99, 132, 0.2)',
  733. borderColor: 'rgba(255, 99, 132, 1)',
  734. borderWidth: 2,
  735. yAxisID: 'y-axis-2'
  736. }
  737. ]
  738. },
  739. options: {
  740. responsive: true,
  741. maintainAspectRatio: false,
  742. scales: {
  743. 'y-axis-1': {
  744. type: 'linear',
  745. position: 'left',
  746. title: {
  747. display: true,
  748. text: '订单数量'
  749. }
  750. },
  751. 'y-axis-2': {
  752. type: 'linear',
  753. position: 'right',
  754. title: {
  755. display: true,
  756. text: '订单金额'
  757. },
  758. grid: {
  759. drawOnChartArea: false
  760. }
  761. }
  762. }
  763. }
  764. });
  765. });
  766. </script>
  767. <?php
  768. /**
  769. * 获取订单金额下降的客户
  770. */
  771. function getDecreasingOrderAmountCustomers($conn, $current_start, $current_end, $previous_start, $previous_end, $threshold, $count_only = false, $selected_employee = 0) {
  772. // 构建业务员筛选条件
  773. $employee_filter = $selected_employee > 0 ? " AND c.cs_belong = ?" : "";
  774. // 如果只需要计数
  775. if ($count_only) {
  776. $sql = "SELECT COUNT(DISTINCT c.id) as count
  777. FROM customer c
  778. LEFT JOIN (
  779. SELECT customer_id, SUM(total_amount) as amount
  780. FROM orders
  781. WHERE order_date BETWEEN ? AND ?
  782. GROUP BY customer_id
  783. ) current_period ON c.id = current_period.customer_id
  784. LEFT JOIN (
  785. SELECT customer_id, SUM(total_amount) as amount
  786. FROM orders
  787. WHERE order_date BETWEEN ? AND ?
  788. GROUP BY customer_id
  789. ) previous_period ON c.id = previous_period.customer_id
  790. JOIN employee e ON c.cs_belong = e.id
  791. WHERE previous_period.amount > 0
  792. AND (current_period.amount IS NULL OR (current_period.amount / previous_period.amount - 1) * 100 <= ?)
  793. AND c.cs_deal = 3" . $employee_filter;
  794. $stmt = $conn->prepare($sql);
  795. if ($selected_employee > 0) {
  796. $stmt->bind_param("ssssdi", $current_start, $current_end, $previous_start, $previous_end, $threshold, $selected_employee);
  797. } else {
  798. $stmt->bind_param("ssssd", $current_start, $current_end, $previous_start, $previous_end, $threshold);
  799. }
  800. $stmt->execute();
  801. $result = $stmt->get_result();
  802. $row = $result->fetch_assoc();
  803. return $row['count'];
  804. }
  805. // 如果需要详细数据
  806. $sql = "SELECT
  807. c.id,
  808. c.cs_company,
  809. IFNULL(current_period.amount, 0) as current_amount,
  810. previous_period.amount as previous_amount,
  811. e.em_user,
  812. IFNULL((SELECT MAX(order_date) FROM orders WHERE customer_id = c.id), '') as last_order_date
  813. FROM customer c
  814. LEFT JOIN (
  815. SELECT customer_id, SUM(total_amount) as amount
  816. FROM orders
  817. WHERE order_date BETWEEN ? AND ?
  818. GROUP BY customer_id
  819. ) current_period ON c.id = current_period.customer_id
  820. LEFT JOIN (
  821. SELECT customer_id, SUM(total_amount) as amount
  822. FROM orders
  823. WHERE order_date BETWEEN ? AND ?
  824. GROUP BY customer_id
  825. ) previous_period ON c.id = previous_period.customer_id
  826. JOIN employee e ON c.cs_belong = e.id
  827. WHERE previous_period.amount > 0
  828. AND (current_period.amount IS NULL OR (current_period.amount / previous_period.amount - 1) * 100 <= ?)
  829. AND c.cs_deal = 3" . $employee_filter . "
  830. ORDER BY (current_period.amount / previous_period.amount) ASC";
  831. $stmt = $conn->prepare($sql);
  832. if ($selected_employee > 0) {
  833. $stmt->bind_param("ssssdi", $current_start, $current_end, $previous_start, $previous_end, $threshold, $selected_employee);
  834. } else {
  835. $stmt->bind_param("ssssd", $current_start, $current_end, $previous_start, $previous_end, $threshold);
  836. }
  837. $stmt->execute();
  838. return $stmt->get_result();
  839. }
  840. /**
  841. * 获取复购周期异常的客户
  842. */
  843. function getAbnormalRepurchaseCycleCustomers($conn, $current_start, $current_end, $max_threshold, $min_threshold, $normal_cycle, $count_only = false, $selected_employee = 0) {
  844. // 构建业务员筛选条件
  845. $employee_filter = $selected_employee > 0 ? " AND c.cs_belong = ?" : "";
  846. if ($count_only) {
  847. $sql = "SELECT COUNT(DISTINCT t.customer_id) as count
  848. FROM (
  849. SELECT
  850. o.customer_id,
  851. COUNT(o.id) as order_count,
  852. AVG(DATEDIFF(o.order_date, prev_order.order_date)) as avg_cycle,
  853. MAX(DATEDIFF(o.order_date, prev_order.order_date)) as recent_cycle
  854. FROM orders o
  855. JOIN customer c ON o.customer_id = c.id
  856. JOIN orders prev_order ON o.customer_id = prev_order.customer_id AND prev_order.order_date < o.order_date
  857. WHERE o.order_date BETWEEN ? AND ?" . $employee_filter . "
  858. GROUP BY o.customer_id
  859. HAVING order_count > 1
  860. AND (recent_cycle > ? OR recent_cycle < (? * ?))
  861. ) t";
  862. $stmt = $conn->prepare($sql);
  863. if ($selected_employee > 0) {
  864. $stmt->bind_param("ssiddd", $current_start, $current_end, $selected_employee, $max_threshold, $normal_cycle, $min_threshold);
  865. } else {
  866. $stmt->bind_param("ssddd", $current_start, $current_end, $max_threshold, $normal_cycle, $min_threshold);
  867. }
  868. $stmt->execute();
  869. $result = $stmt->get_result();
  870. $row = $result->fetch_assoc();
  871. return $row['count'];
  872. }
  873. // 使用子查询方式,先获取所有符合条件的客户及其复购周期数据
  874. $sql = "SELECT
  875. abnormal.customer_id as id,
  876. c.cs_company,
  877. abnormal.order_count,
  878. abnormal.avg_cycle,
  879. abnormal.recent_cycle,
  880. abnormal.last_order_date,
  881. e.em_user,
  882. CASE
  883. WHEN abnormal.recent_cycle > ? THEN 1 /* 周期过长 */
  884. ELSE 2 /* 周期过短 */
  885. END as cycle_type,
  886. CASE
  887. WHEN abnormal.recent_cycle > ? THEN abnormal.recent_cycle
  888. ELSE (? * ?) - abnormal.recent_cycle
  889. END as sort_value
  890. FROM (
  891. SELECT
  892. o.customer_id,
  893. COUNT(o.id) as order_count,
  894. AVG(DATEDIFF(o.order_date, prev_order.order_date)) as avg_cycle,
  895. MAX(DATEDIFF(o.order_date, prev_order.order_date)) as recent_cycle,
  896. (SELECT MAX(order_date) FROM orders WHERE customer_id = o.customer_id) as last_order_date
  897. FROM orders o
  898. JOIN customer c ON o.customer_id = c.id
  899. JOIN orders prev_order ON o.customer_id = prev_order.customer_id AND prev_order.order_date < o.order_date
  900. WHERE o.order_date BETWEEN ? AND ?" . $employee_filter . "
  901. GROUP BY o.customer_id
  902. HAVING order_count > 1
  903. AND (recent_cycle > ? OR recent_cycle < (? * ?))
  904. ) as abnormal
  905. JOIN customer c ON abnormal.customer_id = c.id
  906. JOIN employee e ON c.cs_belong = e.id
  907. ORDER BY sort_value DESC";
  908. $stmt = $conn->prepare($sql);
  909. if ($selected_employee > 0) {
  910. $stmt->bind_param("ddddssiddd", $max_threshold, $max_threshold, $normal_cycle, $min_threshold, $current_start, $current_end, $selected_employee, $max_threshold, $normal_cycle, $min_threshold);
  911. } else {
  912. $stmt->bind_param("ddddssddd", $max_threshold, $max_threshold, $normal_cycle, $min_threshold, $current_start, $current_end, $max_threshold, $normal_cycle, $min_threshold);
  913. }
  914. $stmt->execute();
  915. return $stmt->get_result();
  916. }
  917. /**
  918. * 获取长期不活跃的客户
  919. */
  920. function getInactiveCustomers($conn, $end_date, $inactive_days, $count_only = false, $page = 1, $page_size = 10, $selected_employee = 0) {
  921. // 构建业务员筛选条件
  922. $employee_filter = $selected_employee > 0 ? " AND c.cs_belong = ?" : "";
  923. if ($count_only) {
  924. $sql = "SELECT COUNT(*) as count
  925. FROM customer c
  926. LEFT JOIN (
  927. SELECT customer_id, MAX(order_date) as last_order_date
  928. FROM orders
  929. GROUP BY customer_id
  930. ) last_orders ON c.id = last_orders.customer_id
  931. JOIN employee e ON c.cs_belong = e.id
  932. WHERE c.cs_deal = 3
  933. AND (
  934. last_orders.last_order_date IS NULL
  935. OR DATEDIFF(?, last_orders.last_order_date) > ?
  936. )" . $employee_filter;
  937. $stmt = $conn->prepare($sql);
  938. if ($selected_employee > 0) {
  939. $stmt->bind_param("sii", $end_date, $inactive_days, $selected_employee);
  940. } else {
  941. $stmt->bind_param("si", $end_date, $inactive_days);
  942. }
  943. $stmt->execute();
  944. $result = $stmt->get_result();
  945. $row = $result->fetch_assoc();
  946. return $row['count'];
  947. }
  948. $sql = "SELECT
  949. c.id,
  950. c.cs_company,
  951. c.cs_code,
  952. last_orders.last_order_date,
  953. CASE
  954. WHEN last_orders.last_order_date IS NULL THEN DATEDIFF(?, c.cs_addtime)
  955. ELSE DATEDIFF(?, last_orders.last_order_date)
  956. END as inactive_days,
  957. IFNULL(order_stats.order_count, 0) as order_count,
  958. IFNULL(order_stats.total_amount, 0) as total_amount,
  959. e.em_user
  960. FROM customer c
  961. LEFT JOIN (
  962. SELECT customer_id, MAX(order_date) as last_order_date
  963. FROM orders
  964. GROUP BY customer_id
  965. ) last_orders ON c.id = last_orders.customer_id
  966. LEFT JOIN (
  967. SELECT customer_id, COUNT(*) as order_count, SUM(total_amount) as total_amount
  968. FROM orders
  969. GROUP BY customer_id
  970. ) order_stats ON c.id = order_stats.customer_id
  971. JOIN employee e ON c.cs_belong = e.id
  972. WHERE c.cs_deal = 3
  973. AND (
  974. last_orders.last_order_date IS NULL
  975. OR DATEDIFF(?, last_orders.last_order_date) > ?
  976. )" . $employee_filter . "
  977. ORDER BY inactive_days DESC
  978. LIMIT ?, ?";
  979. $offset = ($page - 1) * $page_size;
  980. $stmt = $conn->prepare($sql);
  981. if ($selected_employee > 0) {
  982. $stmt->bind_param("sssiiii", $end_date, $end_date, $end_date, $inactive_days, $selected_employee, $offset, $page_size);
  983. } else {
  984. $stmt->bind_param("sssiii", $end_date, $end_date, $end_date, $inactive_days, $offset, $page_size);
  985. }
  986. $stmt->execute();
  987. return $stmt->get_result();
  988. }
  989. // 页面底部
  990. include('statistics_footer.php');
  991. ?> ?>