'app_commodity_barcode',//条码商品表 ]; ################################################################接口################################################################ ################################################################接口################################################################ ################################################################接口################################################################ /** * 查询条码商品信息(对外调用接口) * @return \think\response\Json */ public function search_food_barcode(){ // 尝试捕获异常 try { $data = input('post.'); // 验证参数 if(!array_key_exists('barcode', $data)){ return $this->msg(10001,'未发现条形码'); // 10001: 关键参数缺失 } if(!$this->verify_data_is_ok($data['barcode'],'intnum')){ return $this->msg(10005,'请将镜头对准商品条形码'); // 10005: 参数格式错误 } $return_data = $this->search_food_barcode_action($data); return $return_data; } catch (\Exception $e) { // 捕获异常 $logContent["flie"] = $e->getFile(); $logContent["line"] = $e->getLine(); $logContent['all_content'] = "异常信息:\n"; $logContent['all_content'] .= "消息: " . $e->getMessage() . "\n"; $logContent['all_content'] .= "代码: " . $e->getCode() . "\n"; $logContent['all_content'] .= "文件: " . $e->getFile() . "\n"; $logContent['all_content'] .= "行号: " . $e->getLine() . "\n"; $logContent['all_content'] .= "跟踪信息:\n" . $e->getTraceAsString() . "\n"; // 记录日志 $this->record_api_log($data ?? [], $logContent, null); return $this->msg(99999); // 99999: 网络异常,请稍后重试 } } /** * 查询条码商品信息的具体逻辑 * @param array $data 包含barcode的数据 * @return \think\response\Json */ private function search_food_barcode_action($data) { $barcode = $data['barcode']; try { // 1. 先查询本地数据库 $localProduct = $this->getProductFromLocalDB($barcode); // 2. 检查本地数据是否需要更新 $needApiFetch = true; if ($localProduct) { $needApiFetch = $this->shouldUpdateProduct($localProduct); if (!$needApiFetch) { // 本地数据有效,直接返回 $formattedData = $this->buildResponseData($localProduct); return $this->msg($formattedData, '成功获取商品信息'); } } // 3. 需要从API获取数据 if ($needApiFetch) { $apiResult = $this->getOpenFoodFactsProduct($barcode); if (!$apiResult['error'] && isset($apiResult['data'])) { // API获取成功,处理并保存数据 $processedData = $this->processApiProductData($apiResult['data'], $barcode); // 验证处理后的数据是否有效 if (empty($processedData['Calorie']) || $processedData['Calorie'] === '0') { // 如果卡路里为0,说明营养数据可能不完整 if ($localProduct) { // 有本地数据,返回本地数据 $formattedData = $this->buildResponseData($localProduct); return $this->msg($formattedData, 'API数据不完整,使用缓存数据'); } else { // 使用10004: 未找到有效数据 return $this->msg(10004, '获取的商品营养数据不完整'); } } // 保存到数据库,获取保存结果 $saveResult = $this->saveProductToDatabaseAndGetId($processedData, $barcode, $localProduct); if ($saveResult['success']) { // 保存成功,使用数据库真实ID构建返回数据 $savedProduct = $saveResult['product_data']; $formattedData = $this->buildResponseData($savedProduct); $message = '成功获取商品信息'; return $this->msg($formattedData, $message); } else { // 保存失败,返回10002 return $this->msg(10002, '商品信息保存失败'); } } else { // API获取失败 if ($localProduct) { // 有本地数据,返回本地数据 $formattedData = $this->buildResponseData($localProduct); return $this->msg($formattedData, 'API获取失败,使用缓存数据'); } else { // 使用10004: 未找到有效数据 return $this->msg(10004, '未找到该条码对应的商品信息'); } } } } catch (\Exception $e) { // 记录内部逻辑异常 $logContent = [ 'action' => 'search_food_barcode_action', 'barcode' => $barcode, 'status' => 'error', 'error_message' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]; $this->record_api_log($data, $logContent, null); // 使用10002: 操作失败 return $this->msg(10002, '商品信息查询失败'); } } /** * 直接根据条码查询的接口(兼容GET请求) * @param string $barcode * @return \think\response\Json */ public function getProductByBarcode($barcode = '') { try { $request = Request::instance(); $data = [ 'barcode' => $barcode ?: $request->param('barcode', '') ]; if (empty($data['barcode'])) { return $this->msg(10001, 'barcode is miss'); // 10001: 关键参数缺失 } if (!$this->verify_data_is_ok($data['barcode'], 'intnum')) { return $this->msg(10005, 'barcode type is error'); // 10005: 参数格式错误 } $result = $this->search_food_barcode_action($data); return $result; } catch (\Exception $e) { $logContent = [ 'file' => $e->getFile(), 'line' => $e->getLine(), 'error_message' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]; $this->record_api_log(['barcode' => $barcode], $logContent, null); return $this->msg(99999); // 99999: 网络异常,请稍后重试 } } /** * 批量查询条码接口 * @return \think\response\Json */ public function batchSearchFoodBarcode() { try { $data = input('post.'); if(!array_key_exists('barcodes', $data) || !is_array($data['barcodes'])){ return $this->msg(10001, 'barcodes参数缺失或格式错误'); // 10001: 关键参数缺失 } $results = []; $successCount = 0; foreach ($data['barcodes'] as $barcode) { if ($this->verify_data_is_ok($barcode, 'intnum')) { $result = $this->search_food_barcode_action(['barcode' => $barcode]); // 判断result是Json对象还是数组 if ($result instanceof \think\response\Json) { $resultData = json_decode($result->getContent(), true); } else { $resultData = $result; } if (isset($resultData['code']) && $resultData['code'] === 0) { $successCount++; $results[$barcode] = [ 'status' => 'success', 'data' => $resultData['data'] ]; } else { $results[$barcode] = [ 'status' => 'error', 'message' => $resultData['msg'] ?? '查询失败', 'code' => $resultData['code'] ?? 10002 // 10002: 操作失败 ]; } } else { $results[$barcode] = [ 'status' => 'error', 'message' => '无效的条码格式', 'code' => 10005 // 10005: 参数格式错误 ]; } } $responseData = [ 'results' => $results, 'total' => count($data['barcodes']), 'success_count' => $successCount, 'fail_count' => count($data['barcodes']) - $successCount ]; $message = "批量查询完成,成功{$successCount}个,失败" . (count($data['barcodes']) - $successCount) . "个"; return $this->msg($responseData, $message); } catch (\Exception $e) { $logContent = [ 'file' => $e->getFile(), 'line' => $e->getLine(), 'error_message' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]; $this->record_api_log($data ?? [], $logContent, null); return $this->msg(99999); // 99999: 网络异常,请稍后重试 } } #######################################################################小工具####################################################################### #######################################################################小工具####################################################################### #######################################################################小工具####################################################################### /** * 从本地数据库查询商品 * @param string $barcode * @return array|null */ private function getProductFromLocalDB($barcode) { try { $cfc = Db::connect('cfc_db'); $result = $cfc->table($this->barcode_db_msg['tiaoma']) ->where(['code' => $barcode]) ->find(); return $result ?: null; } catch (\Exception $e) { // 数据库查询异常,返回null return null; } } /** * 保存商品到数据库并获取数据库ID * @param array $productData * @param string $barcode * @param array|null $localProduct 本地已存在的数据 * @return array ['success' => bool, 'product_data' => array, 'id' => int|string] */ private function saveProductToDatabaseAndGetId($productData, $barcode, $localProduct = null) { try { $cfc = Db::connect('cfc_db'); if ($localProduct) { // 更新现有记录 $result = $cfc->table($this->barcode_db_msg['tiaoma']) ->where(['code' => $barcode]) ->update($productData); if ($result !== false) { // 更新成功后重新查询获取完整数据 $updatedProduct = $this->getProductFromLocalDB($barcode); if ($updatedProduct) { return [ 'success' => true, 'product_data' => $updatedProduct, 'id' => $updatedProduct['id'] ]; } } } else { // 插入新记录 $result = $cfc->table($this->barcode_db_msg['tiaoma']) ->insertGetId($productData); if ($result !== false) { // 获取新插入的数据 $newProduct = $this->getProductFromLocalDB($barcode); if ($newProduct) { return [ 'success' => true, 'product_data' => $newProduct, 'id' => $result ]; } } } return [ 'success' => false, 'product_data' => null, 'id' => null ]; } catch (\Exception $e) { error_log("保存商品到数据库失败: " . $e->getMessage()); return [ 'success' => false, 'product_data' => null, 'id' => null ]; } } /** * 检查是否需要更新商品信息 * @param array $product * @return bool */ private function shouldUpdateProduct($product) { // 如果数据是最近30天内创建的,不需要更新 if (isset($product['create_time'])) { $createTime = strtotime($product['create_time']); if (time() - $createTime < 30 * 24 * 3600) { // 30天内 return false; } } // 如果数据不完整(四大营养素有缺失),需要更新 $essentialFields = ['Calorie', 'Protein', 'Fat', 'Carbohydrate']; foreach ($essentialFields as $field) { if (empty($product[$field]) || $product[$field] == '0' || $product[$field] == '') { return true; } } return false; } /** * 调用Open Food Facts API获取商品信息 * @param string $barcode * @return array */ private function getOpenFoodFactsProduct($barcode) { // 尝试多个API端点 $endpoints = [ ['country' => 'world', 'timeout' => 8], // 全球站 ['country' => 'cn', 'timeout' => 5], // 中国站 ['country' => 'us', 'timeout' => 8], // 美国站 ['country' => 'uk', 'timeout' => 8], // 英国站 ]; $lastError = null; foreach ($endpoints as $endpoint) { try { $result = $this->callOpenFoodFactsAPI($barcode, $endpoint['country'], $endpoint['timeout']); if (!$result['error']) { return $result; // 成功获取 } $lastError = $result; // 记录错误 // 如果商品明确不存在(404),不需要尝试其他端点 if ($result['http_code'] == 404) { break; } } catch (\Exception $e) { $lastError = [ 'error' => true, 'message' => 'API调用异常: ' . $e->getMessage(), 'http_code' => 0 ]; } // 短暂延迟,避免请求过快 usleep(100000); // 0.1秒 } return $lastError ?: [ 'error' => true, 'message' => '所有API端点都失败', 'http_code' => 0 ]; } /** * 调用Open Food Facts API * @param string $barcode * @param string $countryCode * @param int $timeout * @return array */ private function callOpenFoodFactsAPI($barcode, $countryCode = 'world', $timeout = 10) { // 清理条码,只保留数字 $cleanBarcode = preg_replace('/[^0-9]/', '', $barcode); if (empty($cleanBarcode)) { return [ 'error' => true, 'message' => '无效的条码格式', 'http_code' => 0 ]; } // 构建API URL $baseUrl = "https://{$countryCode}.openfoodfacts.org"; $apiUrl = "{$baseUrl}/api/v0/product/{$cleanBarcode}.json"; // 初始化cURL $ch = curl_init(); // 设置cURL选项 curl_setopt_array($ch, [ CURLOPT_URL => $apiUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_USERAGENT => 'KitchenScaleApp/1.0 (PHP-cURL)', CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'Accept-Charset: utf-8' ] ]); // 执行请求 $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // 错误处理 if ($response === false) { $errorMsg = curl_error($ch); curl_close($ch); return [ 'error' => true, 'message' => "cURL请求失败: {$errorMsg}", 'http_code' => $httpCode ]; } curl_close($ch); // 解析JSON响应 $data = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { return [ 'error' => true, 'message' => 'JSON解析失败: ' . json_last_error_msg(), 'http_code' => $httpCode, 'raw_response' => $response ]; } // 检查API返回状态 if (!isset($data['status']) || $data['status'] !== 1) { return [ 'error' => true, 'message' => '商品未找到或条码无效', 'http_code' => 404, 'api_status' => $data['status'] ?? 'unknown' ]; } // 返回成功结果 return [ 'error' => false, 'data' => $data['product'], 'http_code' => $httpCode, 'barcode' => $cleanBarcode, 'api_source' => $countryCode ]; } /** * 处理API返回的商品数据 * @param array $productData * @param string $barcode * @return array */ private function processApiProductData($productData, $barcode) { $result = [ 'code' => $barcode, 'name' => $this->getProductName($productData), 'pic_sp' => $this->getProductImage($productData), 'pic_yy' => '', 'Calorie' => '0', 'Protein' => '0', 'Fat' => '0', 'Carbohydrate' => '0', 'other_nutrition' => '', 'original_data' => json_encode($productData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), 'create_time' => date('Y-m-d H:i:s') ]; // 提取营养信息(每100克) $nutriments = $productData['nutriments'] ?? []; // 处理四大营养素 - 优先使用_per_100g字段 $energyKcal = $nutriments['energy-kcal_100g'] ?? $nutriments['energy-kcal'] ?? 0; $protein = $nutriments['proteins_100g'] ?? $nutriments['proteins'] ?? 0; $fat = $nutriments['fat_100g'] ?? $nutriments['fat'] ?? 0; $carbohydrate = $nutriments['carbohydrates_100g'] ?? $nutriments['carbohydrates'] ?? 0; $result['Calorie'] = $this->formatNutritionValue($energyKcal); $result['Protein'] = $this->formatNutritionValue($protein); $result['Fat'] = $this->formatNutritionValue($fat); $result['Carbohydrate'] = $this->formatNutritionValue($carbohydrate); // 处理其他营养素 $otherNutrients = $this->extractOtherNutrients($nutriments); if (!empty($otherNutrients)) { $result['other_nutrition'] = json_encode($otherNutrients, JSON_UNESCAPED_UNICODE); } return $result; } /** * 提取其他营养素信息 * @param array $nutriments * @return array */ private function extractOtherNutrients($nutriments) { $nutrients = []; // 定义营养素映射关系 $nutrientMap = [ // 糖类 'sugars_100g' => ['name' => 'Sugar', 'name_ch' => '糖', 'unit' => 'g', 'type' => 1, 'type_name' => '能量及宏量营养素'], // 纤维 'fiber_100g' => ['name' => 'Fiber', 'name_ch' => '膳食纤维', 'unit' => 'g', 'type' => 1, 'type_name' => '能量及宏量营养素'], // 钠盐 'salt_100g' => ['name' => 'Salt', 'name_ch' => '盐', 'unit' => 'g', 'type' => 3, 'type_name' => '矿物质'], 'sodium_100g' => ['name' => 'Sodium', 'name_ch' => '钠', 'unit' => 'mg', 'type' => 3, 'type_name' => '矿物质'], // 维生素 'vitamin-a_100g' => ['name' => 'VitaminA', 'name_ch' => '维生素A', 'unit' => 'μg RAE', 'type' => 2, 'type_name' => '维生素'], 'vitamin-c_100g' => ['name' => 'VitaminC', 'name_ch' => '维生素C', 'unit' => 'mg', 'type' => 2, 'type_name' => '维生素'], 'vitamin-d_100g' => ['name' => 'VitaminD', 'name_ch' => '维生素D', 'unit' => 'μg', 'type' => 2, 'type_name' => '维生素'], 'vitamin-e_100g' => ['name' => 'VitaminE', 'name_ch' => '维生素E', 'unit' => 'mg α-TE', 'type' => 2, 'type_name' => '维生素'], // 矿物质 'calcium_100g' => ['name' => 'Calcium', 'name_ch' => '钙', 'unit' => 'mg', 'type' => 3, 'type_name' => '矿物质'], 'iron_100g' => ['name' => 'Iron', 'name_ch' => '铁', 'unit' => 'mg', 'type' => 3, 'type_name' => '矿物质'], 'magnesium_100g' => ['name' => 'Magnesium', 'name_ch' => '镁', 'unit' => 'mg', 'type' => 3, 'type_name' => '矿物质'], 'phosphorus_100g' => ['name' => 'Phosphorus', 'name_ch' => '磷', 'unit' => 'mg', 'type' => 3, 'type_name' => '矿物质'], 'potassium_100g' => ['name' => 'Potassium', 'name_ch' => '钾', 'unit' => 'mg', 'type' => 3, 'type_name' => '矿物质'], 'zinc_100g' => ['name' => 'Zinc', 'name_ch' => '锌', 'unit' => 'mg', 'type' => 3, 'type_name' => '矿物质'], ]; foreach ($nutrientMap as $key => $info) { $value = $nutriments[$key] ?? 0; // 跳过值为0或空的营养素 if ($value === 0 || $value === '' || $value === null || floatval($value) == 0) { continue; } $formattedValue = $this->formatNutritionValue($value); $nutrients[] = [ 'name' => $info['name'], 'name_ch' => $info['name_ch'], 'unit' => $info['unit'], 'value' => $formattedValue, 'type' => $info['type'], 'type_name' => $info['type_name'], 'color' => $this->getNutrientColor($info['type']) ]; } return $nutrients; } /** * 构建响应数据 * @param array $product * @return array */ private function buildResponseData($product) { // 从数据库获取ID,如果没有则生成唯一ID $recordId = $product['id'] ?? uniqid(); // 解析other_nutrition $otherNutrition = []; if (!empty($product['other_nutrition'])) { $otherNutrition = json_decode($product['other_nutrition'], true) ?: []; } // 计算四大营养素比例 $protein = floatval($product['Protein'] ?? 0); $fat = floatval($product['Fat'] ?? 0); $carb = floatval($product['Carbohydrate'] ?? 0); $total = $protein + $fat + $carb; $proteinPercent = $total > 0 ? round(($protein / $total) * 100) : 0; $fatPercent = $total > 0 ? round(($fat / $total) * 100) : 0; $carbPercent = $total > 0 ? round(($carb / $total) * 100) : 0; // 构建四大营养素数据 $nutrientsFour = [ [ 'name' => '卡路里', 'unit' => 'kcal', 'color' => '', 'value' => $product['Calorie'] ?? '0', 'proportion' => 0 ], [ 'name' => '蛋白质', 'unit' => 'g', 'color' => '#5180D8', 'value' => $product['Protein'] ?? '0', 'proportion' => (string)$proteinPercent ], [ 'name' => '脂肪', 'unit' => 'g', 'color' => '#ED7886', 'value' => $product['Fat'] ?? '0', 'proportion' => (string)$fatPercent ], [ 'name' => '碳水', 'unit' => 'g', 'color' => '#FFB169', 'value' => $product['Carbohydrate'] ?? '0', 'proportion' => (string)$carbPercent ] ]; // 构建完整营养素列表 $nutrientsList = []; // 添加四大营养素 $nutrientsList[] = [ 'name' => 'Calorie', 'name_ch' => '卡路里', 'unit' => 'kcal', 'value' => $product['Calorie'] ?? '0', 'type' => '1', 'type_name' => '能量及宏量营养素', 'color' => '#C4FFE0' ]; $nutrientsList[] = [ 'name' => 'Protein', 'name_ch' => '蛋白质', 'unit' => 'g', 'value' => $product['Protein'] ?? '0', 'type' => '1', 'type_name' => '能量及宏量营养素', 'color' => '#C4FFE0' ]; $nutrientsList[] = [ 'name' => 'Fat', 'name_ch' => '脂肪', 'unit' => 'g', 'value' => $product['Fat'] ?? '0', 'type' => '1', 'type_name' => '能量及宏量营养素', 'color' => '#C4FFE0' ]; $nutrientsList[] = [ 'name' => 'Carbohydrate', 'name_ch' => '碳水化合物', 'unit' => 'g', 'value' => $product['Carbohydrate'] ?? '0', 'type' => '1', 'type_name' => '能量及宏量营养素', 'color' => '#C4FFE0' ]; // 添加其他营养素 foreach ($otherNutrition as $nutrient) { $nutrientsList[] = $nutrient; } return [ 'id' => (string)$recordId, 'record_id' => (string)$recordId, 'food_type' => 'product', 'name' => $product['name'] ?? '未知商品', 'pic_url' => $product['pic_sp'] ?? 'https://tc.pcxbc.com/food_img/none.png', 'kcal' => $product['Calorie'] ?? '0', 'unit' => 'g', // 每100克 'nutrients_four' => $nutrientsFour, 'nutrients_list' => $nutrientsList ]; } /** * 获取商品名称 * @param array $productData * @return string */ private function getProductName($productData) { $possibleFields = [ 'product_name', 'product_name_zh', 'product_name_cn', 'product_name_fr', 'product_name_en', 'generic_name', 'brands' ]; foreach ($possibleFields as $field) { if (!empty($productData[$field]) && trim($productData[$field]) !== '') { $name = trim($productData[$field]); // 如果包含品牌,可以适当处理 if ($field === 'brands' && isset($productData['product_name'])) { $name = $productData['product_name']; } return $name; } } if (!empty($productData['categories'])) { $categories = explode(',', $productData['categories']); return trim($categories[0]) . ' (未命名商品)'; } return '未知商品'; } /** * 获取商品图片 * @param array $productData * @return string */ private function getProductImage($productData) { $imageFields = [ 'image_url', 'image_front_url', 'image_small_url', 'image_thumb_url', ]; foreach ($imageFields as $field) { if (!empty($productData[$field]) && filter_var($productData[$field], FILTER_VALIDATE_URL)) { return $productData[$field]; } } return ''; } /** * 格式化营养值 * @param mixed $value * @return string */ private function formatNutritionValue($value) { if (is_numeric($value)) { $floatValue = (float)$value; if ($floatValue == (int)$floatValue) { return (string)(int)$floatValue; } $formatted = round($floatValue, 2); return rtrim(rtrim($formatted, '0'), '.'); } return '0'; } /** * 获取营养素颜色 * @param int $type * @return string */ private function getNutrientColor($type) { $colors = [ 1 => '#C4FFE0', // 能量及宏量营养素 2 => '#FFEFB7', // 维生素 3 => '#7DA8E0', // 矿物质 ]; return $colors[$type] ?? '#CCCCCC'; } /** * 计算三大营养素比例(蛋白质、脂肪、碳水化合物) * 使用bcmath函数进行高精度计算 * * @param string $protein 蛋白质值 * @param string $fat 脂肪值 * @param string $carb 碳水化合物值 * @return array 包含三个比例值的数组 [protein_percent, fat_percent, carb_percent] */ private function calculateNutrientProportions($protein, $fat, $carb) { // 转换为字符串确保bc函数正常工作 $protein = (string)($protein ?? '0'); $fat = (string)($fat ?? '0'); $carb = (string)($carb ?? '0'); // 初始化比例为0 $proteinPercent = '0'; $fatPercent = '0'; $carbPercent = '0'; // 计算三大营养素总和 $total = '0'; $total = bcadd($total, $protein, 20); $total = bcadd($total, $fat, 20); $total = bcadd($total, $carb, 20); // 如果总和大于0,计算比例 if (bccomp($total, '0', 20) > 0) { // 蛋白质比例 = (蛋白质 / 总和) * 100 if (bccomp($protein, '0', 20) > 0) { $proteinPercent = bcmul(bcdiv($protein, $total, 20), '100', 20); } // 脂肪比例 = (脂肪 / 总和) * 100 if (bccomp($fat, '0', 20) > 0) { $fatPercent = bcmul(bcdiv($fat, $total, 20), '100', 20); } // 碳水化合物比例 = (碳水化合物 / 总和) * 100 if (bccomp($carb, '0', 20) > 0) { $carbPercent = bcmul(bcdiv($carb, $total, 20), '100', 20); } } // 格式化比例(保留两位小数,去除多余的0) $proteinPercent = $this->formatPercentage($proteinPercent); $fatPercent = $this->formatPercentage($fatPercent); $carbPercent = $this->formatPercentage($carbPercent); return [ 'protein' => $proteinPercent, 'fat' => $fatPercent, 'carb' => $carbPercent ]; } /** * 格式化百分比数值 * 保留两位小数,去除多余的0 * * @param string $percentage 百分比字符串 * @return string 格式化后的百分比 */ private function formatPercentage($percentage) { // 保留两位小数 $formatted = bcadd($percentage, '0', 2); // 去除多余的0和小数点 $formatted = rtrim(rtrim($formatted, '0'), '.'); // 如果是空字符串,返回0 return $formatted === '' ? '0' : $formatted; } }