=', $startTime], ['createTime', '<', $endTime], ]; // identifier 条件 if (!empty($identifier)) { $where[] = ['identifier', '=', $identifier]; } // ownerWechatId 条件 if (!empty($ownerWechatId)) { $where[] = ['ownerWechatId', '=', $ownerWechatId]; } // 1. 数据过滤和聚合 - 获取每个客户的R、F、M原始值 $orderModel = new TrafficOrderModel(); $customers = $orderModel ->where($where) ->where(function ($query) { // 只统计有效订单(actualPay大于0) $query->where('actualPay', '>', 0); }) ->field('identifier, MAX(createTime) as lastOrderTime, COUNT(DISTINCT id) as orderCount, SUM(CAST(actualPay AS DECIMAL(18,2))) as totalAmount') ->group('identifier') ->select(); if (empty($customers)) { return [ 'code' => 200, 'msg' => '暂无数据', 'data' => [] ]; } // 2. 计算每个客户的R值(最近消费天数) $customerData = []; foreach ($customers as $customer) { $recencyDays = floor(($endTime - $customer['lastOrderTime']) / (24 * 3600)); $customerData[] = [ 'identifier' => $customer['identifier'], 'R' => $recencyDays, 'F' => (int)$customer['orderCount'], 'M' => (float)$customer['totalAmount'], ]; } // 3. 异常值处理 - 剔除大额异常订单 $mValues = array_column($customerData, 'M'); if (!empty($mValues)) { sort($mValues); $m99Percentile = $this->percentile($mValues, 0.99); $abnormalThreshold = $m99Percentile * $abnormalMoneyRatio; // 标记异常客户(但不删除,仅在计算M维度区间时考虑) foreach ($customerData as &$customer) { $customer['isAbnormal'] = $customer['M'] > $abnormalThreshold; } } // 4. 使用五分位法计算各维度的区间阈值 $rThresholds = $this->calculatePercentiles(array_column($customerData, 'R'), true); // R是反向的 $fThresholds = $this->calculatePercentiles(array_column($customerData, 'F'), false); // M维度排除异常值计算区间 $mValuesForPercentile = array_filter(array_column($customerData, 'M'), function($m) use ($abnormalThreshold) { return isset($abnormalThreshold) ? $m <= $abnormalThreshold : true; }); $mThresholds = $this->calculatePercentiles(array_values($mValuesForPercentile), false); // 5. 计算每个客户的RFM分项得分 $results = []; foreach ($customerData as $customer) { $rScore = $this->scoreByPercentile($customer['R'], $rThresholds, true); // R是反向的 $fScore = $this->scoreByPercentile($customer['F'], $fThresholds, false); $mScore = $customer['isAbnormal'] ? 5 : $this->scoreByPercentile($customer['M'], $mThresholds, false); // 异常值给最高分 // 计算RFM总分(加权求和) $rfmScore = $rScore * $weightR + $fScore * $weightF + $mScore * $weightM; // 可选:标准化为1-100分 $standardScore = null; if ($scoreScale == 100) { $rfmMin = $weightR * 1 + $weightF * 1 + $weightM * 1; $rfmMax = $weightR * 5 + $weightF * 5 + $weightM * 5; $standardScore = (int)round(($rfmScore - $rfmMin) / ($rfmMax - $rfmMin) * 99 + 1); } $results[] = [ 'identifier' => $customer['identifier'], 'R_raw' => $customer['R'], 'R_score' => $rScore, 'F_raw' => $customer['F'], 'F_score' => $fScore, 'M_raw' => round($customer['M'], 2), 'M_score' => $mScore, 'RFM_score' => round($rfmScore, 2), 'RFM_standard_score' => $standardScore, 'cycle_start' => date('Y-m-d H:i:s', $startTime), 'cycle_end' => date('Y-m-d H:i:s', $endTime), 'calculate_time' => date('Y-m-d H:i:s'), ]; } // 按RFM总分降序排序 usort($results, function($a, $b) { return $b['RFM_score'] <=> $a['RFM_score']; }); // 6. 更新 ck_traffic_source 和 s2_wechat_friend 表的RFM值 $this->updateRfmToTables($results, $ownerWechatId); return [ 'code' => 200, 'msg' => '计算成功', 'data' => [ 'results' => $results, 'config' => [ 'cycle_days' => $cycleDays, 'weight_R' => $weightR, 'weight_F' => $weightF, 'weight_M' => $weightM, 'score_scale' => $scoreScale, ], 'statistics' => [ 'total_customers' => count($results), 'avg_rfm_score' => round(array_sum(array_column($results, 'RFM_score')) / count($results), 2), ] ] ]; } catch (\Exception $e) { return [ 'code' => 500, 'msg' => '计算失败:' . $e->getMessage(), 'data' => [] ]; } } /** * 计算 RFM 评分(兼容旧方法,使用固定阈值) * @param int|null $recencyDays 最近购买天数 * @param int $frequency 购买次数 * @param float $monetary 购买金额 * @return array{R:int,F:int,M:int} */ public static function calcRfmScores($recencyDays = 30, $frequency, $monetary) { $recencyDays = is_numeric($recencyDays) ? (int)$recencyDays : 9999; $frequency = max(0, (int)$frequency); $monetary = max(0, (float)$monetary); return [ 'R' => self::scoreR_Default($recencyDays), 'F' => self::scoreF_Default($frequency), 'M' => self::scoreM_Default($monetary), ]; } /** * 使用固定阈值计算R得分(保留兼容性) */ protected static function scoreR_Default(int $days): int { if ($days <= 30) return 5; if ($days <= 60) return 4; if ($days <= 90) return 3; if ($days <= 120) return 2; return 1; } /** * 使用固定阈值计算F得分(保留兼容性) */ protected static function scoreF_Default(int $times): int { if ($times >= 10) return 5; if ($times >= 6) return 4; if ($times >= 3) return 3; if ($times >= 2) return 2; if ($times >= 1) return 1; return 0; } /** * 使用固定阈值计算M得分(保留兼容性) */ protected static function scoreM_Default(float $amount): int { if ($amount >= 2000) return 5; if ($amount >= 1000) return 4; if ($amount >= 500) return 3; if ($amount >= 200) return 2; if ($amount > 0) return 1; return 0; } /** * 计算百分位数(五分位法) * @param array $values 数值数组 * @param bool $reverse 是否反向(R维度需要反向,值越小得分越高) * @return array 返回[0.2, 0.4, 0.6, 0.8]分位数的阈值数组 */ private function calculatePercentiles($values, $reverse = false) { if (empty($values)) { return [0, 0, 0, 0]; } // 去重并排序 $uniqueValues = array_unique($values); sort($uniqueValues); // 如果所有值相同,强制均分5个区间 if (count($uniqueValues) == 1) { $singleValue = $uniqueValues[0]; if ($reverse) { return [$singleValue, $singleValue, $singleValue, $singleValue]; } else { return [$singleValue, $singleValue, $singleValue, $singleValue]; } } $percentiles = [0.2, 0.4, 0.6, 0.8]; $thresholds = []; foreach ($percentiles as $p) { $thresholds[] = $this->percentile($uniqueValues, $p); } return $thresholds; } /** * 计算百分位数 * @param array $sortedArray 已排序的数组 * @param float $percentile 百分位数(0-1之间) * @return float */ private function percentile($sortedArray, $percentile) { if (empty($sortedArray)) { return 0; } $count = count($sortedArray); $index = ($count - 1) * $percentile; $floor = floor($index); $ceil = ceil($index); if ($floor == $ceil) { return $sortedArray[(int)$index]; } $weight = $index - $floor; return $sortedArray[(int)$floor] * (1 - $weight) + $sortedArray[(int)$ceil] * $weight; } /** * 根据五分位法阈值计算得分 * @param float $value 当前值 * @param array $thresholds 阈值数组[T1, T2, T3, T4] * @param bool $reverse 是否反向(R维度反向:值越小得分越高) * @return int 得分1-5 */ private function scoreByPercentile($value, $thresholds, $reverse = false) { if (empty($thresholds) || count($thresholds) < 4) { return 1; } list($t1, $t2, $t3, $t4) = $thresholds; if ($reverse) { // R维度:值越小得分越高 if ($value <= $t1) return 5; if ($value <= $t2) return 4; if ($value <= $t3) return 3; if ($value <= $t4) return 2; return 1; } else { // F和M维度:值越大得分越高 if ($value >= $t4) return 5; if ($value >= $t3) return 4; if ($value >= $t2) return 3; if ($value >= $t1) return 2; return 1; } } /** * 更新RFM值到 ck_traffic_source 和 s2_wechat_friend 表 * * @param array $results RFM计算结果数组 * @param string|null $ownerWechatId 微信ID,用于过滤更新范围 */ private function updateRfmToTables($results, $ownerWechatId = null) { try { foreach ($results as $result) { $identifier = $result['identifier']; $rScore = (string)$result['R_score']; $fScore = (string)$result['F_score']; $mScore = (string)$result['M_score']; // 更新 ck_traffic_source 表 // 根据 identifier 更新所有匹配的记录 $trafficSourceUpdate = [ 'R' => $rScore, 'F' => $fScore, 'M' => $mScore, 'updateTime' => time() ]; TrafficSource::where('identifier', $identifier)->update($trafficSourceUpdate); // 更新 s2_wechat_friend 表 // wechatId 对应 identifier $wechatFriendUpdate = [ 'R' => $rScore, 'F' => $fScore, 'M' => $mScore, 'updateTime' => time() ]; $wechatFriendWhere = ['wechatId' => $identifier]; if (!empty($ownerWechatId)) { $wechatFriendWhere['ownerWechatId'] = $ownerWechatId; } WechatFriendModel::where($wechatFriendWhere)->update($wechatFriendUpdate); } } catch (\Exception $e) { // 记录错误但不影响主流程 \think\Log::error('更新RFM值失败:' . $e->getMessage()); } } }