AiMeasurement.php 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365
  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. // 记录请求日志(不包含图片数据)
  564. // $logData = [
  565. // 'url' => $apiUrl,
  566. // 'height' => $requestData['height'],
  567. // 'image_count' => count(array_filter([
  568. // isset($requestData['image1']),
  569. // isset($requestData['image2']),
  570. // isset($requestData['image3'])
  571. // ]))
  572. // ];
  573. // 记录请求日志(包含身高和图片base64数据的前50个字符)
  574. $logData = [
  575. 'url' => $apiUrl,
  576. 'height' => $heightValue . 'cm',
  577. 'image1' => isset($requestData['image1']) ? substr($requestData['image1'], 0, 50) . '...' : null,
  578. 'image2' => isset($requestData['image2']) ? substr($requestData['image2'], 0, 50) . '...' : null,
  579. 'image3' => isset($requestData['image3']) ? substr($requestData['image3'], 0, 50) . '...' : null,
  580. 'request_data_size' => strlen(json_encode($requestData)) . ' bytes'
  581. ];
  582. $this->writeLog('info', 'Calling third party AI service', $logData);
  583. // 发送POST请求
  584. $ch = curl_init();
  585. curl_setopt_array($ch, [
  586. CURLOPT_URL => $apiUrl,
  587. CURLOPT_POST => true,
  588. CURLOPT_POSTFIELDS => json_encode($requestData),
  589. CURLOPT_RETURNTRANSFER => true,
  590. CURLOPT_TIMEOUT => $this->thirdPartyApiConfig['timeout'],
  591. CURLOPT_CONNECTTIMEOUT => $this->thirdPartyApiConfig['connect_timeout'],
  592. CURLOPT_HTTPHEADER => [
  593. 'Content-Type: application/json',
  594. 'Accept: application/json'
  595. ],
  596. CURLOPT_SSL_VERIFYPEER => false,
  597. CURLOPT_SSL_VERIFYHOST => false
  598. ]);
  599. $response = curl_exec($ch);
  600. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  601. $error = curl_error($ch);
  602. curl_close($ch);
  603. if ($error) {
  604. throw new \Exception('请求第三方AI服务失败: ' . $error);
  605. }
  606. $this->writeLog('info', 'Third party AI service response received', [
  607. 'response_size' => strlen($response) . ' bytes',
  608. 'http_code' => $httpCode,
  609. 'response_preview' => substr($response, 0, 200) . '...'
  610. ]);
  611. // 检查响应内容
  612. if (empty($response)) {
  613. throw new \Exception('第三方AI服务返回空响应');
  614. }
  615. $result = json_decode($response, true);
  616. if (json_last_error() !== JSON_ERROR_NONE) {
  617. // 对于非200状态码,尝试返回原始响应内容作为错误信息
  618. if ($httpCode !== 200) {
  619. throw new \Exception("第三方AI服务返回异常(HTTP {$httpCode}): " . substr($response, 0, 200));
  620. } else {
  621. throw new \Exception('第三方AI服务返回数据格式错误');
  622. }
  623. }
  624. // 处理各种HTTP错误状态,但先检查是否有有效的JSON响应
  625. if ($httpCode !== 200) {
  626. // 如果返回了JSON格式的错误信息,优先使用
  627. if (isset($result['state']) && $result['state'] == 201) {
  628. // 传参错误,通过processMeasurementData处理
  629. return $this->processMeasurementData($result);
  630. } elseif (isset($result['message'])) {
  631. throw new \Exception("第三方AI服务错误(HTTP {$httpCode}): " . $result['message']);
  632. } else {
  633. // 传统的HTTP错误处理
  634. if ($httpCode >= 500) {
  635. throw new \Exception('第三方AI服务内部错误: HTTP ' . $httpCode);
  636. } elseif ($httpCode >= 400) {
  637. throw new \Exception('第三方AI服务请求错误: HTTP ' . $httpCode);
  638. } else {
  639. throw new \Exception('第三方AI服务返回异常状态: HTTP ' . $httpCode);
  640. }
  641. }
  642. }
  643. // 记录API响应信息
  644. // $responseLog = [
  645. // 'http_code' => $httpCode,
  646. // 'response_size' => strlen($response) . ' bytes',
  647. // 'has_body_size' => isset($result['body_size']),
  648. // 'body_size_fields' => isset($result['body_size']) ? array_keys($result['body_size']) : [],
  649. // 'response_preview' => substr($response, 0, 200) . '...'
  650. // ];
  651. // 处理返回的测量数据
  652. // echo "<pre>";
  653. // print_r($result);
  654. // echo "</pre>";
  655. // exit;
  656. return $this->processMeasurementData($result);
  657. } catch (\Exception $e) {
  658. // 记录错误日志
  659. \think\Log::error('Third party AI service error: ' . $e->getMessage());
  660. throw $e;
  661. }
  662. // 如果执行到这里说明没有异常处理,直接返回处理结果
  663. }
  664. /**
  665. * 安全调用第三方AI服务(带异常处理和默认返回)
  666. */
  667. private function safeCallThirdPartyAiService($photos, $height)
  668. {
  669. try {
  670. return $this->callThirdPartyAiService($photos, $height);
  671. } catch (\Exception $e) {
  672. // 记录错误日志
  673. \think\Log::error('Third party AI service error, returning default data: ' . $e->getMessage());
  674. // 返回默认的空测量数据
  675. return $this->getDefaultMeasurementData();
  676. }
  677. }
  678. /**
  679. * 获取默认的空测量数据
  680. */
  681. private function getDefaultMeasurementData()
  682. {
  683. // 返回所有映射字段的空值(使用空字符串)
  684. return [
  685. 'chest'=>'', // 净胸围 → 胸围
  686. 'waist'=>'', // 净腰围 → 腰围
  687. 'hip'=>'', // 净臀围 → 实际臀围
  688. //'thigh', // 净腿根 → 大腿围
  689. 'knee'=>'', // 净膝围 → 膝围
  690. 'calf'=>'', // 净小腿围 → 小腿围
  691. 'arm_length'=>'', // 净手臂长 → 臂长
  692. 'wrist'=>'', // 净手腕围 → 手腕围
  693. 'pants_length'=>'', // 腿长 → 腿长
  694. 'belly_belt'=>'', // 净肚围 → 肚围
  695. 'shoulder_width'=>'', // 净肩宽 → 肩宽
  696. 'leg_root'=>'', // 净腿根 → 大腿围
  697. 'neck'=>'', // 净颈围 → 颈围
  698. 'inseam'=>'', // 内腿长 → 内腿长
  699. 'upper_arm'=>'', // 净上臂围 → 上臂围
  700. 'ankle'=>'', // 净脚踝围 → 脚踝围
  701. 'waist_lower'=>'', // 净小腹围 → 下腰围
  702. 'mid_waist'=>'', // 净中腰 → 中腰围
  703. '_confidence' => 0.0,
  704. 'warnings' => ['第三方AI服务暂时不可用,返回默认数据']
  705. ];
  706. }
  707. /**
  708. * 根据状态码获取错误信息
  709. */
  710. private function getStateErrorMessage($state)
  711. {
  712. $stateMessages = [
  713. 1 => '颈围计算异常',
  714. 2 => '腰围计算异常',
  715. 3 => '大腿围计算异常',
  716. 4 => '膝盖围计算异常',
  717. 5 => '胸围计算异常',
  718. 6 => '小腿围计算异常',
  719. 7 => '肚围计算异常',
  720. 8 => '臀围计算异常',
  721. 9 => '手臂长计算异常',
  722. 10 => '手腕围计算异常',
  723. 11 => '腿长计算异常、裤长计算异常、内腿长计算异常',
  724. 12 => '肩宽计算异常',
  725. 13 => '脚踝计算异常',
  726. 14 => '中腰计算异常',
  727. 15 => '小腹围计算异常',
  728. 16 => '上臂围计算异常',
  729. 101 => '图片1提取尺寸异常',
  730. 102 => '图片2提取尺寸异常',
  731. 103 => '图片3提取尺寸异常',
  732. 201 => '传参错误'
  733. ];
  734. return $stateMessages[$state] ?? "未知错误(状态码: {$state})";
  735. }
  736. /**
  737. * 测试接口 - 返回模拟的第三方API测量数据
  738. * @ApiMethod (POST)
  739. * @ApiParams (name="profile_id", type="integer", required=true, description="档案ID")
  740. */
  741. public function testMeasurementData()
  742. {
  743. $params = $this->request->post();
  744. // 验证必要参数
  745. if (empty($params['profile_id'])) {
  746. $this->error('档案ID不能为空');
  747. }
  748. // 验证档案归属
  749. $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
  750. ->where('user_id', $this->auth->id)
  751. ->find();
  752. if (!$profile) {
  753. $this->error('档案不存在');
  754. }
  755. // 模拟第三方API返回的数据
  756. $mockApiResult = [
  757. "body_size" => [
  758. "datuigen" => 56.973762220946064,
  759. "duwei" => 71.86294164495045,
  760. "jiankuan" => 44.99356951672863,
  761. "jiaohuai" => 20.995062499529606,
  762. "jingwei" => 36.973537078225604,
  763. "neitui" => 67.99506048261769,
  764. "shangbi" => 23.285375591374667,
  765. "shoubichang" => 61.1834335984307,
  766. "shouwanwei" => 16.0697059847192,
  767. "tuichang" => 73.9800462219755,
  768. "tunwei" => 90.08082593388505,
  769. "xiaofu" => 70.98010845587423,
  770. "xiaotuiwei" => 37.2761443409742,
  771. "xigai" => 34.990971006868364,
  772. "xiongwei" => 81.85738385794711,
  773. "yaowei" => 72.93800974219818,
  774. "zhongyao" => 70.99945416888724
  775. ],
  776. "image1" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  777. "image2" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  778. "image3" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  779. "message" => "sucessed",
  780. "state" => 0
  781. ];
  782. // 处理测量数据
  783. $measurements = $this->processMeasurementData($mockApiResult);
  784. // 处理结果
  785. $result = [
  786. 'measurements' => $measurements,
  787. 'confidence' => $measurements['_confidence'] ?? 0.8,
  788. 'warnings' => $measurements['warnings'] ?? []
  789. ];
  790. // 清理内部字段
  791. unset($result['measurements']['_confidence']);
  792. unset($result['measurements']['warnings']);
  793. // 格式化结果用于展示
  794. $formattedResult = $this->formatMeasurementResult($result, $params['profile_id']);
  795. $this->success('测试数据返回成功', [
  796. 'original_api_data' => $mockApiResult,
  797. 'mapped_measurements' => $result['measurements'],
  798. 'formatted_result' => $formattedResult
  799. ]);
  800. }
  801. /**
  802. * 测试错误状态处理 - 返回不同错误状态的模拟数据
  803. * @ApiMethod (POST)
  804. * @ApiParams (name="profile_id", type="integer", required=true, description="档案ID")
  805. * @ApiParams (name="test_state", type="integer", required=false, description="测试状态码(1-16, 101-103, 201)")
  806. */
  807. public function testErrorStates()
  808. {
  809. $params = $this->request->post();
  810. // 验证必要参数
  811. if (empty($params['profile_id'])) {
  812. $this->error('档案ID不能为空');
  813. }
  814. // 验证档案归属
  815. $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
  816. ->where('user_id', $this->auth->id)
  817. ->find();
  818. if (!$profile) {
  819. $this->error('档案不存在');
  820. }
  821. // 获取测试状态码
  822. $testState = intval($params['test_state'] ?? 1);
  823. // 模拟不同错误状态的API返回
  824. $mockApiResults = [
  825. // 部位计算异常(1-16)
  826. 1 => [
  827. "body_size" => [],
  828. "image1" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  829. "image2" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  830. "image3" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  831. "message" => "neck calculation error",
  832. "state" => 1
  833. ],
  834. 5 => [
  835. "body_size" => [
  836. "yaowei" => 72.93800974219818,
  837. "tunwei" => 90.08082593388505
  838. ],
  839. "image1" => "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAME",
  840. "message" => "chest calculation error",
  841. "state" => 5
  842. ],
  843. // 图片提取异常(101-103)
  844. 101 => [
  845. "body_size" => [],
  846. "message" => "image1 size extraction error",
  847. "state" => 101
  848. ],
  849. // 传参错误(201)
  850. 201 => [
  851. "body_size" => [],
  852. "message" => "parameter error",
  853. "state" => 201
  854. ]
  855. ];
  856. // 获取对应的模拟数据,如果没有则使用默认的状态1
  857. $mockApiResult = $mockApiResults[$testState] ?? $mockApiResults[1];
  858. $mockApiResult['state'] = $testState; // 确保状态码正确
  859. try {
  860. // 处理测量数据
  861. $measurements = $this->processMeasurementData($mockApiResult);
  862. $testData = [
  863. 'test_state' => $testState,
  864. 'error_message' => $this->getStateErrorMessage($testState),
  865. 'mock_api_result' => $mockApiResult,
  866. 'processed_measurements' => $measurements
  867. ];
  868. // 检查是否有错误状态
  869. $hasErrors = isset($measurements['warnings']) && !empty($measurements['warnings']);
  870. $state = $measurements['_state'] ?? 0;
  871. if ($state === 0 && !$hasErrors) {
  872. $this->success('错误状态测试成功', $testData);
  873. } else {
  874. // 有错误直接返回错误状态
  875. $errorMsg = $this->getStateErrorMessage($testState);
  876. $this->error($errorMsg, $testData);
  877. }
  878. } catch (\Exception $e) {
  879. // 对于201状态(传参错误),会抛出异常
  880. $this->error('测试异常: ' . $e->getMessage(), [
  881. 'test_state' => $testState,
  882. 'mock_api_result' => $mockApiResult
  883. ]);
  884. }
  885. }
  886. /**
  887. * 将图片URL转换为base64格式
  888. */
  889. private function convertImageToBase64($imageUrl)
  890. {
  891. try {
  892. // 如果已经是base64格式,直接返回
  893. if (strpos($imageUrl, 'data:image') === 0) {
  894. return $imageUrl;
  895. }
  896. // 如果是相对路径,转换为绝对路径
  897. if (strpos($imageUrl, 'http') !== 0) {
  898. $imageUrl = request()->domain() . $imageUrl;
  899. }
  900. // 获取图片数据
  901. $ch = curl_init();
  902. curl_setopt_array($ch, [
  903. CURLOPT_URL => $imageUrl,
  904. CURLOPT_RETURNTRANSFER => true,
  905. CURLOPT_TIMEOUT => 30,
  906. CURLOPT_CONNECTTIMEOUT => 10,
  907. CURLOPT_FOLLOWLOCATION => true,
  908. CURLOPT_SSL_VERIFYPEER => false,
  909. CURLOPT_SSL_VERIFYHOST => false,
  910. CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  911. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  912. CURLOPT_HTTPHEADER => [
  913. 'Accept: image/webp,image/apng,image/*,*/*;q=0.8',
  914. 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
  915. 'Cache-Control: no-cache',
  916. ]
  917. ]);
  918. $imageData = curl_exec($ch);
  919. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  920. $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
  921. $curlError = curl_error($ch);
  922. $effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
  923. curl_close($ch);
  924. // 详细的错误处理
  925. if ($curlError) {
  926. throw new \Exception("网络请求失败: {$curlError} (URL: {$imageUrl})");
  927. }
  928. if ($httpCode !== 200) {
  929. throw new \Exception("图片访问失败,HTTP状态码: {$httpCode} (URL: {$imageUrl})");
  930. }
  931. if (!$imageData || strlen($imageData) === 0) {
  932. throw new \Exception("图片数据为空 (URL: {$imageUrl})");
  933. }
  934. // 验证图片数据是否有效
  935. if (!@getimagesizefromstring($imageData)) {
  936. throw new \Exception("获取的数据不是有效的图片格式 (URL: {$imageUrl})");
  937. }
  938. // 确定MIME类型
  939. if (strpos($contentType, 'image/') === 0) {
  940. $mimeType = $contentType;
  941. } else {
  942. // 通过文件扩展名推断
  943. $extension = strtolower(pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_EXTENSION));
  944. $mimeTypes = [
  945. 'jpg' => 'image/jpeg',
  946. 'jpeg' => 'image/jpeg',
  947. 'png' => 'image/png',
  948. 'gif' => 'image/gif',
  949. 'webp' => 'image/webp',
  950. 'bmp' => 'image/bmp'
  951. ];
  952. $mimeType = $mimeTypes[$extension] ?? 'image/jpeg';
  953. }
  954. // 转换为base64
  955. $base64 = base64_encode($imageData);
  956. return "data:{$mimeType};base64,{$base64}";
  957. } catch (\Exception $e) {
  958. \think\Log::error('Convert image to base64 error: ' . $e->getMessage() . ' URL: ' . $imageUrl);
  959. throw new \Exception('图片转换失败: ' . $e->getMessage());
  960. }
  961. }
  962. /**
  963. * 处理第三方API返回的测量数据
  964. */
  965. private function processMeasurementData($apiResult)
  966. {
  967. $measurements = [];
  968. try {
  969. // 首先检查是否有state字段(错误状态码)
  970. if (isset($apiResult['state'])) {
  971. $state = intval($apiResult['state']);
  972. // state=0为正常,其他为异常
  973. if ($state !== 0) {
  974. $errorMsg = $this->getStateErrorMessage($state);
  975. // 如果是严重错误(传参错误),直接抛出异常
  976. if ($state == 201) {
  977. throw new \Exception($errorMsg);
  978. }
  979. // 对于部位计算异常或图片提取异常,返回部分数据和警告
  980. $measurements = $this->getDefaultMeasurementData();
  981. $measurements['_confidence'] = 0.3; // 低置信度
  982. $measurements['warnings'] = [$errorMsg];
  983. $measurements['_state'] = $state;
  984. $measurements['_message'] = $apiResult['message'] ?? '';
  985. return $measurements;
  986. }
  987. }
  988. // state=0 或没有state字段,处理正常数据
  989. // 检查返回数据结构 - 可能直接包含body_size字段
  990. if (isset($apiResult['body_size'])) {
  991. $data = $apiResult['body_size'];
  992. $hasValidData = true;
  993. } elseif (isset($apiResult['status']) && $apiResult['status'] === 'success') {
  994. $data = $apiResult['data'] ?? [];
  995. $hasValidData = true;
  996. } else {
  997. // 尝试直接使用返回的数据
  998. $data = $apiResult;
  999. $hasValidData = !empty($data) && is_array($data);
  1000. }
  1001. if ($hasValidData) {
  1002. // 映射字段名(根据第三方API返回的字段名进行映射)
  1003. $fieldMapping = [
  1004. 'xiongwei' => 'chest', // 净胸围 → 胸围
  1005. 'yaowei' => 'waist', // 净腰围 → 腰围
  1006. 'tunwei' => 'hip', // 净臀围 → 实际臀围
  1007. //'datuigen' => 'thigh', // 净腿根 → 大腿围
  1008. 'xigai' => 'knee', // 净膝围 → 膝围
  1009. 'xiaotuiwei' => 'calf', // 净小腿围 → 小腿围
  1010. 'shoubichang' => 'arm_length', // 净手臂长 → 臂长
  1011. 'shouwanwei' => 'wrist', // 净手腕围 → 手腕围
  1012. 'tuichang' => 'pants_length', // 腿长 → 腿长
  1013. 'duwei' => 'belly_belt', // 净肚围 → 肚围
  1014. 'jiankuan' => 'shoulder_width', // 净肩宽 → 肩宽
  1015. 'datuigen' => 'leg_root', // 净腿根 → 大腿围
  1016. 'jingwei' => 'neck', // 净颈围 → 颈围
  1017. 'neitui' => 'inseam', // 内腿长 → 内腿长
  1018. 'shangbi' => 'upper_arm', // 净上臂围 → 上臂围
  1019. 'jiaohuai' => 'ankle', // 净脚踝围 → 脚踝围
  1020. 'xiaofu' => 'waist_lower', // 净小腹围 → 下腰围
  1021. 'zhongyao' => 'mid_waist', // 净中腰 → 中腰围
  1022. ];
  1023. foreach ($fieldMapping as $apiField => $localField) {
  1024. if (isset($data[$apiField]) && is_numeric($data[$apiField])) {
  1025. $measurements[$localField] = round(floatval($data[$apiField]), 1);
  1026. }
  1027. }
  1028. // 设置置信度和警告信息
  1029. $measurements['_confidence'] = $apiResult['confidence'] ?? 0.8;
  1030. $measurements['warnings'] = $apiResult['warnings'] ?? [];
  1031. $measurements['_state'] = isset($apiResult['state']) ? intval($apiResult['state']) : 0;
  1032. $measurements['_message'] = $apiResult['message'] ?? 'sucessed';
  1033. // 如果没有测量数据,添加默认警告
  1034. if (count($measurements) <= 4) { // 只有_confidence, warnings, _state, _message
  1035. $measurements['warnings'][] = '第三方AI服务未返回有效的测量数据';
  1036. }
  1037. } else {
  1038. // API返回错误
  1039. $errorMsg = $apiResult['message'] ?? $apiResult['error'] ?? '第三方AI服务返回未知错误';
  1040. throw new \Exception($errorMsg);
  1041. }
  1042. } catch (\Exception $e) {
  1043. // 处理异常,返回错误信息
  1044. $measurements['_confidence'] = 0;
  1045. $measurements['warnings'] = ['数据处理失败: ' . $e->getMessage()];
  1046. }
  1047. return $measurements;
  1048. }
  1049. /**
  1050. * 处理AI测量任务
  1051. */
  1052. private function processTask($taskId)
  1053. {
  1054. try {
  1055. // 更新任务状态为处理中
  1056. \think\Db::table('fa_ai_measurement_task')
  1057. ->where('id', $taskId)
  1058. ->update([
  1059. 'status' => 1,
  1060. 'started_at' => time(),
  1061. 'attempts' => \think\Db::raw('attempts + 1'),
  1062. 'updatetime' => time()
  1063. ]);
  1064. // 获取任务详情
  1065. $task = \think\Db::table('fa_ai_measurement_task')->where('id', $taskId)->find();
  1066. $photos = json_decode($task['photos'], true);
  1067. $params = json_decode($task['params'], true);
  1068. // 安全调用第三方AI分析服务 - 确保身高为数字格式
  1069. $heightCm = is_numeric($params['height']) ? floatval($params['height']) : 0;
  1070. $measurements = $this->safeCallThirdPartyAiService(
  1071. $photos,
  1072. $heightCm
  1073. );
  1074. // 处理结果
  1075. $result = [
  1076. 'measurements' => $measurements,
  1077. 'confidence' => $measurements['_confidence'] ?? 0.8,
  1078. 'warnings' => $measurements['warnings'] ?? []
  1079. ];
  1080. // 清理内部字段
  1081. unset($result['measurements']['_confidence']);
  1082. unset($result['measurements']['warnings']);
  1083. // 更新任务状态为完成
  1084. \think\Db::table('fa_ai_measurement_task')
  1085. ->where('id', $taskId)
  1086. ->update([
  1087. 'status' => 2,
  1088. 'result' => json_encode($result),
  1089. 'completed_at' => time(),
  1090. 'updatetime' => time()
  1091. ]);
  1092. } catch (\Exception $e) {
  1093. // 更新任务状态为失败
  1094. \think\Db::table('fa_ai_measurement_task')
  1095. ->where('id', $taskId)
  1096. ->update([
  1097. 'status' => 3,
  1098. 'error_message' => $e->getMessage(),
  1099. 'updatetime' => time()
  1100. ]);
  1101. }
  1102. }
  1103. /**
  1104. * 估算处理进度
  1105. */
  1106. private function estimateProgress($task)
  1107. {
  1108. $startTime = $task['started_at'];
  1109. $currentTime = time();
  1110. $elapsedTime = $currentTime - $startTime;
  1111. // 假设总处理时间为30秒
  1112. $totalTime = 30;
  1113. $progress = min(95, ($elapsedTime / $totalTime) * 100);
  1114. return round($progress);
  1115. }
  1116. /**
  1117. * 格式化测量结果用于展示
  1118. */
  1119. private function formatMeasurementResult($result, $profileId)
  1120. {
  1121. $profile = \app\common\model\BodyProfile::find($profileId);
  1122. $measurements = $result['measurements'];
  1123. // 获取显示配置
  1124. $displayConfig = AiMeasurementService::getMeasurementDisplayConfig($profile->gender);
  1125. // 格式化数据
  1126. $formattedData = [
  1127. 'profile' => [
  1128. 'id' => $profile->id,
  1129. 'name' => $profile->profile_name,
  1130. 'gender' => $profile->gender,
  1131. 'height' => $profile->height,
  1132. 'weight' => $profile->weight
  1133. ],
  1134. 'measurements' => [],
  1135. 'display_config' => $displayConfig,
  1136. 'confidence' => $result['confidence'] ?? 0,
  1137. 'warnings' => $result['warnings'] ?? []
  1138. ];
  1139. // 组织测量数据
  1140. foreach ($displayConfig as $field => $config) {
  1141. $value = isset($measurements[$field]) && $measurements[$field] > 0
  1142. ? $measurements[$field]
  1143. : null;
  1144. $formattedData['measurements'][$field] = [
  1145. 'label' => $config['label'],
  1146. 'value' => $value,
  1147. 'unit' => 'cm',
  1148. 'position' => $config['position'],
  1149. 'side' => $config['side']
  1150. ];
  1151. }
  1152. // 添加基础数据表格
  1153. $formattedData['basic_data'] = [
  1154. ['label' => '身高', 'value' => $profile->height, 'unit' => 'cm'],
  1155. ['label' => '体重', 'value' => $profile->weight, 'unit' => 'kg'],
  1156. ];
  1157. // 添加测量数据表格
  1158. $tableData = [];
  1159. $fields = array_keys($measurements);
  1160. $chunks = array_chunk($fields, 2);
  1161. foreach ($chunks as $chunk) {
  1162. $row = [];
  1163. foreach ($chunk as $field) {
  1164. if (isset($displayConfig[$field])) {
  1165. $row[] = [
  1166. 'label' => $displayConfig[$field]['label'],
  1167. 'value' => $measurements[$field] ?? null,
  1168. 'unit' => 'cm'
  1169. ];
  1170. }
  1171. }
  1172. if (!empty($row)) {
  1173. $tableData[] = $row;
  1174. }
  1175. }
  1176. $formattedData['table_data'] = $tableData;
  1177. return $formattedData;
  1178. }
  1179. }