SchoolPhysicalExamination/application/KitchenScale/controller/app/Guessyoulike.php

728 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\KitchenScale\controller\app;
use think\Db;
use think\Controller;
class Guessyoulike extends Controller {
protected $kitchenscale_db_msg = [
'cookbook' => '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' => []];
}
}