AiMeasurement.php 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358
  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. // AI测量日志配置
  22. private $logConfig = null;
  23. /**
  24. * 获取AI测量日志配置
  25. */
  26. private function getLogConfig()
  27. {
  28. if ($this->logConfig === null) {
  29. $log_path = RUNTIME_PATH . 'log/ai_measurement/';
  30. if (!is_dir($log_path)) {
  31. @mkdir($log_path, 0755, true);
  32. }
  33. $this->logConfig = [
  34. 'logger' => [
  35. 'enable' => true,
  36. 'file' => $log_path . 'ai_measurement.log',
  37. 'level' => config('app_debug') ? 'debug' : 'info',
  38. 'type' => 'daily',
  39. 'max_file' => 30,
  40. ]
  41. ];
  42. }
  43. return $this->logConfig;
  44. }
  45. /**
  46. * 写入AI测量日志
  47. * @param string $level 日志级别: info, error, debug, warning
  48. * @param string $message 日志消息
  49. * @param array $context 上下文数据
  50. */
  51. private function writeLog($level, $message, $context = [])
  52. {
  53. $config = $this->getLogConfig();
  54. $log_path = dirname($config['logger']['file']);
  55. $log_file = $config['logger']['file'];
  56. // 如果是daily类型,添加日期后缀
  57. if ($config['logger']['type'] === 'daily') {
  58. $log_file = $log_path . '/ai_measurement_' . date('Y-m-d') . '.log';
  59. }
  60. $log_content = [
  61. 'timestamp' => date('Y-m-d H:i:s'),
  62. 'level' => strtoupper($level),
  63. 'message' => $message,
  64. 'context' => $context
  65. ];
  66. $log_text = '[' . $log_content['timestamp'] . '] ' . $log_content['level'] . ': ' . $log_content['message'];
  67. if (!empty($context)) {
  68. $log_text .= ' ' . json_encode($context, JSON_UNESCAPED_UNICODE);
  69. }
  70. $log_text .= PHP_EOL;
  71. file_put_contents($log_file, $log_text, FILE_APPEND | LOCK_EX);
  72. }
  73. /**
  74. * 开始AI身体测量分析
  75. */
  76. public function startAnalysis()
  77. {
  78. $params = $this->request->post();
  79. // 验证必要参数
  80. if (empty($params['profile_id'])) {
  81. $this->error('档案ID不能为空');
  82. }
  83. if (empty($params['photos']) || !is_array($params['photos'])) {
  84. $this->error('请上传身体照片');
  85. }
  86. try {
  87. // 验证档案归属
  88. $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
  89. ->where('user_id', $this->auth->id)
  90. ->find();
  91. if (!$profile) {
  92. $this->error('档案不存在');
  93. }
  94. // 检查是否有正在处理的任务
  95. $existingTask = \think\Db::table('fa_ai_measurement_task')
  96. ->where('profile_id', $params['profile_id'])
  97. ->where('status', 'in', [0, 1]) // 待处理或处理中
  98. ->find();
  99. if ($existingTask) {
  100. $this->error('该档案已有正在处理的AI测量任务,请稍后再试');
  101. }
  102. // 验证照片格式
  103. $requiredPhotos = ['front', 'side', 'back'];
  104. foreach ($requiredPhotos as $angle) {
  105. if (empty($params['photos'][$angle])) {
  106. $this->error("请上传{$angle}角度的身体照片");
  107. }
  108. }
  109. // 创建AI测量任务
  110. $taskData = [
  111. 'profile_id' => $params['profile_id'],
  112. 'user_id' => $this->auth->id,
  113. 'photos' => json_encode($params['photos']),
  114. 'params' => json_encode([
  115. 'gender' => $profile->gender,
  116. 'height' => $profile->height,
  117. 'weight' => $profile->weight
  118. ]),
  119. 'priority' => $params['priority'] ?? 5,
  120. 'status' => 0, // 待处理
  121. 'createtime' => time(),
  122. 'updatetime' => time()
  123. ];
  124. $taskId = \think\Db::table('fa_ai_measurement_task')->insertGetId($taskData);
  125. // 立即处理任务(也可以放到队列中异步处理)
  126. $this->processTask($taskId);
  127. $this->success('AI测量分析已开始', [
  128. 'task_id' => $taskId,
  129. 'estimated_time' => 30 // 预计处理时间(秒)
  130. ]);
  131. } catch (\Exception $e) {
  132. $this->error($e->getMessage());
  133. }
  134. }
  135. /**
  136. * 获取AI测量结果
  137. */
  138. public function getResult()
  139. {
  140. $taskId = $this->request->get('task_id/d');
  141. $profileId = $this->request->get('profile_id/d');
  142. if (!$taskId && !$profileId) {
  143. $this->error('任务ID或档案ID不能为空');
  144. }
  145. try {
  146. $query = \think\Db::table('fa_ai_measurement_task')
  147. ->where('user_id', $this->auth->id);
  148. if ($taskId) {
  149. $query->where('id', $taskId);
  150. } else {
  151. $query->where('profile_id', $profileId)->order('id DESC');
  152. }
  153. $task = $query->find();
  154. if (!$task) {
  155. $this->error('任务不存在');
  156. }
  157. // 根据任务状态返回不同结果
  158. switch ($task['status']) {
  159. case 0: // 待处理
  160. $this->success('任务排队中', [
  161. 'status' => 'pending',
  162. 'message' => '任务正在排队等待处理'
  163. ]);
  164. break;
  165. case 1: // 处理中
  166. $this->success('正在分析中', [
  167. 'status' => 'processing',
  168. 'message' => 'AI正在分析您的身体照片,请稍候...',
  169. 'progress' => $this->estimateProgress($task)
  170. ]);
  171. break;
  172. case 2: // 完成
  173. $result = json_decode($task['result'], true);
  174. $this->success('分析完成', [
  175. 'status' => 'completed',
  176. 'data' => $this->formatMeasurementResult($result, $task['profile_id'])
  177. ]);
  178. break;
  179. case 3: // 失败
  180. $this->success('分析失败', [
  181. 'status' => 'failed',
  182. 'message' => $task['error_message'] ?: '分析过程中出现错误',
  183. 'can_retry' => $task['attempts'] < $task['max_attempts']
  184. ]);
  185. break;
  186. default:
  187. $this->error('未知的任务状态');
  188. }
  189. } catch (\Exception $e) {
  190. $this->error($e->getMessage());
  191. }
  192. }
  193. /**
  194. * 保存AI测量结果
  195. */
  196. public function saveResult()
  197. {
  198. $params = $this->request->post();
  199. if (empty($params['task_id'])) {
  200. $this->error('任务ID不能为空');
  201. }
  202. try {
  203. // 获取任务信息
  204. $task = \think\Db::table('fa_ai_measurement_task')
  205. ->where('id', $params['task_id'])
  206. ->where('user_id', $this->auth->id)
  207. ->where('status', 2) // 只有完成的任务才能保存
  208. ->find();
  209. if (!$task) {
  210. $this->error('任务不存在或未完成');
  211. }
  212. $result = json_decode($task['result'], true);
  213. if (!$result || !isset($result['measurements'])) {
  214. $this->error('测量结果数据异常');
  215. }
  216. // 保存测量数据
  217. $measurement = AiMeasurementService::saveMeasurementResult(
  218. $task['profile_id'],
  219. $result['measurements'],
  220. json_decode($task['photos'], true),
  221. $result['confidence'] ?? null
  222. );
  223. $this->success('测量结果已保存', [
  224. 'measurement_id' => $measurement->id
  225. ]);
  226. } catch (\Exception $e) {
  227. $this->error($e->getMessage());
  228. }
  229. }
  230. /**
  231. * 重新分析
  232. */
  233. public function retryAnalysis()
  234. {
  235. $taskId = $this->request->post('task_id/d');
  236. if (!$taskId) {
  237. $this->error('任务ID不能为空');
  238. }
  239. try {
  240. $task = \think\Db::table('fa_ai_measurement_task')
  241. ->where('id', $taskId)
  242. ->where('user_id', $this->auth->id)
  243. ->where('status', 3) // 失败的任务
  244. ->find();
  245. if (!$task) {
  246. $this->error('任务不存在或不允许重试');
  247. }
  248. if ($task['attempts'] >= $task['max_attempts']) {
  249. $this->error('重试次数已达上限');
  250. }
  251. // 重置任务状态
  252. \think\Db::table('fa_ai_measurement_task')
  253. ->where('id', $taskId)
  254. ->update([
  255. 'status' => 0,
  256. 'error_message' => '',
  257. 'updatetime' => time()
  258. ]);
  259. // 重新处理任务
  260. $this->processTask($taskId);
  261. $this->success('已重新开始分析');
  262. } catch (\Exception $e) {
  263. $this->error($e->getMessage());
  264. }
  265. }
  266. /**
  267. * 获取测量字段配置
  268. */
  269. public function getMeasurementConfig()
  270. {
  271. $gender = $this->request->get('gender/d', 1);
  272. try {
  273. $config = AiMeasurementService::getMeasurementDisplayConfig($gender);
  274. $this->success('获取成功', $config);
  275. } catch (\Exception $e) {
  276. $this->error($e->getMessage());
  277. }
  278. }
  279. /**
  280. * 获取AI测量素材配置
  281. * @ApiMethod (GET)
  282. * @ApiParams (name="mode", type="string", required=false, description="模式:selfie(自拍)、helper(帮拍)、common(通用)、all(全部)")
  283. */
  284. public function getMaterialConfig()
  285. {
  286. $mode = $this->request->get('mode', 'all');
  287. // try {
  288. $config = [];
  289. switch ($mode) {
  290. case 'selfie':
  291. $config = $this->getSelfieConfig();
  292. break;
  293. case 'helper':
  294. $config = $this->getHelperConfig();
  295. break;
  296. case 'common':
  297. $config = $this->getCommonConfig();
  298. break;
  299. case 'all':
  300. default:
  301. $config = [
  302. 'selfie' => $this->getSelfieConfig(),
  303. 'helper' => $this->getHelperConfig(),
  304. 'common' => $this->getCommonConfig()
  305. ];
  306. break;
  307. }
  308. $this->success('获取成功', $config);
  309. // } catch (\Exception $e) {
  310. // $this->error($e->getMessage());
  311. // }
  312. }
  313. /**
  314. * 获取自拍模式配置
  315. */
  316. private function getSelfieConfig()
  317. {
  318. return [
  319. 'enabled' => config('site.ai_measure_selfie_enabled'),
  320. 'intro_image' => $this->formatFileUrl(config('site.ai_measure_selfie_intro_image')),
  321. // 引导教程
  322. 'tutorial' => [
  323. 'images' => $this->parseImages(config('site.ai_measure_selfie_tutorial_images')),
  324. 'video' => $this->formatFileUrl(config('site.ai_measure_selfie_tutorial_video'))
  325. ],
  326. // 陀螺仪检测
  327. 'gyroscope' => [
  328. 'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_gyro_voice')),
  329. 'example' => $this->formatFileUrl(config('site.ai_measure_selfie_gyro_example'))
  330. ],
  331. // 拍摄正面
  332. 'front_shooting' => [
  333. 'frame' => $this->formatFileUrl(config('site.ai_measure_selfie_front_frame')),
  334. 'demo' => $this->formatFileUrl(config('site.ai_measure_selfie_front_demo')),
  335. 'text' => config('site.ai_measure_selfie_front_text'),
  336. 'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_front_voice'))
  337. ],
  338. // 拍摄侧面
  339. 'side_shooting' => [
  340. 'frame' => $this->formatFileUrl(config('site.ai_measure_selfie_side_frame')),
  341. 'demo' => $this->formatFileUrl(config('site.ai_measure_selfie_side_demo')),
  342. 'text' => config('site.ai_measure_selfie_side_text'),
  343. 'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_side_voice'))
  344. ],
  345. // 拍摄正面侧平举
  346. 'arms_shooting' => [
  347. 'frame' => $this->formatFileUrl(config('site.ai_measure_selfie_arms_frame')),
  348. 'demo' => $this->formatFileUrl(config('site.ai_measure_selfie_arms_demo')),
  349. 'text' => config('site.ai_measure_selfie_arms_text'),
  350. 'voice' => $this->formatFileUrl(config('site.ai_measure_selfie_arms_voice'))
  351. ],
  352. // 拍摄过程素材
  353. 'process_materials' => [
  354. 'countdown_voice' => $this->formatFileUrl(config('site.ai_measure_selfie_countdown_voice')),
  355. 'timer_sound' => $this->formatFileUrl(config('site.ai_measure_selfie_timer_sound')),
  356. 'complete_sound' => $this->formatFileUrl(config('site.ai_measure_selfie_complete_sound')),
  357. 'next_voice' => $this->formatFileUrl(config('site.ai_measure_selfie_next_voice')),
  358. 'finish_voice' => $this->formatFileUrl(config('site.ai_measure_selfie_finish_voice'))
  359. ]
  360. ];
  361. }
  362. /**
  363. * 获取帮拍模式配置
  364. */
  365. private function getHelperConfig()
  366. {
  367. return [
  368. 'enabled' => config('site.ai_measure_helper_enabled'),
  369. 'intro_image' => $this->formatFileUrl(config('site.ai_measure_helper_intro_image')),
  370. // 引导教程
  371. 'tutorial' => [
  372. 'images' => $this->parseImages(config('site.ai_measure_helper_tutorial_images')),
  373. 'video' => $this->formatFileUrl(config('site.ai_measure_helper_tutorial_video'))
  374. ],
  375. // 陀螺仪检测
  376. 'gyroscope' => [
  377. 'voice' => $this->formatFileUrl(config('site.ai_measure_helper_gyro_voice')),
  378. 'example' => $this->formatFileUrl(config('site.ai_measure_helper_gyro_example'))
  379. ],
  380. // 拍摄正面
  381. 'front_shooting' => [
  382. 'frame' => $this->formatFileUrl(config('site.ai_measure_helper_front_frame')),
  383. 'demo' => $this->formatFileUrl(config('site.ai_measure_helper_front_demo')),
  384. 'text' => config('site.ai_measure_helper_front_text'),
  385. 'voice' => $this->formatFileUrl(config('site.ai_measure_helper_front_voice'))
  386. ],
  387. // 拍摄侧面
  388. 'side_shooting' => [
  389. 'frame' => $this->formatFileUrl(config('site.ai_measure_helper_side_frame')),
  390. 'demo' => $this->formatFileUrl(config('site.ai_measure_helper_side_demo')),
  391. 'text' => config('site.ai_measure_helper_side_text'),
  392. 'voice' => $this->formatFileUrl(config('site.ai_measure_helper_side_voice'))
  393. ],
  394. // 拍摄正面侧平举
  395. 'arms_shooting' => [
  396. 'frame' => $this->formatFileUrl(config('site.ai_measure_helper_arms_frame')),
  397. 'demo' => $this->formatFileUrl(config('site.ai_measure_helper_arms_demo')),
  398. 'text' => config('site.ai_measure_helper_arms_text'),
  399. 'voice' => $this->formatFileUrl(config('site.ai_measure_helper_arms_voice'))
  400. ],
  401. // 拍摄过程素材
  402. 'process_materials' => [
  403. 'countdown_voice' => cdnurl(config('site.ai_measure_helper_countdown_voice')),
  404. 'timer_sound' => $this->formatFileUrl(config('site.ai_measure_helper_timer_sound')),
  405. 'complete_sound' => $this->formatFileUrl(config('site.ai_measure_helper_complete_sound')),
  406. 'next_voice' => $this->formatFileUrl(config('site.ai_measure_helper_next_voice')),
  407. 'finish_voice' => $this->formatFileUrl(config('site.ai_measure_helper_finish_voice'))
  408. ]
  409. ];
  410. }
  411. /**
  412. * 获取通用配置
  413. */
  414. private function getCommonConfig()
  415. {
  416. return [
  417. 'welcome_notice' => config('site.ai_measure_welcome_notice'),
  418. 'privacy_notice' => config('site.ai_measure_privacy_notice'),
  419. 'accuracy_disclaimer' => config('site.ai_measure_accuracy_disclaimer'),
  420. 'demo_images' => [
  421. 'front_demo' => $this->formatFileUrl(config('site.ai_measure_common_front_demo')),
  422. 'side_demo' => $this->formatFileUrl(config('site.ai_measure_common_side_demo')),
  423. 'arms_demo' => $this->formatFileUrl(config('site.ai_measure_common_arms_demo'))
  424. ]
  425. ];
  426. }
  427. /**
  428. * 格式化文件URL
  429. */
  430. private function formatFileUrl($url)
  431. {
  432. if (empty($url)) {
  433. return null;
  434. }
  435. return cdnurl($url);
  436. }
  437. /**
  438. * 解析图片集合
  439. */
  440. private function parseImages($images)
  441. {
  442. if (empty($images)) {
  443. return [];
  444. }
  445. // 如果是JSON格式的字符串,解析为数组
  446. if (is_string($images)) {
  447. $imageArray = json_decode($images, true);
  448. if (json_last_error() === JSON_ERROR_NONE && is_array($imageArray)) {
  449. $images = $imageArray;
  450. } else {
  451. // 如果不是JSON,可能是逗号分隔的字符串
  452. $images = explode(',', $images);
  453. }
  454. }
  455. if (!is_array($images)) {
  456. return [];
  457. }
  458. // 格式化每个图片URL
  459. return array_map(function($url) {
  460. return $this->formatFileUrl(trim($url));
  461. }, array_filter($images));
  462. }
  463. /**
  464. * 直接调用第三方AI测量服务
  465. * @ApiMethod (POST)
  466. * @ApiParams (name="profile_id", type="integer", required=true, description="档案ID")
  467. * @ApiParams (name="photos", type="object", required=true, description="身体照片对象")
  468. * @ApiParams (name="photos.front", type="string", required=true, description="正面照片URL")
  469. * @ApiParams (name="photos.side", type="string", required=true, description="侧面照片URL")
  470. * @ApiParams (name="photos.back", type="string", required=true, description="背面照片URL")
  471. */
  472. public function measurementDirect()
  473. {
  474. $params = $this->request->post();
  475. // 验证必要参数
  476. if (empty($params['profile_id'])) {
  477. $this->error('档案ID不能为空');
  478. }
  479. // if (empty($params['photos']) || !is_array($params['photos'])) {
  480. // $this->error('请上传身体照片');
  481. // }
  482. // try {
  483. // 验证档案归属
  484. $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
  485. ->where('user_id', $this->auth->id)
  486. ->find();
  487. if (!$profile) {
  488. $this->error('档案不存在');
  489. }
  490. // 验证照片格式
  491. // $requiredPhotos = ['front', 'side', 'back'];
  492. // foreach ($requiredPhotos as $angle) {
  493. // if (empty($params['photos'][$angle])) {
  494. // $this->error("请上传{$angle}角度的身体照片");
  495. // }
  496. // }
  497. // 直接使用档案的
  498. $photos = $profile->body_photos_text;
  499. // 安全调用第三方AI服务 - 确保身高为数字格式
  500. $heightCm = is_numeric($profile->height) ? floatval($profile->height) : 0;
  501. $measurements = $this->safeCallThirdPartyAiService(
  502. $photos,
  503. $heightCm
  504. );
  505. // echo "<pre>";
  506. // print_r($measurements);
  507. // echo "</pre>";
  508. // exit;
  509. // 处理结果
  510. // $result = [
  511. // 'measurements' => $measurements,
  512. // 'confidence' => $measurements['_confidence'] ?? 0.8,
  513. // 'warnings' => $measurements['warnings'] ?? []
  514. // ];
  515. // 清理内部字段
  516. // unset($result['measurements']['_confidence']);
  517. // unset($result['measurements']['warnings']);
  518. // 格式化结果用于展示
  519. //$formattedResult = $this->formatMeasurementResult($result, $params['profile_id']);
  520. $measurements['height'] = $profile->height;
  521. $measurements['weight'] = $profile->weight;
  522. // 检查是否有错误状态
  523. $hasErrors = isset($measurements['warnings']) && !empty($measurements['warnings']);
  524. $state = $measurements['_state'] ?? 0;
  525. $message = $measurements['_message'] ?? 'AI测量完成';
  526. // 构建返回数据
  527. $responseData = $measurements;
  528. // 根据状态和错误情况返回不同的消息
  529. if ($state === 0 && !$hasErrors) {
  530. $this->success('AI测量完成', $responseData);
  531. } else {
  532. // 有错误直接返回错误状态
  533. $errorMsg = $hasErrors ? implode(', ', $measurements['warnings']) : $message;
  534. $this->error($errorMsg, $responseData,3);
  535. }
  536. // } catch (\Exception $e) {
  537. // $this->error($e->getMessage());
  538. // }
  539. }
  540. /**
  541. * 调用第三方AI测量接口
  542. */
  543. private function callThirdPartyAiService($photos, $height)
  544. {
  545. // 第三方API配置
  546. $apiUrl = $this->thirdPartyApiConfig['url'];
  547. try {
  548. // 准备请求数据 - 确保身高为纯数字(厘米)
  549. $heightValue = is_numeric($height) ? floatval($height) : 0;
  550. $requestData = [
  551. 'height' => $heightValue
  552. ];
  553. // 处理照片数据 - 转换为base64格式
  554. if (isset($photos['front'])) {
  555. $requestData['image1'] = $this->convertImageToBase64($photos['front']);
  556. }
  557. if (isset($photos['side'])) {
  558. $requestData['image2'] = $this->convertImageToBase64($photos['side']);
  559. }
  560. if (isset($photos['back'])) {
  561. $requestData['image3'] = $this->convertImageToBase64($photos['back']);
  562. }
  563. // 记录完整的请求日志(包含完整的base64图片数据)
  564. $logData = [
  565. 'url' => $apiUrl,
  566. 'request_data' => $requestData
  567. ];
  568. $this->writeLog('info', 'Calling third party AI service - Full Request', $logData);
  569. // 发送POST请求
  570. $ch = curl_init();
  571. curl_setopt_array($ch, [
  572. CURLOPT_URL => $apiUrl,
  573. CURLOPT_POST => true,
  574. CURLOPT_POSTFIELDS => json_encode($requestData),
  575. CURLOPT_RETURNTRANSFER => true,
  576. CURLOPT_TIMEOUT => $this->thirdPartyApiConfig['timeout'],
  577. CURLOPT_CONNECTTIMEOUT => $this->thirdPartyApiConfig['connect_timeout'],
  578. CURLOPT_HTTPHEADER => [
  579. 'Content-Type: application/json',
  580. 'Accept: application/json'
  581. ],
  582. CURLOPT_SSL_VERIFYPEER => false,
  583. CURLOPT_SSL_VERIFYHOST => false
  584. ]);
  585. $response = curl_exec($ch);
  586. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  587. $error = curl_error($ch);
  588. curl_close($ch);
  589. if ($error) {
  590. throw new \Exception('请求第三方AI服务失败: ' . $error);
  591. }
  592. // 记录完整的响应日志
  593. $this->writeLog('info', 'Third party AI service response - Full Response', [
  594. 'http_code' => $httpCode,
  595. 'response' => $response
  596. ]);
  597. // 检查响应内容
  598. if (empty($response)) {
  599. throw new \Exception('第三方AI服务返回空响应');
  600. }
  601. $result = json_decode($response, true);
  602. if (json_last_error() !== JSON_ERROR_NONE) {
  603. // 记录JSON解析错误的完整信息
  604. $this->writeLog('error', 'Third party AI service JSON decode error', [
  605. 'http_code' => $httpCode,
  606. 'json_error' => json_last_error_msg(),
  607. 'raw_response' => $response
  608. ]);
  609. // 对于非200状态码,尝试返回原始响应内容作为错误信息
  610. if ($httpCode !== 200) {
  611. throw new \Exception("第三方AI服务返回异常(HTTP {$httpCode}): " . $response);
  612. } else {
  613. throw new \Exception('第三方AI服务返回数据格式错误: ' . json_last_error_msg());
  614. }
  615. }
  616. // 处理各种HTTP错误状态,但先检查是否有有效的JSON响应
  617. if ($httpCode !== 200) {
  618. // 记录HTTP错误的完整信息
  619. $this->writeLog('error', 'Third party AI service HTTP error', [
  620. 'http_code' => $httpCode,
  621. 'result' => $result
  622. ]);
  623. // 如果返回了JSON格式的错误信息,优先使用
  624. if (isset($result['state']) && $result['state'] == 201) {
  625. // 传参错误,通过processMeasurementData处理
  626. return $this->processMeasurementData($result);
  627. } elseif (isset($result['message'])) {
  628. throw new \Exception("第三方AI服务错误(HTTP {$httpCode}): " . $result['message']);
  629. } else {
  630. // 传统的HTTP错误处理
  631. if ($httpCode >= 500) {
  632. throw new \Exception('第三方AI服务内部错误: HTTP ' . $httpCode);
  633. } elseif ($httpCode >= 400) {
  634. throw new \Exception('第三方AI服务请求错误: HTTP ' . $httpCode);
  635. } else {
  636. throw new \Exception('第三方AI服务返回异常状态: HTTP ' . $httpCode);
  637. }
  638. }
  639. }
  640. // 记录成功响应的完整解析数据
  641. $this->writeLog('info', 'Third party AI service success - Parsed Result', [
  642. 'http_code' => $httpCode,
  643. 'result' => $result
  644. ]);
  645. // 处理返回的测量数据
  646. // echo "<pre>";
  647. // print_r($result);
  648. // echo "</pre>";
  649. // exit;
  650. return $this->processMeasurementData($result);
  651. } catch (\Exception $e) {
  652. // 记录错误日志
  653. \think\Log::error('Third party AI service error: ' . $e->getMessage());
  654. throw $e;
  655. }
  656. // 如果执行到这里说明没有异常处理,直接返回处理结果
  657. }
  658. /**
  659. * 安全调用第三方AI服务(带异常处理和默认返回)
  660. */
  661. private function safeCallThirdPartyAiService($photos, $height)
  662. {
  663. try {
  664. return $this->callThirdPartyAiService($photos, $height);
  665. } catch (\Exception $e) {
  666. // 记录错误日志
  667. \think\Log::error('Third party AI service error, returning default data: ' . $e->getMessage());
  668. // 返回默认的空测量数据
  669. return $this->getDefaultMeasurementData();
  670. }
  671. }
  672. /**
  673. * 获取默认的空测量数据
  674. */
  675. private function getDefaultMeasurementData()
  676. {
  677. // 返回所有映射字段的空值(使用空字符串)
  678. return [
  679. 'chest'=>'', // 净胸围 → 胸围
  680. 'waist'=>'', // 净腰围 → 腰围
  681. 'hip'=>'', // 净臀围 → 实际臀围
  682. //'thigh', // 净腿根 → 大腿围
  683. 'knee'=>'', // 净膝围 → 膝围
  684. 'calf'=>'', // 净小腿围 → 小腿围
  685. 'arm_length'=>'', // 净手臂长 → 臂长
  686. 'wrist'=>'', // 净手腕围 → 手腕围
  687. 'pants_length'=>'', // 腿长 → 腿长
  688. 'belly_belt'=>'', // 净肚围 → 肚围
  689. 'shoulder_width'=>'', // 净肩宽 → 肩宽
  690. 'leg_root'=>'', // 净腿根 → 大腿围
  691. 'neck'=>'', // 净颈围 → 颈围
  692. 'inseam'=>'', // 内腿长 → 内腿长
  693. 'upper_arm'=>'', // 净上臂围 → 上臂围
  694. 'ankle'=>'', // 净脚踝围 → 脚踝围
  695. 'waist_lower'=>'', // 净小腹围 → 下腰围
  696. 'mid_waist'=>'', // 净中腰 → 中腰围
  697. '_confidence' => 0.0,
  698. 'warnings' => ['第三方AI服务暂时不可用,返回默认数据']
  699. ];
  700. }
  701. /**
  702. * 根据状态码获取错误信息
  703. */
  704. private function getStateErrorMessage($state)
  705. {
  706. $stateMessages = [
  707. 1 => '颈围计算异常',
  708. 2 => '腰围计算异常',
  709. 3 => '大腿围计算异常',
  710. 4 => '膝盖围计算异常',
  711. 5 => '胸围计算异常',
  712. 6 => '小腿围计算异常',
  713. 7 => '肚围计算异常',
  714. 8 => '臀围计算异常',
  715. 9 => '手臂长计算异常',
  716. 10 => '手腕围计算异常',
  717. 11 => '腿长计算异常、裤长计算异常、内腿长计算异常',
  718. 12 => '肩宽计算异常',
  719. 13 => '脚踝计算异常',
  720. 14 => '中腰计算异常',
  721. 15 => '小腹围计算异常',
  722. 16 => '上臂围计算异常',
  723. 101 => '图片1提取尺寸异常',
  724. 102 => '图片2提取尺寸异常',
  725. 103 => '图片3提取尺寸异常',
  726. 201 => '传参错误'
  727. ];
  728. return $stateMessages[$state] ?? "未知错误(状态码: {$state})";
  729. }
  730. /**
  731. * 测试接口 - 返回模拟的第三方API测量数据
  732. * @ApiMethod (POST)
  733. * @ApiParams (name="profile_id", type="integer", required=true, description="档案ID")
  734. */
  735. public function testMeasurementData()
  736. {
  737. $params = $this->request->post();
  738. // 验证必要参数
  739. if (empty($params['profile_id'])) {
  740. $this->error('档案ID不能为空');
  741. }
  742. // 验证档案归属
  743. $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
  744. ->where('user_id', $this->auth->id)
  745. ->find();
  746. if (!$profile) {
  747. $this->error('档案不存在');
  748. }
  749. // 模拟第三方API返回的数据
  750. $mockApiResult = [
  751. "body_size" => [
  752. "datuigen" => 56.973762220946064,
  753. "duwei" => 71.86294164495045,
  754. "jiankuan" => 44.99356951672863,
  755. "jiaohuai" => 20.995062499529606,
  756. "jingwei" => 36.973537078225604,
  757. "neitui" => 67.99506048261769,
  758. "shangbi" => 23.285375591374667,
  759. "shoubichang" => 61.1834335984307,
  760. "shouwanwei" => 16.0697059847192,
  761. "tuichang" => 73.9800462219755,
  762. "tunwei" => 90.08082593388505,
  763. "xiaofu" => 70.98010845587423,
  764. "xiaotuiwei" => 37.2761443409742,
  765. "xigai" => 34.990971006868364,
  766. "xiongwei" => 81.85738385794711,
  767. "yaowei" => 72.93800974219818,
  768. "zhongyao" => 70.99945416888724
  769. ],
  770. "image1" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  771. "image2" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  772. "image3" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  773. "message" => "sucessed",
  774. "state" => 0
  775. ];
  776. // 处理测量数据
  777. $measurements = $this->processMeasurementData($mockApiResult);
  778. // 处理结果
  779. $result = [
  780. 'measurements' => $measurements,
  781. 'confidence' => $measurements['_confidence'] ?? 0.8,
  782. 'warnings' => $measurements['warnings'] ?? []
  783. ];
  784. // 清理内部字段
  785. unset($result['measurements']['_confidence']);
  786. unset($result['measurements']['warnings']);
  787. // 格式化结果用于展示
  788. $formattedResult = $this->formatMeasurementResult($result, $params['profile_id']);
  789. $this->success('测试数据返回成功', [
  790. 'original_api_data' => $mockApiResult,
  791. 'mapped_measurements' => $result['measurements'],
  792. 'formatted_result' => $formattedResult
  793. ]);
  794. }
  795. /**
  796. * 测试错误状态处理 - 返回不同错误状态的模拟数据
  797. * @ApiMethod (POST)
  798. * @ApiParams (name="profile_id", type="integer", required=true, description="档案ID")
  799. * @ApiParams (name="test_state", type="integer", required=false, description="测试状态码(1-16, 101-103, 201)")
  800. */
  801. public function testErrorStates()
  802. {
  803. $params = $this->request->post();
  804. // 验证必要参数
  805. if (empty($params['profile_id'])) {
  806. $this->error('档案ID不能为空');
  807. }
  808. // 验证档案归属
  809. $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
  810. ->where('user_id', $this->auth->id)
  811. ->find();
  812. if (!$profile) {
  813. $this->error('档案不存在');
  814. }
  815. // 获取测试状态码
  816. $testState = intval($params['test_state'] ?? 1);
  817. // 模拟不同错误状态的API返回
  818. $mockApiResults = [
  819. // 部位计算异常(1-16)
  820. 1 => [
  821. "body_size" => [],
  822. "image1" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  823. "image2" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  824. "image3" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  825. "message" => "neck calculation error",
  826. "state" => 1
  827. ],
  828. 5 => [
  829. "body_size" => [
  830. "yaowei" => 72.93800974219818,
  831. "tunwei" => 90.08082593388505
  832. ],
  833. "image1" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  834. "message" => "chest calculation error",
  835. "state" => 5
  836. ],
  837. // 图片提取异常(101-103)
  838. 101 => [
  839. "body_size" => [],
  840. "message" => "image1 size extraction error",
  841. "state" => 101
  842. ],
  843. // 传参错误(201)
  844. 201 => [
  845. "body_size" => [],
  846. "message" => "parameter error",
  847. "state" => 201
  848. ]
  849. ];
  850. // 获取对应的模拟数据,如果没有则使用默认的状态1
  851. $mockApiResult = $mockApiResults[$testState] ?? $mockApiResults[1];
  852. $mockApiResult['state'] = $testState; // 确保状态码正确
  853. try {
  854. // 处理测量数据
  855. $measurements = $this->processMeasurementData($mockApiResult);
  856. $testData = [
  857. 'test_state' => $testState,
  858. 'error_message' => $this->getStateErrorMessage($testState),
  859. 'mock_api_result' => $mockApiResult,
  860. 'processed_measurements' => $measurements
  861. ];
  862. // 检查是否有错误状态
  863. $hasErrors = isset($measurements['warnings']) && !empty($measurements['warnings']);
  864. $state = $measurements['_state'] ?? 0;
  865. if ($state === 0 && !$hasErrors) {
  866. $this->success('错误状态测试成功', $testData);
  867. } else {
  868. // 有错误直接返回错误状态
  869. $errorMsg = $this->getStateErrorMessage($testState);
  870. $this->error($errorMsg, $testData);
  871. }
  872. } catch (\Exception $e) {
  873. // 对于201状态(传参错误),会抛出异常
  874. $this->error('测试异常: ' . $e->getMessage(), [
  875. 'test_state' => $testState,
  876. 'mock_api_result' => $mockApiResult
  877. ]);
  878. }
  879. }
  880. /**
  881. * 将图片URL转换为base64格式
  882. */
  883. private function convertImageToBase64($imageUrl)
  884. {
  885. try {
  886. // 如果已经是base64格式,直接返回
  887. if (strpos($imageUrl, 'data:image') === 0) {
  888. return $imageUrl;
  889. }
  890. // 如果是相对路径,转换为绝对路径
  891. if (strpos($imageUrl, 'http') !== 0) {
  892. $imageUrl = request()->domain() . $imageUrl;
  893. }
  894. // 获取图片数据
  895. $ch = curl_init();
  896. curl_setopt_array($ch, [
  897. CURLOPT_URL => $imageUrl,
  898. CURLOPT_RETURNTRANSFER => true,
  899. CURLOPT_TIMEOUT => 30,
  900. CURLOPT_CONNECTTIMEOUT => 10,
  901. CURLOPT_FOLLOWLOCATION => true,
  902. CURLOPT_SSL_VERIFYPEER => false,
  903. CURLOPT_SSL_VERIFYHOST => false,
  904. CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  905. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  906. CURLOPT_HTTPHEADER => [
  907. 'Accept: image/webp,image/apng,image/*,*/*;q=0.8',
  908. 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
  909. 'Cache-Control: no-cache',
  910. ]
  911. ]);
  912. $imageData = curl_exec($ch);
  913. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  914. $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
  915. $curlError = curl_error($ch);
  916. $effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
  917. curl_close($ch);
  918. // 详细的错误处理
  919. if ($curlError) {
  920. throw new \Exception("网络请求失败: {$curlError} (URL: {$imageUrl})");
  921. }
  922. if ($httpCode !== 200) {
  923. throw new \Exception("图片访问失败,HTTP状态码: {$httpCode} (URL: {$imageUrl})");
  924. }
  925. if (!$imageData || strlen($imageData) === 0) {
  926. throw new \Exception("图片数据为空 (URL: {$imageUrl})");
  927. }
  928. // 验证图片数据是否有效
  929. if (!@getimagesizefromstring($imageData)) {
  930. throw new \Exception("获取的数据不是有效的图片格式 (URL: {$imageUrl})");
  931. }
  932. // 确定MIME类型
  933. if (strpos($contentType, 'image/') === 0) {
  934. $mimeType = $contentType;
  935. } else {
  936. // 通过文件扩展名推断
  937. $extension = strtolower(pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_EXTENSION));
  938. $mimeTypes = [
  939. 'jpg' => 'image/jpeg',
  940. 'jpeg' => 'image/jpeg',
  941. 'png' => 'image/png',
  942. 'gif' => 'image/gif',
  943. 'webp' => 'image/webp',
  944. 'bmp' => 'image/bmp'
  945. ];
  946. $mimeType = $mimeTypes[$extension] ?? 'image/jpeg';
  947. }
  948. // 转换为base64
  949. $base64 = base64_encode($imageData);
  950. return "data:{$mimeType};base64,{$base64}";
  951. } catch (\Exception $e) {
  952. \think\Log::error('Convert image to base64 error: ' . $e->getMessage() . ' URL: ' . $imageUrl);
  953. throw new \Exception('图片转换失败: ' . $e->getMessage());
  954. }
  955. }
  956. /**
  957. * 处理第三方API返回的测量数据
  958. */
  959. private function processMeasurementData($apiResult)
  960. {
  961. $measurements = [];
  962. try {
  963. // 首先检查是否有state字段(错误状态码)
  964. if (isset($apiResult['state'])) {
  965. $state = intval($apiResult['state']);
  966. // state=0为正常,其他为异常
  967. if ($state !== 0) {
  968. $errorMsg = $this->getStateErrorMessage($state);
  969. // 如果是严重错误(传参错误),直接抛出异常
  970. if ($state == 201) {
  971. throw new \Exception($errorMsg);
  972. }
  973. // 对于部位计算异常或图片提取异常,返回部分数据和警告
  974. $measurements = $this->getDefaultMeasurementData();
  975. $measurements['_confidence'] = 0.3; // 低置信度
  976. $measurements['warnings'] = [$errorMsg];
  977. $measurements['_state'] = $state;
  978. $measurements['_message'] = $apiResult['message'] ?? '';
  979. return $measurements;
  980. }
  981. }
  982. // state=0 或没有state字段,处理正常数据
  983. // 检查返回数据结构 - 可能直接包含body_size字段
  984. if (isset($apiResult['body_size'])) {
  985. $data = $apiResult['body_size'];
  986. $hasValidData = true;
  987. } elseif (isset($apiResult['status']) && $apiResult['status'] === 'success') {
  988. $data = $apiResult['data'] ?? [];
  989. $hasValidData = true;
  990. } else {
  991. // 尝试直接使用返回的数据
  992. $data = $apiResult;
  993. $hasValidData = !empty($data) && is_array($data);
  994. }
  995. if ($hasValidData) {
  996. // 映射字段名(根据第三方API返回的字段名进行映射)
  997. $fieldMapping = [
  998. 'xiongwei' => 'chest', // 净胸围 → 胸围
  999. 'yaowei' => 'waist', // 净腰围 → 腰围
  1000. 'tunwei' => 'hip', // 净臀围 → 实际臀围
  1001. //'datuigen' => 'thigh', // 净腿根 → 大腿围
  1002. 'xigai' => 'knee', // 净膝围 → 膝围
  1003. 'xiaotuiwei' => 'calf', // 净小腿围 → 小腿围
  1004. 'shoubichang' => 'arm_length', // 净手臂长 → 臂长
  1005. 'shouwanwei' => 'wrist', // 净手腕围 → 手腕围
  1006. 'tuichang' => 'pants_length', // 腿长 → 腿长
  1007. 'duwei' => 'belly_belt', // 净肚围 → 肚围
  1008. 'jiankuan' => 'shoulder_width', // 净肩宽 → 肩宽
  1009. 'datuigen' => 'leg_root', // 净腿根 → 大腿围
  1010. 'jingwei' => 'neck', // 净颈围 → 颈围
  1011. 'neitui' => 'inseam', // 内腿长 → 内腿长
  1012. 'shangbi' => 'upper_arm', // 净上臂围 → 上臂围
  1013. 'jiaohuai' => 'ankle', // 净脚踝围 → 脚踝围
  1014. 'xiaofu' => 'waist_lower', // 净小腹围 → 下腰围
  1015. 'zhongyao' => 'mid_waist', // 净中腰 → 中腰围
  1016. ];
  1017. foreach ($fieldMapping as $apiField => $localField) {
  1018. if (isset($data[$apiField]) && is_numeric($data[$apiField])) {
  1019. $measurements[$localField] = round(floatval($data[$apiField]), 1);
  1020. }
  1021. }
  1022. // 设置置信度和警告信息
  1023. $measurements['_confidence'] = $apiResult['confidence'] ?? 0.8;
  1024. $measurements['warnings'] = $apiResult['warnings'] ?? [];
  1025. $measurements['_state'] = isset($apiResult['state']) ? intval($apiResult['state']) : 0;
  1026. $measurements['_message'] = $apiResult['message'] ?? 'sucessed';
  1027. // 如果没有测量数据,添加默认警告
  1028. if (count($measurements) <= 4) { // 只有_confidence, warnings, _state, _message
  1029. $measurements['warnings'][] = '第三方AI服务未返回有效的测量数据';
  1030. }
  1031. } else {
  1032. // API返回错误
  1033. $errorMsg = $apiResult['message'] ?? $apiResult['error'] ?? '第三方AI服务返回未知错误';
  1034. throw new \Exception($errorMsg);
  1035. }
  1036. } catch (\Exception $e) {
  1037. // 处理异常,返回错误信息
  1038. $measurements['_confidence'] = 0;
  1039. $measurements['warnings'] = ['数据处理失败: ' . $e->getMessage()];
  1040. }
  1041. return $measurements;
  1042. }
  1043. /**
  1044. * 处理AI测量任务
  1045. */
  1046. private function processTask($taskId)
  1047. {
  1048. try {
  1049. // 更新任务状态为处理中
  1050. \think\Db::table('fa_ai_measurement_task')
  1051. ->where('id', $taskId)
  1052. ->update([
  1053. 'status' => 1,
  1054. 'started_at' => time(),
  1055. 'attempts' => \think\Db::raw('attempts + 1'),
  1056. 'updatetime' => time()
  1057. ]);
  1058. // 获取任务详情
  1059. $task = \think\Db::table('fa_ai_measurement_task')->where('id', $taskId)->find();
  1060. $photos = json_decode($task['photos'], true);
  1061. $params = json_decode($task['params'], true);
  1062. // 安全调用第三方AI分析服务 - 确保身高为数字格式
  1063. $heightCm = is_numeric($params['height']) ? floatval($params['height']) : 0;
  1064. $measurements = $this->safeCallThirdPartyAiService(
  1065. $photos,
  1066. $heightCm
  1067. );
  1068. // 处理结果
  1069. $result = [
  1070. 'measurements' => $measurements,
  1071. 'confidence' => $measurements['_confidence'] ?? 0.8,
  1072. 'warnings' => $measurements['warnings'] ?? []
  1073. ];
  1074. // 清理内部字段
  1075. unset($result['measurements']['_confidence']);
  1076. unset($result['measurements']['warnings']);
  1077. // 更新任务状态为完成
  1078. \think\Db::table('fa_ai_measurement_task')
  1079. ->where('id', $taskId)
  1080. ->update([
  1081. 'status' => 2,
  1082. 'result' => json_encode($result),
  1083. 'completed_at' => time(),
  1084. 'updatetime' => time()
  1085. ]);
  1086. } catch (\Exception $e) {
  1087. // 更新任务状态为失败
  1088. \think\Db::table('fa_ai_measurement_task')
  1089. ->where('id', $taskId)
  1090. ->update([
  1091. 'status' => 3,
  1092. 'error_message' => $e->getMessage(),
  1093. 'updatetime' => time()
  1094. ]);
  1095. }
  1096. }
  1097. /**
  1098. * 估算处理进度
  1099. */
  1100. private function estimateProgress($task)
  1101. {
  1102. $startTime = $task['started_at'];
  1103. $currentTime = time();
  1104. $elapsedTime = $currentTime - $startTime;
  1105. // 假设总处理时间为30秒
  1106. $totalTime = 30;
  1107. $progress = min(95, ($elapsedTime / $totalTime) * 100);
  1108. return round($progress);
  1109. }
  1110. /**
  1111. * 格式化测量结果用于展示
  1112. */
  1113. private function formatMeasurementResult($result, $profileId)
  1114. {
  1115. $profile = \app\common\model\BodyProfile::find($profileId);
  1116. $measurements = $result['measurements'];
  1117. // 获取显示配置
  1118. $displayConfig = AiMeasurementService::getMeasurementDisplayConfig($profile->gender);
  1119. // 格式化数据
  1120. $formattedData = [
  1121. 'profile' => [
  1122. 'id' => $profile->id,
  1123. 'name' => $profile->profile_name,
  1124. 'gender' => $profile->gender,
  1125. 'height' => $profile->height,
  1126. 'weight' => $profile->weight
  1127. ],
  1128. 'measurements' => [],
  1129. 'display_config' => $displayConfig,
  1130. 'confidence' => $result['confidence'] ?? 0,
  1131. 'warnings' => $result['warnings'] ?? []
  1132. ];
  1133. // 组织测量数据
  1134. foreach ($displayConfig as $field => $config) {
  1135. $value = isset($measurements[$field]) && $measurements[$field] > 0
  1136. ? $measurements[$field]
  1137. : null;
  1138. $formattedData['measurements'][$field] = [
  1139. 'label' => $config['label'],
  1140. 'value' => $value,
  1141. 'unit' => 'cm',
  1142. 'position' => $config['position'],
  1143. 'side' => $config['side']
  1144. ];
  1145. }
  1146. // 添加基础数据表格
  1147. $formattedData['basic_data'] = [
  1148. ['label' => '身高', 'value' => $profile->height, 'unit' => 'cm'],
  1149. ['label' => '体重', 'value' => $profile->weight, 'unit' => 'kg'],
  1150. ];
  1151. // 添加测量数据表格
  1152. $tableData = [];
  1153. $fields = array_keys($measurements);
  1154. $chunks = array_chunk($fields, 2);
  1155. foreach ($chunks as $chunk) {
  1156. $row = [];
  1157. foreach ($chunk as $field) {
  1158. if (isset($displayConfig[$field])) {
  1159. $row[] = [
  1160. 'label' => $displayConfig[$field]['label'],
  1161. 'value' => $measurements[$field] ?? null,
  1162. 'unit' => 'cm'
  1163. ];
  1164. }
  1165. }
  1166. if (!empty($row)) {
  1167. $tableData[] = $row;
  1168. }
  1169. }
  1170. $formattedData['table_data'] = $tableData;
  1171. return $formattedData;
  1172. }
  1173. }