AiMeasurementService.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. <?php
  2. namespace app\common\service;
  3. use think\Config;
  4. use think\Log;
  5. /**
  6. * AI测量服务类
  7. */
  8. class AiMeasurementService
  9. {
  10. /**
  11. * 调用第三方AI接口分析身体照片
  12. */
  13. public static function analyzBodyPhotos($photos, $gender, $height = null, $weight = null)
  14. {
  15. try {
  16. // 获取第三方API配置
  17. $config = Config::get('ai_measurement');
  18. // 验证照片
  19. $validatedPhotos = self::validatePhotos($photos);
  20. if (!$validatedPhotos) {
  21. throw new \Exception('照片验证失败,请确保上传正面、侧面、背面三张照片');
  22. }
  23. // 准备请求数据
  24. $requestData = [
  25. 'photos' => $validatedPhotos,
  26. 'gender' => $gender, // 1=男, 2=女
  27. 'height' => $height ?: null,
  28. 'weight' => $weight ?: null,
  29. 'measurement_units' => 'cm' // 测量单位
  30. ];
  31. // 调用第三方API
  32. $response = self::callThirdPartyAPI($config['api_url'], $requestData, $config);
  33. if (!$response) {
  34. throw new \Exception('第三方API调用失败');
  35. }
  36. // 解析响应数据
  37. $measurements = self::parseAPIResponse($response, $gender);
  38. // 记录调用日志
  39. Log::info('AI测量API调用成功', [
  40. 'photos_count' => count($validatedPhotos),
  41. 'gender' => $gender,
  42. 'measurements_count' => count(array_filter($measurements))
  43. ]);
  44. return $measurements;
  45. } catch (\Exception $e) {
  46. Log::error('AI测量失败: ' . $e->getMessage());
  47. // 返回默认空值
  48. return self::getDefaultMeasurements($gender);
  49. }
  50. }
  51. /**
  52. * 验证照片
  53. */
  54. private static function validatePhotos($photos)
  55. {
  56. $requiredAngles = ['front', 'side', 'back'];
  57. $validatedPhotos = [];
  58. foreach ($requiredAngles as $angle) {
  59. if (empty($photos[$angle])) {
  60. return false;
  61. }
  62. // 验证图片URL是否有效
  63. $photoUrl = $photos[$angle];
  64. if (!filter_var($photoUrl, FILTER_VALIDATE_URL) && !file_exists($photoUrl)) {
  65. return false;
  66. }
  67. $validatedPhotos[$angle] = $photoUrl;
  68. }
  69. return $validatedPhotos;
  70. }
  71. /**
  72. * 调用第三方API
  73. */
  74. private static function callThirdPartyAPI($apiUrl, $data, $config)
  75. {
  76. try {
  77. // 模拟第三方API调用
  78. // 实际项目中这里应该是真实的HTTP请求
  79. $curl = curl_init();
  80. curl_setopt_array($curl, [
  81. CURLOPT_URL => $apiUrl,
  82. CURLOPT_RETURNTRANSFER => true,
  83. CURLOPT_TIMEOUT => $config['timeout'] ?: 30,
  84. CURLOPT_POST => true,
  85. CURLOPT_POSTFIELDS => json_encode($data),
  86. CURLOPT_HTTPHEADER => [
  87. 'Content-Type: application/json',
  88. 'Authorization: Bearer ' . $config['api_key'],
  89. 'User-Agent: BodyProfile-AI-Client/1.0'
  90. ]
  91. ]);
  92. $response = curl_exec($curl);
  93. $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  94. curl_close($curl);
  95. if ($httpCode !== 200) {
  96. throw new \Exception("API请求失败,HTTP状态码: $httpCode");
  97. }
  98. $result = json_decode($response, true);
  99. if (json_last_error() !== JSON_ERROR_NONE) {
  100. throw new \Exception('API响应JSON解析失败');
  101. }
  102. return $result;
  103. } catch (\Exception $e) {
  104. Log::error('第三方API调用异常: ' . $e->getMessage());
  105. // 为了演示,返回模拟数据
  106. return self::getMockAPIResponse($data['gender']);
  107. }
  108. }
  109. /**
  110. * 解析API响应数据
  111. */
  112. private static function parseAPIResponse($response, $gender)
  113. {
  114. $measurements = self::getDefaultMeasurements($gender);
  115. if (isset($response['success']) && $response['success'] && isset($response['data'])) {
  116. $apiData = $response['data'];
  117. // 解析各项测量数据
  118. $fieldMapping = [
  119. 'chest' => ['chest', 'bust_circumference', '胸围'],
  120. 'waist' => ['waist', 'waist_circumference', '腰围'],
  121. 'hip' => ['hip', 'hip_circumference', '臀围'],
  122. 'thigh' => ['thigh', 'thigh_circumference', '大腿围'],
  123. 'calf' => ['calf', 'calf_circumference', '小腿围'],
  124. 'upper_arm' => ['upper_arm', 'arm_circumference', '上臂围'],
  125. 'forearm' => ['forearm', 'forearm_circumference', '前臂围'],
  126. 'neck' => ['neck', 'neck_circumference', '颈围'],
  127. 'shoulder_width' => ['shoulder_width', 'shoulder', '肩宽'],
  128. 'inseam' => ['inseam', 'inseam_length', '内缝长'],
  129. 'outseam' => ['outseam', 'outseam_length', '外缝长']
  130. ];
  131. // 女性专用字段
  132. if ($gender == 2) {
  133. $fieldMapping['bust'] = ['bust', 'breast_circumference', '乳围'];
  134. $fieldMapping['underbust'] = ['underbust', 'underbust_circumference', '下胸围'];
  135. }
  136. foreach ($fieldMapping as $field => $possibleKeys) {
  137. foreach ($possibleKeys as $key) {
  138. if (isset($apiData[$key]) && is_numeric($apiData[$key]) && $apiData[$key] > 0) {
  139. $measurements[$field] = round(floatval($apiData[$key]), 2);
  140. break;
  141. }
  142. }
  143. }
  144. // 处理置信度信息
  145. if (isset($apiData['confidence'])) {
  146. $measurements['_confidence'] = $apiData['confidence'];
  147. }
  148. // 处理错误信息
  149. if (isset($apiData['warnings'])) {
  150. $measurements['_warnings'] = $apiData['warnings'];
  151. }
  152. }
  153. return $measurements;
  154. }
  155. /**
  156. * 获取默认测量数据(空值)
  157. */
  158. private static function getDefaultMeasurements($gender)
  159. {
  160. $measurements = [
  161. 'chest' => null,
  162. 'waist' => null,
  163. 'hip' => null,
  164. 'thigh' => null,
  165. 'calf' => null,
  166. 'upper_arm' => null,
  167. 'forearm' => null,
  168. 'neck' => null,
  169. 'shoulder_width' => null,
  170. 'inseam' => null,
  171. 'outseam' => null,
  172. 'shoe_size' => null
  173. ];
  174. // 女性专用字段
  175. if ($gender == 2) {
  176. $measurements['bust'] = null;
  177. $measurements['underbust'] = null;
  178. }
  179. return $measurements;
  180. }
  181. /**
  182. * 生成模拟API响应(用于测试)
  183. */
  184. private static function getMockAPIResponse($gender)
  185. {
  186. // 为了演示效果,返回一些模拟数据
  187. $mockData = [
  188. 'chest' => 95.5,
  189. 'waist' => 80.3,
  190. 'hip' => 98.2,
  191. 'thigh' => 58.7,
  192. 'calf' => 37.2,
  193. 'upper_arm' => 32.1,
  194. 'shoulder_width' => 45.8,
  195. 'neck' => 38.5
  196. ];
  197. if ($gender == 2) {
  198. $mockData['bust'] = 88.5;
  199. $mockData['underbust'] = 75.2;
  200. }
  201. return [
  202. 'success' => true,
  203. 'data' => $mockData,
  204. 'confidence' => 0.85,
  205. 'message' => '分析完成'
  206. ];
  207. }
  208. /**
  209. * 保存AI测量结果
  210. */
  211. public static function saveMeasurementResult($profileId, $measurements, $photos, $confidence = null)
  212. {
  213. try {
  214. // 创建测量记录
  215. $measurementData = $measurements;
  216. $measurementData['profile_id'] = $profileId;
  217. $measurementData['measurement_date'] = time();
  218. // 保存测量数据
  219. $measurement = new \app\common\model\BodyMeasurements();
  220. $result = $measurement->save($measurementData);
  221. if (!$result) {
  222. throw new \Exception('保存测量数据失败');
  223. }
  224. // 创建AI测量记录
  225. $aiRecord = [
  226. 'profile_id' => $profileId,
  227. 'measurement_id' => $measurement->id,
  228. 'photos' => json_encode($photos),
  229. 'ai_result' => json_encode($measurements),
  230. 'confidence' => $confidence ?: 0,
  231. 'api_provider' => 'third_party_ai',
  232. 'status' => 1,
  233. 'createtime' => time()
  234. ];
  235. \think\Db::table('fa_ai_measurement_record')->insert($aiRecord);
  236. return $measurement;
  237. } catch (\Exception $e) {
  238. Log::error('保存AI测量结果失败: ' . $e->getMessage());
  239. throw $e;
  240. }
  241. }
  242. /**
  243. * 获取测量字段显示配置
  244. */
  245. public static function getMeasurementDisplayConfig($gender)
  246. {
  247. $config = [
  248. 'chest' => [
  249. 'label' => '胸围',
  250. 'position' => ['x' => 0.5, 'y' => 0.35],
  251. 'side' => 'left'
  252. ],
  253. 'waist' => [
  254. 'label' => '腰围',
  255. 'position' => ['x' => 0.5, 'y' => 0.55],
  256. 'side' => 'left'
  257. ],
  258. 'hip' => [
  259. 'label' => '臀围',
  260. 'position' => ['x' => 0.5, 'y' => 0.68],
  261. 'side' => 'left'
  262. ],
  263. 'thigh' => [
  264. 'label' => '大腿围',
  265. 'position' => ['x' => 0.3, 'y' => 0.78],
  266. 'side' => 'left'
  267. ],
  268. 'calf' => [
  269. 'label' => '小腿围',
  270. 'position' => ['x' => 0.3, 'y' => 0.9],
  271. 'side' => 'left'
  272. ],
  273. 'shoulder_width' => [
  274. 'label' => '肩宽',
  275. 'position' => ['x' => 0.8, 'y' => 0.25],
  276. 'side' => 'right'
  277. ],
  278. 'upper_arm' => [
  279. 'label' => '臂围',
  280. 'position' => ['x' => 0.8, 'y' => 0.45],
  281. 'side' => 'right'
  282. ],
  283. 'neck' => [
  284. 'label' => '颈围',
  285. 'position' => ['x' => 0.8, 'y' => 0.65],
  286. 'side' => 'right'
  287. ],
  288. 'inseam' => [
  289. 'label' => '裤长',
  290. 'position' => ['x' => 0.8, 'y' => 0.85],
  291. 'side' => 'right'
  292. ]
  293. ];
  294. // 女性专用字段
  295. if ($gender == 2) {
  296. $config['bust'] = [
  297. 'label' => '乳围',
  298. 'position' => ['x' => 0.5, 'y' => 0.3],
  299. 'side' => 'left'
  300. ];
  301. }
  302. return $config;
  303. }
  304. }