'http://85cg744gf528.vicp.fun:25085/get_bodysize', 'timeout' => 120, 'connect_timeout' => 30 ]; /** * 开始AI身体测量分析 */ public function startAnalysis() { $params = $this->request->post(); // 验证必要参数 if (empty($params['profile_id'])) { $this->error('档案ID不能为空'); } if (empty($params['photos']) || !is_array($params['photos'])) { $this->error('请上传身体照片'); } try { // 验证档案归属 $profile = \app\common\model\BodyProfile::where('id', $params['profile_id']) ->where('user_id', $this->auth->id) ->find(); if (!$profile) { $this->error('档案不存在'); } // 检查是否有正在处理的任务 $existingTask = \think\Db::table('fa_ai_measurement_task') ->where('profile_id', $params['profile_id']) ->where('status', 'in', [0, 1]) // 待处理或处理中 ->find(); if ($existingTask) { $this->error('该档案已有正在处理的AI测量任务,请稍后再试'); } // 验证照片格式 $requiredPhotos = ['front', 'side', 'back']; foreach ($requiredPhotos as $angle) { if (empty($params['photos'][$angle])) { $this->error("请上传{$angle}角度的身体照片"); } } // 创建AI测量任务 $taskData = [ 'profile_id' => $params['profile_id'], 'user_id' => $this->auth->id, 'photos' => json_encode($params['photos']), 'params' => json_encode([ 'gender' => $profile->gender, 'height' => $profile->height, 'weight' => $profile->weight ]), 'priority' => $params['priority'] ?? 5, 'status' => 0, // 待处理 'createtime' => time(), 'updatetime' => time() ]; $taskId = \think\Db::table('fa_ai_measurement_task')->insertGetId($taskData); // 立即处理任务(也可以放到队列中异步处理) $this->processTask($taskId); $this->success('AI测量分析已开始', [ 'task_id' => $taskId, 'estimated_time' => 30 // 预计处理时间(秒) ]); } catch (\Exception $e) { $this->error($e->getMessage()); } } /** * 获取AI测量结果 */ public function getResult() { $taskId = $this->request->get('task_id/d'); $profileId = $this->request->get('profile_id/d'); if (!$taskId && !$profileId) { $this->error('任务ID或档案ID不能为空'); } try { $query = \think\Db::table('fa_ai_measurement_task') ->where('user_id', $this->auth->id); if ($taskId) { $query->where('id', $taskId); } else { $query->where('profile_id', $profileId)->order('id DESC'); } $task = $query->find(); if (!$task) { $this->error('任务不存在'); } // 根据任务状态返回不同结果 switch ($task['status']) { case 0: // 待处理 $this->success('任务排队中', [ 'status' => 'pending', 'message' => '任务正在排队等待处理' ]); break; case 1: // 处理中 $this->success('正在分析中', [ 'status' => 'processing', 'message' => 'AI正在分析您的身体照片,请稍候...', 'progress' => $this->estimateProgress($task) ]); break; case 2: // 完成 $result = json_decode($task['result'], true); $this->success('分析完成', [ 'status' => 'completed', 'data' => $this->formatMeasurementResult($result, $task['profile_id']) ]); break; case 3: // 失败 $this->success('分析失败', [ 'status' => 'failed', 'message' => $task['error_message'] ?: '分析过程中出现错误', 'can_retry' => $task['attempts'] < $task['max_attempts'] ]); break; default: $this->error('未知的任务状态'); } } catch (\Exception $e) { $this->error($e->getMessage()); } } /** * 保存AI测量结果 */ public function saveResult() { $params = $this->request->post(); if (empty($params['task_id'])) { $this->error('任务ID不能为空'); } try { // 获取任务信息 $task = \think\Db::table('fa_ai_measurement_task') ->where('id', $params['task_id']) ->where('user_id', $this->auth->id) ->where('status', 2) // 只有完成的任务才能保存 ->find(); if (!$task) { $this->error('任务不存在或未完成'); } $result = json_decode($task['result'], true); if (!$result || !isset($result['measurements'])) { $this->error('测量结果数据异常'); } // 保存测量数据 $measurement = AiMeasurementService::saveMeasurementResult( $task['profile_id'], $result['measurements'], json_decode($task['photos'], true), $result['confidence'] ?? null ); $this->success('测量结果已保存', [ 'measurement_id' => $measurement->id ]); } catch (\Exception $e) { $this->error($e->getMessage()); } } /** * 重新分析 */ public function retryAnalysis() { $taskId = $this->request->post('task_id/d'); if (!$taskId) { $this->error('任务ID不能为空'); } try { $task = \think\Db::table('fa_ai_measurement_task') ->where('id', $taskId) ->where('user_id', $this->auth->id) ->where('status', 3) // 失败的任务 ->find(); if (!$task) { $this->error('任务不存在或不允许重试'); } if ($task['attempts'] >= $task['max_attempts']) { $this->error('重试次数已达上限'); } // 重置任务状态 \think\Db::table('fa_ai_measurement_task') ->where('id', $taskId) ->update([ 'status' => 0, 'error_message' => '', 'updatetime' => time() ]); // 重新处理任务 $this->processTask($taskId); $this->success('已重新开始分析'); } catch (\Exception $e) { $this->error($e->getMessage()); } } /** * 获取测量字段配置 */ public function getMeasurementConfig() { $gender = $this->request->get('gender/d', 1); try { $config = AiMeasurementService::getMeasurementDisplayConfig($gender); $this->success('获取成功', $config); } catch (\Exception $e) { $this->error($e->getMessage()); } } /** * 获取AI测量素材配置 * @ApiMethod (GET) * @ApiParams (name="mode", type="string", required=false, description="模式:selfie(自拍)、helper(帮拍)、common(通用)、all(全部)") */ public function getMaterialConfig() { $mode = $this->request->get('mode', 'all'); // try { $config = []; switch ($mode) { case 'selfie': $config = $this->getSelfieConfig(); break; case 'helper': $config = $this->getHelperConfig(); break; case 'common': $config = $this->getCommonConfig(); break; case 'all': default: $config = [ 'selfie' => $this->getSelfieConfig(), 'helper' => $this->getHelperConfig(), 'common' => $this->getCommonConfig() ]; break; } $this->success('获取成功', $config); // } catch (\Exception $e) { // $this->error($e->getMessage()); // } } /** * 获取自拍模式配置 */ private function getSelfieConfig() { return [ 'enabled' => config('site.ai_measure_selfie_enabled'), 'intro_image' => $this->formatFileUrl(config('site.ai_measure_selfie_intro_image')), // 引导教程 'tutorial' => [ 'images' => $this->parseImages(config('site.ai_measure_selfie_tutorial_images')), 'video' => $this->formatFileUrl(config('site.ai_measure_selfie_tutorial_video')) ], // 陀螺仪检测 'gyroscope' => [ 'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_gyro_voice')), 'example' => $this->formatFileUrl(config('site.ai_measure_selfie_gyro_example')) ], // 拍摄正面 'front_shooting' => [ 'frame' => $this->formatFileUrl(config('site.ai_measure_selfie_front_frame')), 'demo' => $this->formatFileUrl(config('site.ai_measure_selfie_front_demo')), 'text' => config('site.ai_measure_selfie_front_text'), 'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_front_voice')) ], // 拍摄侧面 'side_shooting' => [ 'frame' => $this->formatFileUrl(config('site.ai_measure_selfie_side_frame')), 'demo' => $this->formatFileUrl(config('site.ai_measure_selfie_side_demo')), 'text' => config('site.ai_measure_selfie_side_text'), 'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_side_voice')) ], // 拍摄正面侧平举 'arms_shooting' => [ 'frame' => $this->formatFileUrl(config('site.ai_measure_selfie_arms_frame')), 'demo' => $this->formatFileUrl(config('site.ai_measure_selfie_arms_demo')), 'text' => config('site.ai_measure_selfie_arms_text'), 'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_arms_voice')) ], // 拍摄过程素材 'process_materials' => [ 'countdown_voice' => $this->formatFileUrl(config('site.ai_measure_selfie_countdown_voice')), 'timer_sound' => $this->formatFileUrl(config('site.ai_measure_selfie_timer_sound')), 'complete_sound' => $this->formatFileUrl(config('site.ai_measure_selfie_complete_sound')), 'next_voice' => $this->formatFileUrl(config('site.ai_measure_selfie_next_voice')), 'finish_voice' => $this->formatFileUrl(config('site.ai_measure_selfie_finish_voice')) ] ]; } /** * 获取帮拍模式配置 */ private function getHelperConfig() { return [ 'enabled' => config('site.ai_measure_helper_enabled'), 'intro_image' => $this->formatFileUrl(config('site.ai_measure_helper_intro_image')), // 引导教程 'tutorial' => [ 'images' => $this->parseImages(config('site.ai_measure_helper_tutorial_images')), 'video' => $this->formatFileUrl(config('site.ai_measure_helper_tutorial_video')) ], // 陀螺仪检测 'gyroscope' => [ 'voice' => $this->formatFileUrl(config('site.ai_measure_helper_gyro_voice')), 'example' => $this->formatFileUrl(config('site.ai_measure_helper_gyro_example')) ], // 拍摄正面 'front_shooting' => [ 'frame' => $this->formatFileUrl(config('site.ai_measure_helper_front_frame')), 'demo' => $this->formatFileUrl(config('site.ai_measure_helper_front_demo')), 'text' => config('site.ai_measure_helper_front_text'), 'voice' => $this->formatFileUrl(config('site.ai_measure_helper_front_voice')) ], // 拍摄侧面 'side_shooting' => [ 'frame' => $this->formatFileUrl(config('site.ai_measure_helper_side_frame')), 'demo' => $this->formatFileUrl(config('site.ai_measure_helper_side_demo')), 'text' => config('site.ai_measure_helper_side_text'), 'voice' => $this->formatFileUrl(config('site.ai_measure_helper_side_voice')) ], // 拍摄正面侧平举 'arms_shooting' => [ 'frame' => $this->formatFileUrl(config('site.ai_measure_helper_arms_frame')), 'demo' => $this->formatFileUrl(config('site.ai_measure_helper_arms_demo')), 'text' => config('site.ai_measure_helper_arms_text'), 'voice' => $this->formatFileUrl(config('site.ai_measure_helper_arms_voice')) ], // 拍摄过程素材 'process_materials' => [ 'countdown_voice' => cdnurl(config('site.ai_measure_helper_countdown_voice')), 'timer_sound' => $this->formatFileUrl(config('site.ai_measure_helper_timer_sound')), 'complete_sound' => $this->formatFileUrl(config('site.ai_measure_helper_complete_sound')), 'next_voice' => $this->formatFileUrl(config('site.ai_measure_helper_next_voice')), 'finish_voice' => $this->formatFileUrl(config('site.ai_measure_helper_finish_voice')) ] ]; } /** * 获取通用配置 */ private function getCommonConfig() { return [ 'welcome_notice' => config('site.ai_measure_welcome_notice'), 'privacy_notice' => config('site.ai_measure_privacy_notice'), 'accuracy_disclaimer' => config('site.ai_measure_accuracy_disclaimer'), 'demo_images' => [ 'front_demo' => $this->formatFileUrl(config('site.ai_measure_common_front_demo')), 'side_demo' => $this->formatFileUrl(config('site.ai_measure_common_side_demo')), 'arms_demo' => $this->formatFileUrl(config('site.ai_measure_common_arms_demo')) ] ]; } /** * 格式化文件URL */ private function formatFileUrl($url) { if (empty($url)) { return null; } return cdnurl($url); } /** * 解析图片集合 */ private function parseImages($images) { if (empty($images)) { return []; } // 如果是JSON格式的字符串,解析为数组 if (is_string($images)) { $imageArray = json_decode($images, true); if (json_last_error() === JSON_ERROR_NONE && is_array($imageArray)) { $images = $imageArray; } else { // 如果不是JSON,可能是逗号分隔的字符串 $images = explode(',', $images); } } if (!is_array($images)) { return []; } // 格式化每个图片URL return array_map(function($url) { return $this->formatFileUrl(trim($url)); }, array_filter($images)); } /** * 直接调用第三方AI测量服务 * @ApiMethod (POST) * @ApiParams (name="profile_id", type="integer", required=true, description="档案ID") * @ApiParams (name="photos", type="object", required=true, description="身体照片对象") * @ApiParams (name="photos.front", type="string", required=true, description="正面照片URL") * @ApiParams (name="photos.side", type="string", required=true, description="侧面照片URL") * @ApiParams (name="photos.back", type="string", required=true, description="背面照片URL") */ public function measurementDirect() { $params = $this->request->post(); // 验证必要参数 if (empty($params['profile_id'])) { $this->error('档案ID不能为空'); } // if (empty($params['photos']) || !is_array($params['photos'])) { // $this->error('请上传身体照片'); // } // try { // 验证档案归属 $profile = \app\common\model\BodyProfile::where('id', $params['profile_id']) ->where('user_id', $this->auth->id) ->find(); if (!$profile) { $this->error('档案不存在'); } // 验证照片格式 // $requiredPhotos = ['front', 'side', 'back']; // foreach ($requiredPhotos as $angle) { // if (empty($params['photos'][$angle])) { // $this->error("请上传{$angle}角度的身体照片"); // } // } // 直接使用档案的 $photos = $profile->body_photos_text; // 安全调用第三方AI服务 - 确保身高为数字格式 $heightCm = is_numeric($profile->height) ? floatval($profile->height) : 0; $measurements = $this->safeCallThirdPartyAiService( $photos, $heightCm ); // echo "
";
// print_r($measurements);
// echo "";
// exit;
// 处理结果
// $result = [
// 'measurements' => $measurements,
// 'confidence' => $measurements['_confidence'] ?? 0.8,
// 'warnings' => $measurements['_warnings'] ?? []
// ];
// 清理内部字段
// unset($result['measurements']['_confidence']);
// unset($result['measurements']['_warnings']);
// 格式化结果用于展示
//$formattedResult = $this->formatMeasurementResult($result, $params['profile_id']);
$measurements['height'] = $profile->height;
$measurements['weight'] = $profile->weight;
$this->success('AI测量完成', $measurements);
// } catch (\Exception $e) {
// $this->error($e->getMessage());
// }
}
/**
* 调用第三方AI测量接口
*/
private function callThirdPartyAiService($photos, $height)
{
// 第三方API配置
$apiUrl = $this->thirdPartyApiConfig['url'];
try {
// 准备请求数据 - 确保身高为纯数字(厘米)
$heightValue = is_numeric($height) ? floatval($height) : 0;
$requestData = [
'height' => $heightValue
];
// 处理照片数据 - 转换为base64格式
if (isset($photos['front'])) {
$requestData['image1'] = $this->convertImageToBase64($photos['front']);
}
if (isset($photos['side'])) {
$requestData['image2'] = $this->convertImageToBase64($photos['side']);
}
if (isset($photos['back'])) {
$requestData['image3'] = $this->convertImageToBase64($photos['back']);
}
// 记录请求日志(不包含图片数据)
// $logData = [
// 'url' => $apiUrl,
// 'height' => $requestData['height'],
// 'image_count' => count(array_filter([
// isset($requestData['image1']),
// isset($requestData['image2']),
// isset($requestData['image3'])
// ]))
// ];
// 记录请求日志(包含身高和图片base64数据的前50个字符)
$logData = [
'url' => $apiUrl,
'height' => $heightValue . 'cm',
'image1_preview' => isset($requestData['image1']) ? substr($requestData['image1'], 0, 50) . '...' : null,
'image2_preview' => isset($requestData['image2']) ? substr($requestData['image2'], 0, 50) . '...' : null,
'image3_preview' => isset($requestData['image3']) ? substr($requestData['image3'], 0, 50) . '...' : null,
'request_data_size' => strlen(json_encode($requestData)) . ' bytes'
];
\think\Log::info('Calling third party AI service: ' . json_encode($logData));
// 发送POST请求
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $apiUrl,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($requestData),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->thirdPartyApiConfig['timeout'],
CURLOPT_CONNECTTIMEOUT => $this->thirdPartyApiConfig['connect_timeout'],
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json'
],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new \Exception('请求第三方AI服务失败: ' . $error);
}
// 处理各种HTTP错误状态
if ($httpCode >= 500) {
throw new \Exception('第三方AI服务内部错误: HTTP ' . $httpCode);
} elseif ($httpCode >= 400) {
throw new \Exception('第三方AI服务请求错误: HTTP ' . $httpCode);
} elseif ($httpCode !== 200) {
throw new \Exception('第三方AI服务返回异常状态: HTTP ' . $httpCode);
}
\think\Log::info('Third party AI service response: ' .$response);
// 检查响应内容
if (empty($response)) {
throw new \Exception('第三方AI服务返回空响应');
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('第三方AI服务返回数据格式错误');
}
// 记录API响应信息
// $responseLog = [
// 'http_code' => $httpCode,
// 'response_size' => strlen($response) . ' bytes',
// 'has_body_size' => isset($result['body_size']),
// 'body_size_fields' => isset($result['body_size']) ? array_keys($result['body_size']) : [],
// 'response_preview' => substr($response, 0, 200) . '...'
// ];
// 处理返回的测量数据
// echo "";
// print_r($result);
// echo "";
// exit;
return $this->processMeasurementData($result);
} catch (\Exception $e) {
// 记录错误日志
\think\Log::error('Third party AI service error: ' . $e->getMessage());
throw $e;
}
// 如果执行到这里说明没有异常处理,直接返回处理结果
}
/**
* 安全调用第三方AI服务(带异常处理和默认返回)
*/
private function safeCallThirdPartyAiService($photos, $height)
{
try {
return $this->callThirdPartyAiService($photos, $height);
} catch (\Exception $e) {
// 记录错误日志
\think\Log::error('Third party AI service error, returning default data: ' . $e->getMessage());
// 返回默认的空测量数据
return $this->getDefaultMeasurementData();
}
}
/**
* 获取默认的空测量数据
*/
private function getDefaultMeasurementData()
{
// 返回所有映射字段的空值(使用空字符串)
return [
'chest'=>'', // 净胸围 → 胸围
'waist'=>'', // 净腰围 → 腰围
'hip'=>'', // 净臀围 → 实际臀围
//'thigh', // 净腿根 → 大腿围
'knee'=>'', // 净膝围 → 膝围
'calf'=>'', // 净小腿围 → 小腿围
'arm_length'=>'', // 净手臂长 → 臂长
'wrist'=>'', // 净手腕围 → 手腕围
'pants_length'=>'', // 腿长 → 腿长
'belly_belt'=>'', // 净肚围 → 肚围
'shoulder_width'=>'', // 净肩宽 → 肩宽
'leg_root'=>'', // 净腿根 → 大腿围
'neck'=>'', // 净颈围 → 颈围
'inseam'=>'', // 内腿长 → 内腿长
'upper_arm'=>'', // 净上臂围 → 上臂围
'ankle'=>'', // 净脚踝围 → 脚踝围
'waist_lower'=>'', // 净小腹围 → 下腰围
'mid_waist'=>'', // 净中腰 → 中腰围
'_confidence' => 0.0,
'_warnings' => ['第三方AI服务暂时不可用,返回默认数据']
];
}
/**
* 测试接口 - 返回模拟的第三方API测量数据
* @ApiMethod (POST)
* @ApiParams (name="profile_id", type="integer", required=true, description="档案ID")
*/
public function testMeasurementData()
{
$params = $this->request->post();
// 验证必要参数
if (empty($params['profile_id'])) {
$this->error('档案ID不能为空');
}
// 验证档案归属
$profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
->where('user_id', $this->auth->id)
->find();
if (!$profile) {
$this->error('档案不存在');
}
// 模拟第三方API返回的数据
$mockApiResult = [
"body_size" => [
"datuigen" => 56.973762220946064,
"duwei" => 71.86294164495045,
"jiankuan" => 44.99356951672863,
"jiaohuai" => 20.995062499529606,
"jingwei" => 36.973537078225604,
"neitui" => 67.99506048261769,
"shangbi" => 23.285375591374667,
"shoubichang" => 61.1834335984307,
"shouwanwei" => 16.0697059847192,
"tuichang" => 73.9800462219755,
"tunwei" => 90.08082593388505,
"xiaofu" => 70.98010845587423,
"xiaotuiwei" => 37.2761443409742,
"xigai" => 34.990971006868364,
"xiongwei" => 81.85738385794711,
"yaowei" => 72.93800974219818,
"zhongyao" => 70.99945416888724
],
"confidence" => 0.85
];
// 处理测量数据
$measurements = $this->processMeasurementData($mockApiResult);
// 处理结果
$result = [
'measurements' => $measurements,
'confidence' => $measurements['_confidence'] ?? 0.8,
'warnings' => $measurements['_warnings'] ?? []
];
// 清理内部字段
unset($result['measurements']['_confidence']);
unset($result['measurements']['_warnings']);
// 格式化结果用于展示
$formattedResult = $this->formatMeasurementResult($result, $params['profile_id']);
$this->success('测试数据返回成功', [
'original_api_data' => $mockApiResult['body_size'],
'mapped_measurements' => $result['measurements'],
'formatted_result' => $formattedResult
]);
}
/**
* 将图片URL转换为base64格式
*/
private function convertImageToBase64($imageUrl)
{
try {
// 如果已经是base64格式,直接返回
if (strpos($imageUrl, 'data:image') === 0) {
return $imageUrl;
}
// 如果是相对路径,转换为绝对路径
if (strpos($imageUrl, 'http') !== 0) {
$imageUrl = request()->domain() . $imageUrl;
}
// 获取图片数据
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $imageUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_HTTPHEADER => [
'Accept: image/webp,image/apng,image/*,*/*;q=0.8',
'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
'Cache-Control: no-cache',
]
]);
$imageData = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$curlError = curl_error($ch);
$effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
curl_close($ch);
// 详细的错误处理
if ($curlError) {
throw new \Exception("网络请求失败: {$curlError} (URL: {$imageUrl})");
}
if ($httpCode !== 200) {
throw new \Exception("图片访问失败,HTTP状态码: {$httpCode} (URL: {$imageUrl})");
}
if (!$imageData || strlen($imageData) === 0) {
throw new \Exception("图片数据为空 (URL: {$imageUrl})");
}
// 验证图片数据是否有效
if (!@getimagesizefromstring($imageData)) {
throw new \Exception("获取的数据不是有效的图片格式 (URL: {$imageUrl})");
}
// 确定MIME类型
if (strpos($contentType, 'image/') === 0) {
$mimeType = $contentType;
} else {
// 通过文件扩展名推断
$extension = strtolower(pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_EXTENSION));
$mimeTypes = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'bmp' => 'image/bmp'
];
$mimeType = $mimeTypes[$extension] ?? 'image/jpeg';
}
// 转换为base64
$base64 = base64_encode($imageData);
return "data:{$mimeType};base64,{$base64}";
} catch (\Exception $e) {
\think\Log::error('Convert image to base64 error: ' . $e->getMessage() . ' URL: ' . $imageUrl);
throw new \Exception('图片转换失败: ' . $e->getMessage());
}
}
/**
* 处理第三方API返回的测量数据
*/
private function processMeasurementData($apiResult)
{
// 根据第三方API的返回格式处理数据
// 这里需要根据实际的API返回格式进行调整
$measurements = [];
try {
// 假设API返回格式类似:
// {
// "status": "success",
// "data": {
// "chest": 95.5,
// "waist": 75.2,
// "hip": 98.7,
// ...
// },
// "confidence": 0.85
// }
// 检查返回数据结构 - 可能直接包含body_size字段
if (isset($apiResult['body_size'])) {
$data = $apiResult['body_size'];
$hasValidData = true;
} elseif (isset($apiResult['status']) && $apiResult['status'] === 'success') {
$data = $apiResult['data'] ?? [];
$hasValidData = true;
} else {
// 尝试直接使用返回的数据
$data = $apiResult;
$hasValidData = !empty($data) && is_array($data);
}
if ($hasValidData) {
// 映射字段名(根据第三方API返回的字段名进行映射)
$fieldMapping = [
'xiongwei' => 'chest', // 净胸围 → 胸围
'yaowei' => 'waist', // 净腰围 → 腰围
'tunwei' => 'hip', // 净臀围 → 实际臀围
//'datuigen' => 'thigh', // 净腿根 → 大腿围
'xigai' => 'knee', // 净膝围 → 膝围
'xiaotuiwei' => 'calf', // 净小腿围 → 小腿围
'shoubichang' => 'arm_length', // 净手臂长 → 臂长
'shouwanwei' => 'wrist', // 净手腕围 → 手腕围
'tuichang' => 'pants_length', // 腿长 → 腿长
'duwei' => 'belly_belt', // 净肚围 → 肚围
'jiankuan' => 'shoulder_width', // 净肩宽 → 肩宽
'datuigen' => 'leg_root', // 净腿根 → 大腿围
'jingwei' => 'neck', // 净颈围 → 颈围
'neitui' => 'inseam', // 内腿长 → 内腿长
'shangbi' => 'upper_arm', // 净上臂围 → 上臂围
'jiaohuai' => 'ankle', // 净脚踝围 → 脚踝围
'xiaofu' => 'waist_lower', // 净小腹围 → 下腰围
'zhongyao' => 'mid_waist', // 净中腰 → 中腰围
];
foreach ($fieldMapping as $apiField => $localField) {
if (isset($data[$apiField]) && is_numeric($data[$apiField])) {
$measurements[$localField] = round(floatval($data[$apiField]), 1);
}
}
// 设置置信度和警告信息
$measurements['_confidence'] = $apiResult['confidence'] ?? 0.8;
$measurements['_warnings'] = $apiResult['warnings'] ?? [];
// 如果没有测量数据,添加默认警告
if (count($measurements) === 2) { // 只有_confidence和_warnings
$measurements['_warnings'][] = '第三方AI服务未返回有效的测量数据';
}
} else {
// API返回错误
$errorMsg = $apiResult['message'] ?? $apiResult['error'] ?? '第三方AI服务返回未知错误';
throw new \Exception($errorMsg);
}
} catch (\Exception $e) {
// 处理异常,返回错误信息
$measurements['_confidence'] = 0;
$measurements['_warnings'] = ['数据处理失败: ' . $e->getMessage()];
}
return $measurements;
}
/**
* 处理AI测量任务
*/
private function processTask($taskId)
{
try {
// 更新任务状态为处理中
\think\Db::table('fa_ai_measurement_task')
->where('id', $taskId)
->update([
'status' => 1,
'started_at' => time(),
'attempts' => \think\Db::raw('attempts + 1'),
'updatetime' => time()
]);
// 获取任务详情
$task = \think\Db::table('fa_ai_measurement_task')->where('id', $taskId)->find();
$photos = json_decode($task['photos'], true);
$params = json_decode($task['params'], true);
// 安全调用第三方AI分析服务 - 确保身高为数字格式
$heightCm = is_numeric($params['height']) ? floatval($params['height']) : 0;
$measurements = $this->safeCallThirdPartyAiService(
$photos,
$heightCm
);
// 处理结果
$result = [
'measurements' => $measurements,
'confidence' => $measurements['_confidence'] ?? 0.8,
'warnings' => $measurements['_warnings'] ?? []
];
// 清理内部字段
unset($result['measurements']['_confidence']);
unset($result['measurements']['_warnings']);
// 更新任务状态为完成
\think\Db::table('fa_ai_measurement_task')
->where('id', $taskId)
->update([
'status' => 2,
'result' => json_encode($result),
'completed_at' => time(),
'updatetime' => time()
]);
} catch (\Exception $e) {
// 更新任务状态为失败
\think\Db::table('fa_ai_measurement_task')
->where('id', $taskId)
->update([
'status' => 3,
'error_message' => $e->getMessage(),
'updatetime' => time()
]);
}
}
/**
* 估算处理进度
*/
private function estimateProgress($task)
{
$startTime = $task['started_at'];
$currentTime = time();
$elapsedTime = $currentTime - $startTime;
// 假设总处理时间为30秒
$totalTime = 30;
$progress = min(95, ($elapsedTime / $totalTime) * 100);
return round($progress);
}
/**
* 格式化测量结果用于展示
*/
private function formatMeasurementResult($result, $profileId)
{
$profile = \app\common\model\BodyProfile::find($profileId);
$measurements = $result['measurements'];
// 获取显示配置
$displayConfig = AiMeasurementService::getMeasurementDisplayConfig($profile->gender);
// 格式化数据
$formattedData = [
'profile' => [
'id' => $profile->id,
'name' => $profile->profile_name,
'gender' => $profile->gender,
'height' => $profile->height,
'weight' => $profile->weight
],
'measurements' => [],
'display_config' => $displayConfig,
'confidence' => $result['confidence'] ?? 0,
'warnings' => $result['warnings'] ?? []
];
// 组织测量数据
foreach ($displayConfig as $field => $config) {
$value = isset($measurements[$field]) && $measurements[$field] > 0
? $measurements[$field]
: null;
$formattedData['measurements'][$field] = [
'label' => $config['label'],
'value' => $value,
'unit' => 'cm',
'position' => $config['position'],
'side' => $config['side']
];
}
// 添加基础数据表格
$formattedData['basic_data'] = [
['label' => '身高', 'value' => $profile->height, 'unit' => 'cm'],
['label' => '体重', 'value' => $profile->weight, 'unit' => 'kg'],
];
// 添加测量数据表格
$tableData = [];
$fields = array_keys($measurements);
$chunks = array_chunk($fields, 2);
foreach ($chunks as $chunk) {
$row = [];
foreach ($chunk as $field) {
if (isset($displayConfig[$field])) {
$row[] = [
'label' => $displayConfig[$field]['label'],
'value' => $measurements[$field] ?? null,
'unit' => 'cm'
];
}
}
if (!empty($row)) {
$tableData[] = $row;
}
}
$formattedData['table_data'] = $tableData;
return $formattedData;
}
}