AiMeasurement.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906
  1. <?php
  2. namespace app\api\controller;
  3. use app\common\controller\Api;
  4. use app\common\service\AiMeasurementService;
  5. use app\common\service\BodyProfileService;
  6. use app\api\validate\BodyProfile as BodyProfileValidate;
  7. use think\Cache;
  8. /**
  9. * AI测量API控制器
  10. */
  11. class AiMeasurement extends Api
  12. {
  13. protected $noNeedLogin = [];
  14. protected $noNeedRight = '*';
  15. // 第三方AI服务配置
  16. private $thirdPartyApiConfig = [
  17. 'url' => 'http://85cg744gf528.vicp.fun:25085/get_bodysize',
  18. 'timeout' => 120,
  19. 'connect_timeout' => 30
  20. ];
  21. /**
  22. * 开始AI身体测量分析
  23. */
  24. public function startAnalysis()
  25. {
  26. $params = $this->request->post();
  27. // 验证必要参数
  28. if (empty($params['profile_id'])) {
  29. $this->error('档案ID不能为空');
  30. }
  31. if (empty($params['photos']) || !is_array($params['photos'])) {
  32. $this->error('请上传身体照片');
  33. }
  34. try {
  35. // 验证档案归属
  36. $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
  37. ->where('user_id', $this->auth->id)
  38. ->find();
  39. if (!$profile) {
  40. $this->error('档案不存在');
  41. }
  42. // 检查是否有正在处理的任务
  43. $existingTask = \think\Db::table('fa_ai_measurement_task')
  44. ->where('profile_id', $params['profile_id'])
  45. ->where('status', 'in', [0, 1]) // 待处理或处理中
  46. ->find();
  47. if ($existingTask) {
  48. $this->error('该档案已有正在处理的AI测量任务,请稍后再试');
  49. }
  50. // 验证照片格式
  51. $requiredPhotos = ['front', 'side', 'back'];
  52. foreach ($requiredPhotos as $angle) {
  53. if (empty($params['photos'][$angle])) {
  54. $this->error("请上传{$angle}角度的身体照片");
  55. }
  56. }
  57. // 创建AI测量任务
  58. $taskData = [
  59. 'profile_id' => $params['profile_id'],
  60. 'user_id' => $this->auth->id,
  61. 'photos' => json_encode($params['photos']),
  62. 'params' => json_encode([
  63. 'gender' => $profile->gender,
  64. 'height' => $profile->height,
  65. 'weight' => $profile->weight
  66. ]),
  67. 'priority' => $params['priority'] ?? 5,
  68. 'status' => 0, // 待处理
  69. 'createtime' => time(),
  70. 'updatetime' => time()
  71. ];
  72. $taskId = \think\Db::table('fa_ai_measurement_task')->insertGetId($taskData);
  73. // 立即处理任务(也可以放到队列中异步处理)
  74. $this->processTask($taskId);
  75. $this->success('AI测量分析已开始', [
  76. 'task_id' => $taskId,
  77. 'estimated_time' => 30 // 预计处理时间(秒)
  78. ]);
  79. } catch (\Exception $e) {
  80. $this->error($e->getMessage());
  81. }
  82. }
  83. /**
  84. * 获取AI测量结果
  85. */
  86. public function getResult()
  87. {
  88. $taskId = $this->request->get('task_id/d');
  89. $profileId = $this->request->get('profile_id/d');
  90. if (!$taskId && !$profileId) {
  91. $this->error('任务ID或档案ID不能为空');
  92. }
  93. try {
  94. $query = \think\Db::table('fa_ai_measurement_task')
  95. ->where('user_id', $this->auth->id);
  96. if ($taskId) {
  97. $query->where('id', $taskId);
  98. } else {
  99. $query->where('profile_id', $profileId)->order('id DESC');
  100. }
  101. $task = $query->find();
  102. if (!$task) {
  103. $this->error('任务不存在');
  104. }
  105. // 根据任务状态返回不同结果
  106. switch ($task['status']) {
  107. case 0: // 待处理
  108. $this->success('任务排队中', [
  109. 'status' => 'pending',
  110. 'message' => '任务正在排队等待处理'
  111. ]);
  112. break;
  113. case 1: // 处理中
  114. $this->success('正在分析中', [
  115. 'status' => 'processing',
  116. 'message' => 'AI正在分析您的身体照片,请稍候...',
  117. 'progress' => $this->estimateProgress($task)
  118. ]);
  119. break;
  120. case 2: // 完成
  121. $result = json_decode($task['result'], true);
  122. $this->success('分析完成', [
  123. 'status' => 'completed',
  124. 'data' => $this->formatMeasurementResult($result, $task['profile_id'])
  125. ]);
  126. break;
  127. case 3: // 失败
  128. $this->success('分析失败', [
  129. 'status' => 'failed',
  130. 'message' => $task['error_message'] ?: '分析过程中出现错误',
  131. 'can_retry' => $task['attempts'] < $task['max_attempts']
  132. ]);
  133. break;
  134. default:
  135. $this->error('未知的任务状态');
  136. }
  137. } catch (\Exception $e) {
  138. $this->error($e->getMessage());
  139. }
  140. }
  141. /**
  142. * 保存AI测量结果
  143. */
  144. public function saveResult()
  145. {
  146. $params = $this->request->post();
  147. if (empty($params['task_id'])) {
  148. $this->error('任务ID不能为空');
  149. }
  150. try {
  151. // 获取任务信息
  152. $task = \think\Db::table('fa_ai_measurement_task')
  153. ->where('id', $params['task_id'])
  154. ->where('user_id', $this->auth->id)
  155. ->where('status', 2) // 只有完成的任务才能保存
  156. ->find();
  157. if (!$task) {
  158. $this->error('任务不存在或未完成');
  159. }
  160. $result = json_decode($task['result'], true);
  161. if (!$result || !isset($result['measurements'])) {
  162. $this->error('测量结果数据异常');
  163. }
  164. // 保存测量数据
  165. $measurement = AiMeasurementService::saveMeasurementResult(
  166. $task['profile_id'],
  167. $result['measurements'],
  168. json_decode($task['photos'], true),
  169. $result['confidence'] ?? null
  170. );
  171. $this->success('测量结果已保存', [
  172. 'measurement_id' => $measurement->id
  173. ]);
  174. } catch (\Exception $e) {
  175. $this->error($e->getMessage());
  176. }
  177. }
  178. /**
  179. * 重新分析
  180. */
  181. public function retryAnalysis()
  182. {
  183. $taskId = $this->request->post('task_id/d');
  184. if (!$taskId) {
  185. $this->error('任务ID不能为空');
  186. }
  187. try {
  188. $task = \think\Db::table('fa_ai_measurement_task')
  189. ->where('id', $taskId)
  190. ->where('user_id', $this->auth->id)
  191. ->where('status', 3) // 失败的任务
  192. ->find();
  193. if (!$task) {
  194. $this->error('任务不存在或不允许重试');
  195. }
  196. if ($task['attempts'] >= $task['max_attempts']) {
  197. $this->error('重试次数已达上限');
  198. }
  199. // 重置任务状态
  200. \think\Db::table('fa_ai_measurement_task')
  201. ->where('id', $taskId)
  202. ->update([
  203. 'status' => 0,
  204. 'error_message' => '',
  205. 'updatetime' => time()
  206. ]);
  207. // 重新处理任务
  208. $this->processTask($taskId);
  209. $this->success('已重新开始分析');
  210. } catch (\Exception $e) {
  211. $this->error($e->getMessage());
  212. }
  213. }
  214. /**
  215. * 获取测量字段配置
  216. */
  217. public function getMeasurementConfig()
  218. {
  219. $gender = $this->request->get('gender/d', 1);
  220. try {
  221. $config = AiMeasurementService::getMeasurementDisplayConfig($gender);
  222. $this->success('获取成功', $config);
  223. } catch (\Exception $e) {
  224. $this->error($e->getMessage());
  225. }
  226. }
  227. /**
  228. * 直接调用第三方AI测量服务
  229. * @ApiMethod (POST)
  230. * @ApiParams (name="profile_id", type="integer", required=true, description="档案ID")
  231. * @ApiParams (name="photos", type="object", required=true, description="身体照片对象")
  232. * @ApiParams (name="photos.front", type="string", required=true, description="正面照片URL")
  233. * @ApiParams (name="photos.side", type="string", required=true, description="侧面照片URL")
  234. * @ApiParams (name="photos.back", type="string", required=true, description="背面照片URL")
  235. */
  236. public function measurementDirect()
  237. {
  238. $params = $this->request->post();
  239. // 验证必要参数
  240. if (empty($params['profile_id'])) {
  241. $this->error('档案ID不能为空');
  242. }
  243. // if (empty($params['photos']) || !is_array($params['photos'])) {
  244. // $this->error('请上传身体照片');
  245. // }
  246. // try {
  247. // 验证档案归属
  248. $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
  249. ->where('user_id', $this->auth->id)
  250. ->find();
  251. if (!$profile) {
  252. $this->error('档案不存在');
  253. }
  254. // 验证照片格式
  255. // $requiredPhotos = ['front', 'side', 'back'];
  256. // foreach ($requiredPhotos as $angle) {
  257. // if (empty($params['photos'][$angle])) {
  258. // $this->error("请上传{$angle}角度的身体照片");
  259. // }
  260. // }
  261. // 直接使用档案的 body_photos 字段转json
  262. $photos = json_decode($profile->body_photos, true);
  263. // 安全调用第三方AI服务 - 确保身高为数字格式
  264. $heightCm = is_numeric($profile->height) ? floatval($profile->height) : 0;
  265. $measurements = $this->safeCallThirdPartyAiService(
  266. $photos,
  267. $heightCm
  268. );
  269. // echo "<pre>";
  270. // print_r($measurements);
  271. // echo "</pre>";
  272. // exit;
  273. // 处理结果
  274. // $result = [
  275. // 'measurements' => $measurements,
  276. // 'confidence' => $measurements['_confidence'] ?? 0.8,
  277. // 'warnings' => $measurements['_warnings'] ?? []
  278. // ];
  279. // 清理内部字段
  280. // unset($result['measurements']['_confidence']);
  281. // unset($result['measurements']['_warnings']);
  282. // 格式化结果用于展示
  283. //$formattedResult = $this->formatMeasurementResult($result, $params['profile_id']);
  284. $this->success('AI测量完成', $measurements);
  285. // } catch (\Exception $e) {
  286. // $this->error($e->getMessage());
  287. // }
  288. }
  289. /**
  290. * 调用第三方AI测量接口
  291. */
  292. private function callThirdPartyAiService($photos, $height)
  293. {
  294. // 第三方API配置
  295. $apiUrl = $this->thirdPartyApiConfig['url'];
  296. try {
  297. // 准备请求数据 - 确保身高为纯数字(厘米)
  298. $heightValue = is_numeric($height) ? floatval($height) : 0;
  299. $requestData = [
  300. 'height' => $heightValue
  301. ];
  302. // 处理照片数据 - 转换为base64格式
  303. if (isset($photos['front'])) {
  304. $requestData['image1'] = $this->convertImageToBase64($photos['front']);
  305. }
  306. if (isset($photos['side'])) {
  307. $requestData['image2'] = $this->convertImageToBase64($photos['side']);
  308. }
  309. if (isset($photos['back'])) {
  310. $requestData['image3'] = $this->convertImageToBase64($photos['back']);
  311. }
  312. // 记录请求日志(不包含图片数据)
  313. // $logData = [
  314. // 'url' => $apiUrl,
  315. // 'height' => $requestData['height'],
  316. // 'image_count' => count(array_filter([
  317. // isset($requestData['image1']),
  318. // isset($requestData['image2']),
  319. // isset($requestData['image3'])
  320. // ]))
  321. // ];
  322. // 记录请求日志(包含身高和图片base64数据的前50个字符)
  323. $logData = [
  324. 'url' => $apiUrl,
  325. 'height' => $heightValue . 'cm',
  326. 'image1_preview' => isset($requestData['image1']) ? substr($requestData['image1'], 0, 50) . '...' : null,
  327. 'image2_preview' => isset($requestData['image2']) ? substr($requestData['image2'], 0, 50) . '...' : null,
  328. 'image3_preview' => isset($requestData['image3']) ? substr($requestData['image3'], 0, 50) . '...' : null,
  329. 'request_data_size' => strlen(json_encode($requestData)) . ' bytes'
  330. ];
  331. \think\Log::info('Calling third party AI service: ' . json_encode($logData));
  332. // 发送POST请求
  333. $ch = curl_init();
  334. curl_setopt_array($ch, [
  335. CURLOPT_URL => $apiUrl,
  336. CURLOPT_POST => true,
  337. CURLOPT_POSTFIELDS => json_encode($requestData),
  338. CURLOPT_RETURNTRANSFER => true,
  339. CURLOPT_TIMEOUT => $this->thirdPartyApiConfig['timeout'],
  340. CURLOPT_CONNECTTIMEOUT => $this->thirdPartyApiConfig['connect_timeout'],
  341. CURLOPT_HTTPHEADER => [
  342. 'Content-Type: application/json',
  343. 'Accept: application/json'
  344. ],
  345. CURLOPT_SSL_VERIFYPEER => false,
  346. CURLOPT_SSL_VERIFYHOST => false
  347. ]);
  348. $response = curl_exec($ch);
  349. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  350. $error = curl_error($ch);
  351. curl_close($ch);
  352. if ($error) {
  353. throw new \Exception('请求第三方AI服务失败: ' . $error);
  354. }
  355. // 处理各种HTTP错误状态
  356. if ($httpCode >= 500) {
  357. throw new \Exception('第三方AI服务内部错误: HTTP ' . $httpCode);
  358. } elseif ($httpCode >= 400) {
  359. throw new \Exception('第三方AI服务请求错误: HTTP ' . $httpCode);
  360. } elseif ($httpCode !== 200) {
  361. throw new \Exception('第三方AI服务返回异常状态: HTTP ' . $httpCode);
  362. }
  363. // 检查响应内容
  364. if (empty($response)) {
  365. throw new \Exception('第三方AI服务返回空响应');
  366. }
  367. $result = json_decode($response, true);
  368. if (json_last_error() !== JSON_ERROR_NONE) {
  369. throw new \Exception('第三方AI服务返回数据格式错误');
  370. }
  371. // 记录API响应信息
  372. $responseLog = [
  373. 'http_code' => $httpCode,
  374. 'response_size' => strlen($response) . ' bytes',
  375. 'has_body_size' => isset($result['body_size']),
  376. 'body_size_fields' => isset($result['body_size']) ? array_keys($result['body_size']) : [],
  377. 'response_preview' => substr($response, 0, 200) . '...'
  378. ];
  379. \think\Log::info('Third party AI service response: ' . json_encode($responseLog));
  380. // 处理返回的测量数据
  381. // echo "<pre>";
  382. // print_r($result);
  383. // echo "</pre>";
  384. // exit;
  385. return $this->processMeasurementData($result);
  386. } catch (\Exception $e) {
  387. // 记录错误日志
  388. \think\Log::error('Third party AI service error: ' . $e->getMessage());
  389. throw $e;
  390. }
  391. // 如果执行到这里说明没有异常处理,直接返回处理结果
  392. }
  393. /**
  394. * 安全调用第三方AI服务(带异常处理和默认返回)
  395. */
  396. private function safeCallThirdPartyAiService($photos, $height)
  397. {
  398. try {
  399. return $this->callThirdPartyAiService($photos, $height);
  400. } catch (\Exception $e) {
  401. // 记录错误日志
  402. \think\Log::error('Third party AI service error, returning default data: ' . $e->getMessage());
  403. // 返回默认的空测量数据
  404. return $this->getDefaultMeasurementData();
  405. }
  406. }
  407. /**
  408. * 获取默认的空测量数据
  409. */
  410. private function getDefaultMeasurementData()
  411. {
  412. // 返回所有映射字段的空值(使用空字符串)
  413. return [
  414. 'waist' => '', // 腰围
  415. 'thigh' => '', // 大腿围
  416. 'neck' => '', // 颈围
  417. 'knee' => '', // 膝围
  418. 'chest' => '', // 胸围
  419. 'calf' => '', // 小腿围
  420. 'leg_length' => '', // 腿长
  421. 'hip' => '', // 臀围
  422. 'inner_leg' => '', // 内腿长
  423. 'hip_actual' => '', // 实际臀围
  424. 'waist_lower' => '', // 下腰围
  425. 'shoulder_width' => '', // 肩宽
  426. 'arm_length' => '', // 臂长
  427. 'wrist' => '', // 手腕围
  428. 'upper_arm' => '', // 上臂围
  429. 'mid_waist' => '', // 中腰围
  430. 'ankle' => '', // 脚踝围
  431. '_confidence' => 0.0,
  432. '_warnings' => ['第三方AI服务暂时不可用,返回默认数据']
  433. ];
  434. }
  435. /**
  436. * 测试接口 - 返回模拟的第三方API测量数据
  437. * @ApiMethod (POST)
  438. * @ApiParams (name="profile_id", type="integer", required=true, description="档案ID")
  439. */
  440. public function testMeasurementData()
  441. {
  442. $params = $this->request->post();
  443. // 验证必要参数
  444. if (empty($params['profile_id'])) {
  445. $this->error('档案ID不能为空');
  446. }
  447. // 验证档案归属
  448. $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
  449. ->where('user_id', $this->auth->id)
  450. ->find();
  451. if (!$profile) {
  452. $this->error('档案不存在');
  453. }
  454. // 模拟第三方API返回的数据
  455. $mockApiResult = [
  456. "body_size" => [
  457. "datuigen" => 56.973762220946064,
  458. "duwei" => 71.86294164495045,
  459. "jiankuan" => 44.99356951672863,
  460. "jiaohuai" => 20.995062499529606,
  461. "jingwei" => 36.973537078225604,
  462. "neitui" => 67.99506048261769,
  463. "shangbi" => 23.285375591374667,
  464. "shoubichang" => 61.1834335984307,
  465. "shouwanwei" => 16.0697059847192,
  466. "tuichang" => 73.9800462219755,
  467. "tunwei" => 90.08082593388505,
  468. "xiaofu" => 70.98010845587423,
  469. "xiaotuiwei" => 37.2761443409742,
  470. "xigai" => 34.990971006868364,
  471. "xiongwei" => 81.85738385794711,
  472. "yaowei" => 72.93800974219818,
  473. "zhongyao" => 70.99945416888724
  474. ],
  475. "confidence" => 0.85
  476. ];
  477. // 处理测量数据
  478. $measurements = $this->processMeasurementData($mockApiResult);
  479. // 处理结果
  480. $result = [
  481. 'measurements' => $measurements,
  482. 'confidence' => $measurements['_confidence'] ?? 0.8,
  483. 'warnings' => $measurements['_warnings'] ?? []
  484. ];
  485. // 清理内部字段
  486. unset($result['measurements']['_confidence']);
  487. unset($result['measurements']['_warnings']);
  488. // 格式化结果用于展示
  489. $formattedResult = $this->formatMeasurementResult($result, $params['profile_id']);
  490. $this->success('测试数据返回成功', [
  491. 'original_api_data' => $mockApiResult['body_size'],
  492. 'mapped_measurements' => $result['measurements'],
  493. 'formatted_result' => $formattedResult
  494. ]);
  495. }
  496. /**
  497. * 将图片URL转换为base64格式
  498. */
  499. private function convertImageToBase64($imageUrl)
  500. {
  501. try {
  502. // 如果已经是base64格式,直接返回
  503. if (strpos($imageUrl, 'data:image') === 0) {
  504. return $imageUrl;
  505. }
  506. // 如果是相对路径,转换为绝对路径
  507. if (strpos($imageUrl, 'http') !== 0) {
  508. $imageUrl = request()->domain() . $imageUrl;
  509. }
  510. // 获取图片数据
  511. $ch = curl_init();
  512. curl_setopt_array($ch, [
  513. CURLOPT_URL => $imageUrl,
  514. CURLOPT_RETURNTRANSFER => true,
  515. CURLOPT_TIMEOUT => 30,
  516. CURLOPT_CONNECTTIMEOUT => 10,
  517. CURLOPT_FOLLOWLOCATION => true,
  518. CURLOPT_SSL_VERIFYPEER => false,
  519. CURLOPT_SSL_VERIFYHOST => false,
  520. CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  521. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  522. CURLOPT_HTTPHEADER => [
  523. 'Accept: image/webp,image/apng,image/*,*/*;q=0.8',
  524. 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
  525. 'Cache-Control: no-cache',
  526. ]
  527. ]);
  528. $imageData = curl_exec($ch);
  529. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  530. $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
  531. $curlError = curl_error($ch);
  532. $effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
  533. curl_close($ch);
  534. // 详细的错误处理
  535. if ($curlError) {
  536. throw new \Exception("网络请求失败: {$curlError} (URL: {$imageUrl})");
  537. }
  538. if ($httpCode !== 200) {
  539. throw new \Exception("图片访问失败,HTTP状态码: {$httpCode} (URL: {$imageUrl})");
  540. }
  541. if (!$imageData || strlen($imageData) === 0) {
  542. throw new \Exception("图片数据为空 (URL: {$imageUrl})");
  543. }
  544. // 验证图片数据是否有效
  545. if (!@getimagesizefromstring($imageData)) {
  546. throw new \Exception("获取的数据不是有效的图片格式 (URL: {$imageUrl})");
  547. }
  548. // 确定MIME类型
  549. if (strpos($contentType, 'image/') === 0) {
  550. $mimeType = $contentType;
  551. } else {
  552. // 通过文件扩展名推断
  553. $extension = strtolower(pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_EXTENSION));
  554. $mimeTypes = [
  555. 'jpg' => 'image/jpeg',
  556. 'jpeg' => 'image/jpeg',
  557. 'png' => 'image/png',
  558. 'gif' => 'image/gif',
  559. 'webp' => 'image/webp',
  560. 'bmp' => 'image/bmp'
  561. ];
  562. $mimeType = $mimeTypes[$extension] ?? 'image/jpeg';
  563. }
  564. // 转换为base64
  565. $base64 = base64_encode($imageData);
  566. return "data:{$mimeType};base64,{$base64}";
  567. } catch (\Exception $e) {
  568. \think\Log::error('Convert image to base64 error: ' . $e->getMessage() . ' URL: ' . $imageUrl);
  569. throw new \Exception('图片转换失败: ' . $e->getMessage());
  570. }
  571. }
  572. /**
  573. * 处理第三方API返回的测量数据
  574. */
  575. private function processMeasurementData($apiResult)
  576. {
  577. // 根据第三方API的返回格式处理数据
  578. // 这里需要根据实际的API返回格式进行调整
  579. $measurements = [];
  580. try {
  581. // 假设API返回格式类似:
  582. // {
  583. // "status": "success",
  584. // "data": {
  585. // "chest": 95.5,
  586. // "waist": 75.2,
  587. // "hip": 98.7,
  588. // ...
  589. // },
  590. // "confidence": 0.85
  591. // }
  592. // 检查返回数据结构 - 可能直接包含body_size字段
  593. if (isset($apiResult['body_size'])) {
  594. $data = $apiResult['body_size'];
  595. $hasValidData = true;
  596. } elseif (isset($apiResult['status']) && $apiResult['status'] === 'success') {
  597. $data = $apiResult['data'] ?? [];
  598. $hasValidData = true;
  599. } else {
  600. // 尝试直接使用返回的数据
  601. $data = $apiResult;
  602. $hasValidData = !empty($data) && is_array($data);
  603. }
  604. if ($hasValidData) {
  605. // 映射字段名(根据第三方API返回的字段名进行映射)
  606. $fieldMapping = [
  607. 'yaowei' => 'waist', // 净腰围 → 腰围
  608. 'datuigen' => 'thigh', // 净腿根 → 大腿围
  609. 'jingwei' => 'neck', // 净颈围 → 颈围
  610. 'xigai' => 'knee', // 净膝围 → 膝围
  611. 'xiongwei' => 'chest', // 净胸围 → 胸围
  612. 'xiaotuiwei' => 'calf', // 净小腿围 → 小腿围
  613. 'tuichang' => 'leg_length', // 腿长 → 腿长
  614. 'duwei' => 'hip', // 净肚围 → 臀围
  615. 'neitui' => 'inner_leg', // 内腿长 → 内腿长
  616. 'tunwei' => 'hip_actual', // 净臀围 → 实际臀围
  617. 'xiaofu' => 'waist_lower', // 净小腹围 → 下腰围
  618. 'jiankuan' => 'shoulder_width', // 净肩宽 → 肩宽
  619. 'shoubichang' => 'arm_length', // 净手臂长 → 臂长
  620. 'shouwanwei' => 'wrist', // 净手腕围 → 手腕围
  621. 'shangbi' => 'upper_arm', // 净上臂围 → 上臂围
  622. 'zhongyao' => 'mid_waist', // 净中腰 → 中腰围
  623. 'jiaohuai' => 'ankle', // 净脚踝围 → 脚踝围
  624. ];
  625. foreach ($fieldMapping as $apiField => $localField) {
  626. if (isset($data[$apiField]) && is_numeric($data[$apiField])) {
  627. $measurements[$localField] = round(floatval($data[$apiField]), 1);
  628. }
  629. }
  630. // 设置置信度和警告信息
  631. $measurements['_confidence'] = $apiResult['confidence'] ?? 0.8;
  632. $measurements['_warnings'] = $apiResult['warnings'] ?? [];
  633. // 如果没有测量数据,添加默认警告
  634. if (count($measurements) === 2) { // 只有_confidence和_warnings
  635. $measurements['_warnings'][] = '第三方AI服务未返回有效的测量数据';
  636. }
  637. } else {
  638. // API返回错误
  639. $errorMsg = $apiResult['message'] ?? $apiResult['error'] ?? '第三方AI服务返回未知错误';
  640. throw new \Exception($errorMsg);
  641. }
  642. } catch (\Exception $e) {
  643. // 处理异常,返回错误信息
  644. $measurements['_confidence'] = 0;
  645. $measurements['_warnings'] = ['数据处理失败: ' . $e->getMessage()];
  646. }
  647. return $measurements;
  648. }
  649. /**
  650. * 处理AI测量任务
  651. */
  652. private function processTask($taskId)
  653. {
  654. try {
  655. // 更新任务状态为处理中
  656. \think\Db::table('fa_ai_measurement_task')
  657. ->where('id', $taskId)
  658. ->update([
  659. 'status' => 1,
  660. 'started_at' => time(),
  661. 'attempts' => \think\Db::raw('attempts + 1'),
  662. 'updatetime' => time()
  663. ]);
  664. // 获取任务详情
  665. $task = \think\Db::table('fa_ai_measurement_task')->where('id', $taskId)->find();
  666. $photos = json_decode($task['photos'], true);
  667. $params = json_decode($task['params'], true);
  668. // 安全调用第三方AI分析服务 - 确保身高为数字格式
  669. $heightCm = is_numeric($params['height']) ? floatval($params['height']) : 0;
  670. $measurements = $this->safeCallThirdPartyAiService(
  671. $photos,
  672. $heightCm
  673. );
  674. // 处理结果
  675. $result = [
  676. 'measurements' => $measurements,
  677. 'confidence' => $measurements['_confidence'] ?? 0.8,
  678. 'warnings' => $measurements['_warnings'] ?? []
  679. ];
  680. // 清理内部字段
  681. unset($result['measurements']['_confidence']);
  682. unset($result['measurements']['_warnings']);
  683. // 更新任务状态为完成
  684. \think\Db::table('fa_ai_measurement_task')
  685. ->where('id', $taskId)
  686. ->update([
  687. 'status' => 2,
  688. 'result' => json_encode($result),
  689. 'completed_at' => time(),
  690. 'updatetime' => time()
  691. ]);
  692. } catch (\Exception $e) {
  693. // 更新任务状态为失败
  694. \think\Db::table('fa_ai_measurement_task')
  695. ->where('id', $taskId)
  696. ->update([
  697. 'status' => 3,
  698. 'error_message' => $e->getMessage(),
  699. 'updatetime' => time()
  700. ]);
  701. }
  702. }
  703. /**
  704. * 估算处理进度
  705. */
  706. private function estimateProgress($task)
  707. {
  708. $startTime = $task['started_at'];
  709. $currentTime = time();
  710. $elapsedTime = $currentTime - $startTime;
  711. // 假设总处理时间为30秒
  712. $totalTime = 30;
  713. $progress = min(95, ($elapsedTime / $totalTime) * 100);
  714. return round($progress);
  715. }
  716. /**
  717. * 格式化测量结果用于展示
  718. */
  719. private function formatMeasurementResult($result, $profileId)
  720. {
  721. $profile = \app\common\model\BodyProfile::find($profileId);
  722. $measurements = $result['measurements'];
  723. // 获取显示配置
  724. $displayConfig = AiMeasurementService::getMeasurementDisplayConfig($profile->gender);
  725. // 格式化数据
  726. $formattedData = [
  727. 'profile' => [
  728. 'id' => $profile->id,
  729. 'name' => $profile->profile_name,
  730. 'gender' => $profile->gender,
  731. 'height' => $profile->height,
  732. 'weight' => $profile->weight
  733. ],
  734. 'measurements' => [],
  735. 'display_config' => $displayConfig,
  736. 'confidence' => $result['confidence'] ?? 0,
  737. 'warnings' => $result['warnings'] ?? []
  738. ];
  739. // 组织测量数据
  740. foreach ($displayConfig as $field => $config) {
  741. $value = isset($measurements[$field]) && $measurements[$field] > 0
  742. ? $measurements[$field]
  743. : null;
  744. $formattedData['measurements'][$field] = [
  745. 'label' => $config['label'],
  746. 'value' => $value,
  747. 'unit' => 'cm',
  748. 'position' => $config['position'],
  749. 'side' => $config['side']
  750. ];
  751. }
  752. // 添加基础数据表格
  753. $formattedData['basic_data'] = [
  754. ['label' => '身高', 'value' => $profile->height, 'unit' => 'cm'],
  755. ['label' => '体重', 'value' => $profile->weight, 'unit' => 'kg'],
  756. ];
  757. // 添加测量数据表格
  758. $tableData = [];
  759. $fields = array_keys($measurements);
  760. $chunks = array_chunk($fields, 2);
  761. foreach ($chunks as $chunk) {
  762. $row = [];
  763. foreach ($chunk as $field) {
  764. if (isset($displayConfig[$field])) {
  765. $row[] = [
  766. 'label' => $displayConfig[$field]['label'],
  767. 'value' => $measurements[$field] ?? null,
  768. 'unit' => 'cm'
  769. ];
  770. }
  771. }
  772. if (!empty($row)) {
  773. $tableData[] = $row;
  774. }
  775. }
  776. $formattedData['table_data'] = $tableData;
  777. return $formattedData;
  778. }
  779. }