'app_user_cookbook', 'cookbook_label' => 'app_user_cookbook_label', 'foodlist2' => 'app_z_national_standard_food_type_2', 'foodlist3' => 'app_z_national_standard_food_type_3', 'kcal_log' => 'app_user_kcal_log', 'search_log' => 'app_user_search_log', 'search_history' => 'app_user_search_history', 'tag_preference' => 'app_user_tag_preference', 'recommend_cache' => 'app_recommend_cache' ]; protected $config = [ 'tag_limit' => 5, 'item_limit' => 8, 'cache_time' => 3600 ]; /** * 猜你喜欢主接口 */ public function getGuessYouLike($user_id, $input_tags = 'cookbook', $limit = null) { try { if (empty($user_id)) { return ['code' => 0, 'msg' => '用户ID不能为空', 'data' => []]; } if (!in_array($input_tags, ['cookbook', 'food'])) { return ['code' => 0, 'msg' => '类型参数错误,只能是cookbook或food', 'data' => []]; } $tag_limit = $limit ?: $this->config['tag_limit']; $item_limit = $this->config['item_limit']; $cache_key = $this->generateCacheKey($user_id, $input_tags, $tag_limit); $cached_result = $this->getCachedRecommendation($cache_key); if ($cached_result) { return $cached_result; } $is_new_user = $this->isNewUser($user_id); if ($is_new_user) { $result = $this->getHotRecommendationsForNewUser($input_tags, $item_limit); } else { if ($input_tags === 'cookbook') { $result = $this->getCookbookRecommendations($user_id, $tag_limit, $item_limit); } else { $result = $this->getFoodRecommendations($user_id, $tag_limit, $item_limit); } } $this->cacheRecommendation($cache_key, $result, $user_id, $input_tags); return ['code' => 1, 'msg' => '获取成功', 'data' => $result]; } catch (\Exception $e) { return ['code' => 0, 'msg' => '推荐系统异常: ' . $e->getMessage(), 'data' => []]; } } /** * 判断是否是新用户 */ private function isNewUser($user_id) { $cfc = Db::connect('cfc_db'); $has_search_history = $cfc->table($this->kitchenscale_db_msg['search_history']) ->where('user_id', $user_id) ->where('is_del', 0) ->count(); $has_diet_history = $cfc->table($this->kitchenscale_db_msg['kcal_log']) ->where('aud_id', $user_id) ->where('is_del', 0) ->count(); return ($has_search_history + $has_diet_history) == 0; } /** * 为新用户获取热门推荐(只有一个"热门搜索"标签) */ private function getHotRecommendationsForNewUser($input_tags, $item_limit) { $result = []; if ($input_tags === 'cookbook') { $hot_recipes = $this->getHotRecipes($item_limit); $result['热门搜索'] = $hot_recipes; } else { $hot_foods = $this->getHotFoods($item_limit); $result['热门搜索'] = $hot_foods; } return $result; } /** * 获取热门菜谱(基于所有用户的搜索记录和饮食记录) */ private function getHotRecipes($limit) { $cfc = Db::connect('cfc_db'); // 方法1:从搜索记录中获取热门菜谱关键词 $hot_search_keywords = $cfc->table($this->kitchenscale_db_msg['search_history']) ->where('is_del', 0) ->group('keyword') ->order('SUM(search_count) DESC') ->limit($limit * 2) ->field('keyword, SUM(search_count) as total_searches') ->select(); $recipes = []; foreach ($hot_search_keywords as $search_item) { $keyword = $search_item['keyword']; // 查找匹配的菜谱 $recipe = $cfc->table($this->kitchenscale_db_msg['cookbook']) ->where("(title LIKE '%".$keyword."%' OR describe_data LIKE '%".$keyword."%') and is_del = 0") ->field('id, title as name, "cookbook" as type') ->order('create_time DESC') ->find(); if ($recipe && !in_array($recipe['id'], array_column($recipes, 'id'))) { $recipes[] = $recipe; if (count($recipes) >= $limit) break; } } // 方法2:从饮食记录中分析热门菜谱(通过食材关联) if (count($recipes) < $limit) { $remaining = $limit - count($recipes); // 获取热门食材 $hot_foods = $cfc->table($this->kitchenscale_db_msg['kcal_log'] . ' kcal') ->join($this->kitchenscale_db_msg['foodlist3'] . ' f3', 'kcal.food_id = f3.id') ->where('kcal.is_del', 0) ->group('f3.id, f3.name') ->order('COUNT(*) DESC') ->limit(10) ->field('f3.id, f3.name') ->select(); foreach ($hot_foods as $food) { // 查找包含这些食材的菜谱 $related_recipes = $cfc->table($this->kitchenscale_db_msg['cookbook']) ->where("(title LIKE '%".$food['name']."%' OR describe_data LIKE '%".$food['name']."%') and is_del = 0") ->whereNotIn('id', array_column($recipes, 'id')) ->field('id, title as name, "cookbook" as type') ->order('create_time DESC') ->limit($remaining) ->select(); if ($related_recipes) { $recipes = array_merge($recipes, $related_recipes); if (count($recipes) >= $limit) break; } } } // 方法3:如果还不够,从菜谱表中按创建时间获取最新的 if (count($recipes) < $limit) { $remaining = $limit - count($recipes); $more_recipes = $cfc->table($this->kitchenscale_db_msg['cookbook']) ->where('is_del', 0) ->whereNotIn('id', array_column($recipes, 'id')) ->field('id, title as name, "cookbook" as type') ->order('create_time DESC') ->limit($remaining) ->select(); $recipes = array_merge($recipes, $more_recipes ?: []); } return array_slice($recipes, 0, $limit); } /** * 获取热门食材(基于所有用户的饮食记录和搜索记录) */ private function getHotFoods($limit) { $cfc = Db::connect('cfc_db'); $foods = []; // 方法1:从饮食记录中获取热门食材 $hot_from_diet = $cfc->table($this->kitchenscale_db_msg['kcal_log'] . ' kcal') ->join($this->kitchenscale_db_msg['foodlist3'] . ' f3', 'kcal.food_id = f3.id') ->where('kcal.is_del', 0) ->where('f3.is_del', 0) ->group('f3.id, f3.name') ->order('COUNT(*) DESC') ->limit($limit) ->field('f3.id, f3.name, "food" as type, COUNT(*) as eat_count') ->select(); if ($hot_from_diet) { $foods = array_merge($foods, $hot_from_diet); } // 方法2:从搜索记录中获取食材相关关键词 if (count($foods) < $limit) { $remaining = $limit - count($foods); $food_keywords = $cfc->table($this->kitchenscale_db_msg['search_history']) ->where('is_del', 0) ->group('keyword') ->order('SUM(search_count) DESC') ->limit($remaining * 2) ->field('keyword, SUM(search_count) as total_searches') ->select(); foreach ($food_keywords as $keyword_item) { $keyword = $keyword_item['keyword']; // 查找匹配的食材 $food = $cfc->table($this->kitchenscale_db_msg['foodlist3']) ->where('name', 'like', '%' . $keyword . '%') ->where('is_del', 0) ->whereNotIn('id', array_column($foods, 'id')) ->field('id, name, "food" as type') ->find(); if ($food) { $foods[] = $food; if (count($foods) >= $limit) break; } } } // 方法3:如果还不够,从食材表中获取常用食材 if (count($foods) < $limit) { $remaining = $limit - count($foods); $more_foods = $cfc->table($this->kitchenscale_db_msg['foodlist3']) ->where('is_del', 0) ->whereNotIn('id', array_column($foods, 'id')) ->field('id, name, "food" as type') ->order('id DESC') ->limit($remaining) ->select(); $foods = array_merge($foods, $more_foods ?: []); } return array_slice($foods, 0, $limit); } /** * 获取食谱推荐(老用户) */ private function getCookbookRecommendations($user_id, $tag_limit, $item_limit) { $result = []; $preferred_tags = $this->getUserCookbookTags($user_id, $tag_limit); if (empty($preferred_tags)) { $preferred_tags = $this->getHotCookbookTags($tag_limit); } foreach ($preferred_tags as $tag_info) { $tag_name = $tag_info['tag_name']; $recipes = $this->getRecipesByTag($tag_name, $item_limit); if (!empty($recipes)) { $result[$tag_name] = $recipes; } } return $result; } /** * 获取用户食谱标签(基于用户搜索记录和饮食记录) */ private function getUserCookbookTags($user_id, $limit) { $cfc = Db::connect('cfc_db'); $tags = []; // 从用户搜索记录中获取标签 $user_search_tags = $cfc->table($this->kitchenscale_db_msg['search_history']) ->where('user_id', $user_id) ->where('is_del', 0) ->order('search_count DESC, last_searched_at DESC') ->limit($limit * 2) ->field('keyword, search_count') ->select(); foreach ($user_search_tags as $search_item) { $keyword = $search_item['keyword']; // 验证关键词是否对应有效的菜谱标签 $valid_tag = $cfc->table($this->kitchenscale_db_msg['cookbook_label']) ->where('label_name', 'like', '%' . $keyword . '%') ->where('is_del', 0) ->find(); if ($valid_tag) { $tags[] = [ 'tag_name' => $valid_tag['label_name'], 'tag_id' => $valid_tag['id'], 'source' => 'search', 'weight' => $search_item['search_count'] ]; } if (count($tags) >= $limit) break; } // 从用户饮食记录中分析标签(通过食材关联菜谱标签) if (count($tags) < $limit) { $user_diet_foods = $cfc->table($this->kitchenscale_db_msg['kcal_log'] . ' kcal') ->join($this->kitchenscale_db_msg['foodlist3'] . ' f3', 'kcal.food_id = f3.id') ->where('kcal.aud_id', $user_id) ->where('kcal.is_del', 0) ->group('f3.id, f3.name') ->order('COUNT(*) DESC') ->limit(5) ->field('f3.id, f3.name, COUNT(*) as eat_count') ->select(); foreach ($user_diet_foods as $food) { // 查找包含这些食材的菜谱标签 $related_tags = $cfc->table($this->kitchenscale_db_msg['cookbook'] . ' c') ->join($this->kitchenscale_db_msg['cookbook_label'] . ' cl', 'FIND_IN_SET(cl.id, c.cook_label)') ->where('c.food_ids', 'like', '%"' . $food['id'] . '"%') ->where('c.is_del', 0) ->where('cl.is_del', 0) ->group('cl.id, cl.label_name') ->order('COUNT(*) DESC') ->limit(2) ->field('cl.id, cl.label_name, COUNT(*) as recipe_count') ->select(); foreach ($related_tags as $tag) { if (!in_array($tag['label_name'], array_column($tags, 'tag_name'))) { $tags[] = [ 'tag_name' => $tag['label_name'], 'tag_id' => $tag['id'], 'source' => 'diet', 'weight' => $food['eat_count'] ]; } if (count($tags) >= $limit) break; } if (count($tags) >= $limit) break; } } // 按权重排序 usort($tags, function($a, $b) { return $b['weight'] <=> $a['weight']; }); return array_slice($tags, 0, $limit); } /** * 获取热门食谱标签(基于所有用户数据) */ private function getHotCookbookTags($limit) { $cfc = Db::connect('cfc_db'); // 从搜索记录中分析热门标签 $hot_search_tags = $cfc->table($this->kitchenscale_db_msg['search_history']) ->where('is_del', 0) ->group('keyword') ->order('SUM(search_count) DESC') ->limit($limit * 2) ->field('keyword, SUM(search_count) as total_searches') ->select(); $tags = []; foreach ($hot_search_tags as $search_item) { $keyword = $search_item['keyword']; $valid_tag = $cfc->table($this->kitchenscale_db_msg['cookbook_label']) ->where('label_name', 'like', '%' . $keyword . '%') ->where('is_del', 0) ->find(); if ($valid_tag && !in_array($valid_tag['label_name'], array_column($tags, 'tag_name'))) { $tags[] = [ 'tag_name' => $valid_tag['label_name'], 'tag_id' => $valid_tag['id'], 'search_count' => $search_item['total_searches'] ]; } if (count($tags) >= $limit) break; } // 如果不够,从标签表中获取 if (count($tags) < $limit) { $remaining = $limit - count($tags); $more_tags = $cfc->table($this->kitchenscale_db_msg['cookbook_label']) ->where('is_del', 0) ->whereNotIn('id', array_column($tags, 'tag_id')) ->field('id, label_name as tag_name') ->order('id DESC') ->limit($remaining) ->select(); $tags = array_merge($tags, $more_tags ?: []); } return array_slice($tags, 0, $limit); } /** * 根据标签获取食谱 */ private function getRecipesByTag($tag_name, $limit) { $cfc = Db::connect('cfc_db'); $recipes = $cfc->table($this->kitchenscale_db_msg['cookbook'] . ' c') ->join($this->kitchenscale_db_msg['cookbook_label'] . ' cl', 'FIND_IN_SET(cl.id, c.cook_label)') ->where('cl.label_name', 'like', '%' . $tag_name . '%') ->where('c.is_del', 0) ->where('cl.is_del', 0) ->field('c.id, c.title as name, "cookbook" as type') ->order('c.create_time DESC') ->limit($limit) ->select(); return $recipes ?: []; } /** * 获取食材推荐(老用户) */ private function getFoodRecommendations($user_id, $tag_limit, $item_limit) { $result = []; $preferred_categories = $this->getUserFoodCategories($user_id, $tag_limit); if (empty($preferred_categories)) { $preferred_categories = $this->getHotFoodCategories($tag_limit); } foreach ($preferred_categories as $category_info) { $category_name = $category_info['category_name']; $foods = $this->getFoodsByCategory($category_info['category_id'], $item_limit); if (!empty($foods)) { $result[$category_name] = $foods; } } return $result; } /** * 获取用户食材分类(基于用户饮食记录) */ private function getUserFoodCategories($user_id, $limit) { $cfc = Db::connect('cfc_db'); $categories = []; $diet_categories = $cfc->table($this->kitchenscale_db_msg['kcal_log'] . ' kcal') ->join($this->kitchenscale_db_msg['foodlist3'] . ' f3', 'kcal.food_id = f3.id') ->join($this->kitchenscale_db_msg['foodlist2'] . ' f2', 'f3.two_id = f2.id') ->where('kcal.aud_id', $user_id) ->where('kcal.is_del', 0) ->group('f2.id, f2.name') ->order('COUNT(*) DESC') ->limit($limit) ->field('f2.id as category_id, f2.name as category_name, COUNT(*) as eat_count') ->select(); foreach ($diet_categories as $category) { $categories[] = [ 'category_id' => $category['category_id'], 'category_name' => $category['category_name'], 'eat_count' => $category['eat_count'] ]; } return array_slice($categories, 0, $limit); } /** * 获取热门食材分类(基于所有用户数据) */ private function getHotFoodCategories($limit) { $cfc = Db::connect('cfc_db'); // 从饮食记录中分析热门分类 $hot_categories = $cfc->table($this->kitchenscale_db_msg['kcal_log'] . ' kcal') ->join($this->kitchenscale_db_msg['foodlist3'] . ' f3', 'kcal.food_id = f3.id') ->join($this->kitchenscale_db_msg['foodlist2'] . ' f2', 'f3.two_id = f2.id') ->where('kcal.is_del', 0) ->group('f2.id, f2.name') ->order('COUNT(*) DESC') ->limit($limit) ->field('f2.id as category_id, f2.name as category_name, COUNT(*) as total_eat_count') ->select(); if ($hot_categories && count($hot_categories) >= $limit) { return $hot_categories; } // 如果不够,从分类表中获取 $categories = $hot_categories ?: []; if (count($categories) < $limit) { $remaining = $limit - count($categories); $more_categories = $cfc->table($this->kitchenscale_db_msg['foodlist2']) ->where('is_del', 0) ->whereNotIn('id', array_column($categories, 'category_id')) ->field('id as category_id, name as category_name') ->order('id DESC') ->limit($remaining) ->select(); $categories = array_merge($categories, $more_categories ?: []); } return array_slice($categories, 0, $limit); } /** * 根据分类获取食材 */ private function getFoodsByCategory($category_id, $limit) { $cfc = Db::connect('cfc_db'); $foods = $cfc->table($this->kitchenscale_db_msg['foodlist3']) ->where('two_id', $category_id) ->where('is_del', 0) ->field('id, name, "food" as type') ->order('id DESC') ->limit($limit) ->select(); return $foods ?: []; } /** * 生成缓存key */ private function generateCacheKey($user_id, $input_tags, $tag_limit) { return $user_id . ':' . $input_tags . ':' . $tag_limit; } /** * 获取缓存推荐 */ private function getCachedRecommendation($cache_key) { $cfc = Db::connect('cfc_db'); $cache = $cfc->table($this->kitchenscale_db_msg['recommend_cache']) ->where('cache_key', $cache_key) ->where('is_del', 0) ->where('last_hit', '>=', date('Y-m-d H:i:s', time() - $this->config['cache_time'])) ->find(); if ($cache) { $cfc->table($this->kitchenscale_db_msg['recommend_cache']) ->where('id', $cache['id']) ->update([ 'hit_count' => $cache['hit_count'] + 1, 'last_hit' => date('Y-m-d H:i:s') ]); return json_decode($cache['recommend_data'], true); } return null; } /** * 缓存推荐结果 */ private function cacheRecommendation($cache_key, $data, $user_id, $input_tags) { $cfc = Db::connect('cfc_db'); $cache_data = [ 'cache_key' => $cache_key, 'user_id' => $user_id, 'keyword' => $input_tags, 'recommend_data' => json_encode($data, JSON_UNESCAPED_UNICODE), 'hit_count' => 1, 'last_hit' => date('Y-m-d H:i:s'), 'create_time' => date('Y-m-d H:i:s') ]; $cfc->table($this->kitchenscale_db_msg['recommend_cache']) ->insert($cache_data); } /** * 记录用户标签点击(用于更新偏好权重) */ public function recordTagClick($user_id, $tag_name, $tag_type) { try { $cfc = Db::connect('cfc_db'); $preference = $cfc->table($this->kitchenscale_db_msg['tag_preference']) ->where('user_id', $user_id) ->where('tag_name', $tag_name) ->where('tag_type', $tag_type) ->where('is_del', 0) ->find(); if ($preference) { $new_weight = min($preference['preference_weight'] + 1, 10); $cfc->table($this->kitchenscale_db_msg['tag_preference']) ->where('id', $preference['id']) ->update([ 'preference_weight' => $new_weight, 'last_updated' => date('Y-m-d H:i:s') ]); } else { $cfc->table($this->kitchenscale_db_msg['tag_preference']) ->insert([ 'user_id' => $user_id, 'tag_type' => $tag_type, 'tag_name' => $tag_name, 'tag_id' => 0, 'preference_weight' => 1, 'create_time' => date('Y-m-d H:i:s'), 'last_updated' => date('Y-m-d H:i:s') ]); } return true; } catch (\Exception $e) { return false; } } /** * 清除用户推荐缓存 */ public function clearUserCache($user_id) { try { $cfc = Db::connect('cfc_db'); $cfc->table($this->kitchenscale_db_msg['recommend_cache']) ->where('user_id', $user_id) ->update(['is_del' => 1]); return true; } catch (\Exception $e) { return false; } } /** * 获取推荐统计信息 */ public function getRecommendStats($user_id) { $cfc = Db::connect('cfc_db'); $cache_stats = $cfc->table($this->kitchenscale_db_msg['recommend_cache']) ->where('user_id', $user_id) ->where('is_del', 0) ->field('COUNT(*) as cache_count, SUM(hit_count) as total_hits') ->find(); $preference_stats = $cfc->table($this->kitchenscale_db_msg['tag_preference']) ->where('user_id', $user_id) ->where('is_del', 0) ->field('COUNT(*) as tag_count, AVG(preference_weight) as avg_weight') ->find(); return [ 'cache_count' => $cache_stats['cache_count'] ?? 0, 'total_hits' => $cache_stats['total_hits'] ?? 0, 'tag_count' => $preference_stats['tag_count'] ?? 0, 'avg_weight' => round($preference_stats['avg_weight'] ?? 0, 2) ]; } /** * 测试方法 */ public function test() { $user_id = 1; // 测试新用户食谱推荐 $result1 = $this->getGuessYouLike($user_id, 'cookbook'); // 测试新用户食材推荐 $result2 = $this->getGuessYouLike($user_id, 'food'); // 自定义数量 $result3 = $this->getGuessYouLike($user_id, 'cookbook', 3); // 记录标签点击 $this->recordTagClick($user_id, '家常菜', 'cookbook_label'); // 获取统计信息 $stats = $this->getRecommendStats($user_id); return [ 'cookbook_result' => $result1, 'food_result' => $result2, 'custom_limit' => $result3, 'stats' => $stats ]; } /** * 成功返回方法 */ private function success($msg, $data = []) { return ['code' => 1, 'msg' => $msg, 'data' => $data]; } /** * 错误返回方法 */ private function error($msg) { return ['code' => 0, 'msg' => $msg, 'data' => []]; } }