903 lines
33 KiB
PHP
903 lines
33 KiB
PHP
<?php
|
||
|
||
namespace app\KitchenScale2\controller\app;
|
||
|
||
use think\Db;
|
||
use think\Request;
|
||
use app\KitchenScale2\controller\app\Base;
|
||
|
||
class Barcode extends Base{
|
||
|
||
protected $barcode_db_msg = [
|
||
'tiaoma'=>'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;
|
||
}
|
||
} |