AiMeasurement.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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. /**
  16. * 开始AI身体测量分析
  17. */
  18. public function startAnalysis()
  19. {
  20. $params = $this->request->post();
  21. // 验证必要参数
  22. if (empty($params['profile_id'])) {
  23. $this->error('档案ID不能为空');
  24. }
  25. if (empty($params['photos']) || !is_array($params['photos'])) {
  26. $this->error('请上传身体照片');
  27. }
  28. try {
  29. // 验证档案归属
  30. $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
  31. ->where('user_id', $this->auth->id)
  32. ->find();
  33. if (!$profile) {
  34. $this->error('档案不存在');
  35. }
  36. // 检查是否有正在处理的任务
  37. $existingTask = \think\Db::table('fa_ai_measurement_task')
  38. ->where('profile_id', $params['profile_id'])
  39. ->where('status', 'in', [0, 1]) // 待处理或处理中
  40. ->find();
  41. if ($existingTask) {
  42. $this->error('该档案已有正在处理的AI测量任务,请稍后再试');
  43. }
  44. // 验证照片格式
  45. $requiredPhotos = ['front', 'side', 'back'];
  46. foreach ($requiredPhotos as $angle) {
  47. if (empty($params['photos'][$angle])) {
  48. $this->error("请上传{$angle}角度的身体照片");
  49. }
  50. }
  51. // 创建AI测量任务
  52. $taskData = [
  53. 'profile_id' => $params['profile_id'],
  54. 'user_id' => $this->auth->id,
  55. 'photos' => json_encode($params['photos']),
  56. 'params' => json_encode([
  57. 'gender' => $profile->gender,
  58. 'height' => $profile->height,
  59. 'weight' => $profile->weight
  60. ]),
  61. 'priority' => $params['priority'] ?? 5,
  62. 'status' => 0, // 待处理
  63. 'createtime' => time(),
  64. 'updatetime' => time()
  65. ];
  66. $taskId = \think\Db::table('fa_ai_measurement_task')->insertGetId($taskData);
  67. // 立即处理任务(也可以放到队列中异步处理)
  68. $this->processTask($taskId);
  69. $this->success('AI测量分析已开始', [
  70. 'task_id' => $taskId,
  71. 'estimated_time' => 30 // 预计处理时间(秒)
  72. ]);
  73. } catch (\Exception $e) {
  74. $this->error($e->getMessage());
  75. }
  76. }
  77. /**
  78. * 获取AI测量结果
  79. */
  80. public function getResult()
  81. {
  82. $taskId = $this->request->get('task_id/d');
  83. $profileId = $this->request->get('profile_id/d');
  84. if (!$taskId && !$profileId) {
  85. $this->error('任务ID或档案ID不能为空');
  86. }
  87. try {
  88. $query = \think\Db::table('fa_ai_measurement_task')
  89. ->where('user_id', $this->auth->id);
  90. if ($taskId) {
  91. $query->where('id', $taskId);
  92. } else {
  93. $query->where('profile_id', $profileId)->order('id DESC');
  94. }
  95. $task = $query->find();
  96. if (!$task) {
  97. $this->error('任务不存在');
  98. }
  99. // 根据任务状态返回不同结果
  100. switch ($task['status']) {
  101. case 0: // 待处理
  102. $this->success('任务排队中', [
  103. 'status' => 'pending',
  104. 'message' => '任务正在排队等待处理'
  105. ]);
  106. break;
  107. case 1: // 处理中
  108. $this->success('正在分析中', [
  109. 'status' => 'processing',
  110. 'message' => 'AI正在分析您的身体照片,请稍候...',
  111. 'progress' => $this->estimateProgress($task)
  112. ]);
  113. break;
  114. case 2: // 完成
  115. $result = json_decode($task['result'], true);
  116. $this->success('分析完成', [
  117. 'status' => 'completed',
  118. 'data' => $this->formatMeasurementResult($result, $task['profile_id'])
  119. ]);
  120. break;
  121. case 3: // 失败
  122. $this->success('分析失败', [
  123. 'status' => 'failed',
  124. 'message' => $task['error_message'] ?: '分析过程中出现错误',
  125. 'can_retry' => $task['attempts'] < $task['max_attempts']
  126. ]);
  127. break;
  128. default:
  129. $this->error('未知的任务状态');
  130. }
  131. } catch (\Exception $e) {
  132. $this->error($e->getMessage());
  133. }
  134. }
  135. /**
  136. * 保存AI测量结果
  137. */
  138. public function saveResult()
  139. {
  140. $params = $this->request->post();
  141. if (empty($params['task_id'])) {
  142. $this->error('任务ID不能为空');
  143. }
  144. try {
  145. // 获取任务信息
  146. $task = \think\Db::table('fa_ai_measurement_task')
  147. ->where('id', $params['task_id'])
  148. ->where('user_id', $this->auth->id)
  149. ->where('status', 2) // 只有完成的任务才能保存
  150. ->find();
  151. if (!$task) {
  152. $this->error('任务不存在或未完成');
  153. }
  154. $result = json_decode($task['result'], true);
  155. if (!$result || !isset($result['measurements'])) {
  156. $this->error('测量结果数据异常');
  157. }
  158. // 保存测量数据
  159. $measurement = AiMeasurementService::saveMeasurementResult(
  160. $task['profile_id'],
  161. $result['measurements'],
  162. json_decode($task['photos'], true),
  163. $result['confidence'] ?? null
  164. );
  165. $this->success('测量结果已保存', [
  166. 'measurement_id' => $measurement->id
  167. ]);
  168. } catch (\Exception $e) {
  169. $this->error($e->getMessage());
  170. }
  171. }
  172. /**
  173. * 重新分析
  174. */
  175. public function retryAnalysis()
  176. {
  177. $taskId = $this->request->post('task_id/d');
  178. if (!$taskId) {
  179. $this->error('任务ID不能为空');
  180. }
  181. try {
  182. $task = \think\Db::table('fa_ai_measurement_task')
  183. ->where('id', $taskId)
  184. ->where('user_id', $this->auth->id)
  185. ->where('status', 3) // 失败的任务
  186. ->find();
  187. if (!$task) {
  188. $this->error('任务不存在或不允许重试');
  189. }
  190. if ($task['attempts'] >= $task['max_attempts']) {
  191. $this->error('重试次数已达上限');
  192. }
  193. // 重置任务状态
  194. \think\Db::table('fa_ai_measurement_task')
  195. ->where('id', $taskId)
  196. ->update([
  197. 'status' => 0,
  198. 'error_message' => '',
  199. 'updatetime' => time()
  200. ]);
  201. // 重新处理任务
  202. $this->processTask($taskId);
  203. $this->success('已重新开始分析');
  204. } catch (\Exception $e) {
  205. $this->error($e->getMessage());
  206. }
  207. }
  208. /**
  209. * 获取测量字段配置
  210. */
  211. public function getMeasurementConfig()
  212. {
  213. $gender = $this->request->get('gender/d', 1);
  214. try {
  215. $config = AiMeasurementService::getMeasurementDisplayConfig($gender);
  216. $this->success('获取成功', $config);
  217. } catch (\Exception $e) {
  218. $this->error($e->getMessage());
  219. }
  220. }
  221. /**
  222. * 处理AI测量任务
  223. */
  224. private function processTask($taskId)
  225. {
  226. try {
  227. // 更新任务状态为处理中
  228. \think\Db::table('fa_ai_measurement_task')
  229. ->where('id', $taskId)
  230. ->update([
  231. 'status' => 1,
  232. 'started_at' => time(),
  233. 'attempts' => \think\Db::raw('attempts + 1'),
  234. 'updatetime' => time()
  235. ]);
  236. // 获取任务详情
  237. $task = \think\Db::table('fa_ai_measurement_task')->where('id', $taskId)->find();
  238. $photos = json_decode($task['photos'], true);
  239. $params = json_decode($task['params'], true);
  240. // 调用AI分析服务
  241. $measurements = AiMeasurementService::analyzBodyPhotos(
  242. $photos,
  243. $params['gender'],
  244. $params['height'],
  245. $params['weight']
  246. );
  247. // 处理结果
  248. $result = [
  249. 'measurements' => $measurements,
  250. 'confidence' => $measurements['_confidence'] ?? 0.8,
  251. 'warnings' => $measurements['_warnings'] ?? []
  252. ];
  253. // 清理内部字段
  254. unset($result['measurements']['_confidence']);
  255. unset($result['measurements']['_warnings']);
  256. // 更新任务状态为完成
  257. \think\Db::table('fa_ai_measurement_task')
  258. ->where('id', $taskId)
  259. ->update([
  260. 'status' => 2,
  261. 'result' => json_encode($result),
  262. 'completed_at' => time(),
  263. 'updatetime' => time()
  264. ]);
  265. } catch (\Exception $e) {
  266. // 更新任务状态为失败
  267. \think\Db::table('fa_ai_measurement_task')
  268. ->where('id', $taskId)
  269. ->update([
  270. 'status' => 3,
  271. 'error_message' => $e->getMessage(),
  272. 'updatetime' => time()
  273. ]);
  274. }
  275. }
  276. /**
  277. * 估算处理进度
  278. */
  279. private function estimateProgress($task)
  280. {
  281. $startTime = $task['started_at'];
  282. $currentTime = time();
  283. $elapsedTime = $currentTime - $startTime;
  284. // 假设总处理时间为30秒
  285. $totalTime = 30;
  286. $progress = min(95, ($elapsedTime / $totalTime) * 100);
  287. return round($progress);
  288. }
  289. /**
  290. * 格式化测量结果用于展示
  291. */
  292. private function formatMeasurementResult($result, $profileId)
  293. {
  294. $profile = \app\common\model\BodyProfile::find($profileId);
  295. $measurements = $result['measurements'];
  296. // 获取显示配置
  297. $displayConfig = AiMeasurementService::getMeasurementDisplayConfig($profile->gender);
  298. // 格式化数据
  299. $formattedData = [
  300. 'profile' => [
  301. 'id' => $profile->id,
  302. 'name' => $profile->profile_name,
  303. 'gender' => $profile->gender,
  304. 'height' => $profile->height,
  305. 'weight' => $profile->weight
  306. ],
  307. 'measurements' => [],
  308. 'display_config' => $displayConfig,
  309. 'confidence' => $result['confidence'] ?? 0,
  310. 'warnings' => $result['warnings'] ?? []
  311. ];
  312. // 组织测量数据
  313. foreach ($displayConfig as $field => $config) {
  314. $value = isset($measurements[$field]) && $measurements[$field] > 0
  315. ? $measurements[$field]
  316. : null;
  317. $formattedData['measurements'][$field] = [
  318. 'label' => $config['label'],
  319. 'value' => $value,
  320. 'unit' => 'cm',
  321. 'position' => $config['position'],
  322. 'side' => $config['side']
  323. ];
  324. }
  325. // 添加基础数据表格
  326. $formattedData['basic_data'] = [
  327. ['label' => '身高', 'value' => $profile->height, 'unit' => 'cm'],
  328. ['label' => '体重', 'value' => $profile->weight, 'unit' => 'kg'],
  329. ];
  330. // 添加测量数据表格
  331. $tableData = [];
  332. $fields = array_keys($measurements);
  333. $chunks = array_chunk($fields, 2);
  334. foreach ($chunks as $chunk) {
  335. $row = [];
  336. foreach ($chunk as $field) {
  337. if (isset($displayConfig[$field])) {
  338. $row[] = [
  339. 'label' => $displayConfig[$field]['label'],
  340. 'value' => $measurements[$field] ?? null,
  341. 'unit' => 'cm'
  342. ];
  343. }
  344. }
  345. if (!empty($row)) {
  346. $tableData[] = $row;
  347. }
  348. }
  349. $formattedData['table_data'] = $tableData;
  350. return $formattedData;
  351. }
  352. }