'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 (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}角度的身体照片"); // } // } // 直接使用档案的 body_photos 字段转json $photos = json_decode($profile->body_photos, true); // 安全调用第三方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']); $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); } // 检查响应内容 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) . '...' ]; \think\Log::info('Third party AI service response: ' . json_encode($responseLog)); // 处理返回的测量数据 // 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; } }