Aicompute.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <?php
  2. namespace app\api\controller;
  3. use app\common\controller\Api;
  4. use think\Db;
  5. class Aicompute extends Api
  6. {
  7. protected $noNeedLogin = ['*'];
  8. protected $noNeedRight = ['*'];
  9. /**
  10. * AI计算产品计件单价
  11. */
  12. public function salarybatchcalc($gdbh, $jtbh)
  13. {
  14. if (empty($gdbh)) {
  15. $this->error('工单编号错误');
  16. }
  17. if (empty($jtbh)) {
  18. $this->error('机台编号错误');
  19. }
  20. $orderProductName = $this->getOrderProductName(trim($gdbh));
  21. if ($orderProductName === '') {
  22. $this->error('未找到对应工单编号或产品名称为空');
  23. }
  24. $finalPrice = $this->resolvePiecePriceByAi(trim($gdbh), trim($jtbh));
  25. if ($finalPrice === null) {
  26. $this->error('AI估算单价失败');
  27. }
  28. $this->success('计算成功', [
  29. 'product_name' => $orderProductName,
  30. 'piece_price' => $finalPrice,
  31. ]);
  32. }
  33. /**
  34. * 按机台编号获取车间单价库数据
  35. * @param string $jtbh
  36. * @return array
  37. */
  38. public function fetchWorkshopRatesByMachine($jtbh)
  39. {
  40. $jtbh = trim((string)$jtbh);
  41. if ($jtbh === '') {
  42. return [];
  43. }
  44. return Db::name('workshop_box_piece_rate_final')
  45. ->field('id,product_name,piece_price,machine_code')
  46. ->where('product_name', '<>', '')
  47. ->where('piece_price', '>', 0)
  48. ->where('machine_code', $jtbh)
  49. ->order('id desc')
  50. ->select() ?: [];
  51. }
  52. /**
  53. * 按产品名称匹配车间单价
  54. * @param array $rates
  55. * @param string $productName
  56. * @return array|null
  57. */
  58. public function matchWorkshopRateByProductName(array $rates, $productName)
  59. {
  60. $productName = trim((string)$productName);
  61. if ($productName === '' || empty($rates)) {
  62. return null;
  63. }
  64. $orderMatchName = preg_replace('/[()()\s]/u', '', $productName);
  65. foreach ($rates as $row) {
  66. $keywords = $this->extractRateKeywords($row['product_name']);
  67. usort($keywords, function ($a, $b) {
  68. return mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8');
  69. });
  70. foreach ($keywords as $kw) {
  71. if ($kw === '') {
  72. continue;
  73. }
  74. if (mb_strpos($productName, $kw) !== false
  75. || mb_strpos($orderMatchName, $kw) !== false) {
  76. return $row;
  77. }
  78. }
  79. }
  80. return null;
  81. }
  82. /**
  83. * AI对照估算计件单价
  84. * @param string $gdbh
  85. * @param string $jtbh
  86. * @return float|null
  87. */
  88. public function resolvePiecePriceByAi($gdbh, $jtbh)
  89. {
  90. $gdbh = trim((string)$gdbh);
  91. $jtbh = trim((string)$jtbh);
  92. if ($gdbh === '' || $jtbh === '') {
  93. return null;
  94. }
  95. $orderProductName = $this->getOrderProductName($gdbh);
  96. if ($orderProductName === '') {
  97. return null;
  98. }
  99. $allRates = $this->fetchWorkshopRatesByMachine($jtbh);
  100. if (empty($allRates)) {
  101. return null;
  102. }
  103. $orderMatchName = preg_replace('/[()()\s]/u', '', $orderProductName);
  104. $candidateList = $this->buildAiCandidateRates($allRates, $orderProductName, $orderMatchName);
  105. $rateJson = json_encode($candidateList, JSON_UNESCAPED_UNICODE);
  106. $prompt = "角色:计件单价分析助手。
  107. 背景:单价表由人工维护,覆盖不全。请参考给定单价数据,评估当前工单产品最合理的计件单价(不是简单照搬某一条记录)。
  108. 【工单】
  109. 工单号:{$gdbh}
  110. 产品名称:{$orderProductName}
  111. 【参考单价数据】
  112. {$rateJson}
  113. 【规则】
  114. 1. 综合产品名称、系列、款式、工艺等信息,判断该工单产品应对应哪类单价最合理。
  115. 2. 如果产品名称中包含国家名称,则全部属于外烟产品
  116. 3. 参考相近产品的单价水平进行评估,给出最合理的计件单价。
  117. 4. 必须返回数字单价,不得为 null 或空。
  118. 【输出】
  119. 仅输出 JSON,无其他文字:
  120. {\"product_name\":\"{$orderProductName}\",\"piece_price\":数字}";
  121. $apiKey = 'sk-fxlawqVtbbQbNW0wInR3E4wsLo5JHozDC2XOHzMa711su6ss';
  122. $apiUrl = 'https://chatapi.onechats.top/v1/chat/completions';
  123. $postData = [
  124. 'model' => 'gpt-4.1',
  125. 'messages' => [
  126. [
  127. 'role' => 'user',
  128. 'content' => $prompt,
  129. ],
  130. ],
  131. 'max_tokens' => 1024,
  132. 'temperature' => 0.2,
  133. ];
  134. $ch = curl_init($apiUrl);
  135. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  136. curl_setopt($ch, CURLOPT_POST, true);
  137. curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData, JSON_UNESCAPED_UNICODE));
  138. curl_setopt($ch, CURLOPT_HTTPHEADER, [
  139. 'Content-Type: application/json',
  140. 'Authorization: Bearer ' . $apiKey,
  141. ]);
  142. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
  143. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  144. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  145. $response = curl_exec($ch);
  146. if (curl_errno($ch)) {
  147. curl_close($ch);
  148. return null;
  149. }
  150. curl_close($ch);
  151. $responseData = json_decode($response, true);
  152. if (!isset($responseData['choices'][0]['message']['content'])) {
  153. return null;
  154. }
  155. $gptReply = trim($responseData['choices'][0]['message']['content']);
  156. $gptReply = preg_replace('/^```(?:json)?\s*|\s*```$/u', '', $gptReply);
  157. $result = json_decode($gptReply, true);
  158. if (!is_array($result) || !isset($result['piece_price']) || !is_numeric($result['piece_price'])) {
  159. return null;
  160. }
  161. return (float)$result['piece_price'];
  162. }
  163. /**
  164. * 获取工单产品名称
  165. * @param string $gdbh
  166. * @return string
  167. */
  168. protected function getOrderProductName($gdbh)
  169. {
  170. $order = Db::name('工单_基本资料')
  171. ->field('Gd_cpmc')
  172. ->where('Gd_gdbh', $gdbh)
  173. ->find();
  174. if (!$order || empty(trim($order['Gd_cpmc']))) {
  175. return '';
  176. }
  177. return trim($order['Gd_cpmc']);
  178. }
  179. /**
  180. * 构建AI候选单价数据
  181. * @param array $allRates
  182. * @param string $orderProductName
  183. * @param string $orderMatchName
  184. * @return array
  185. */
  186. protected function buildAiCandidateRates(array $allRates, $orderProductName, $orderMatchName)
  187. {
  188. $list = [];
  189. foreach ($allRates as $row) {
  190. $keywords = $this->extractRateKeywords($row['product_name']);
  191. usort($keywords, function ($a, $b) {
  192. return mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8');
  193. });
  194. foreach ($keywords as $kw) {
  195. if ($kw === '') {
  196. continue;
  197. }
  198. if (mb_strpos($orderProductName, $kw) !== false
  199. || mb_strpos($orderMatchName, $kw) !== false) {
  200. $list[] = $row;
  201. break;
  202. }
  203. }
  204. }
  205. return !empty($list) ? array_slice($list, 0, 20) : array_slice($allRates, 0, 20);
  206. }
  207. /**
  208. * 从单价表产品名提取匹配关键字
  209. * @param string $productName 产品名称
  210. * @return array
  211. */
  212. protected function extractRateKeywords(string $productName): array
  213. {
  214. $ignoreWords = ['小盒', '条盒', '系列', '彩样', '打样', '裱上盖', '拆片', '做底'];
  215. $keywords = [];
  216. // 斜杠分割多产品名称
  217. $parts = preg_split('/[\/]/u', $productName);
  218. foreach ($parts as $part) {
  219. // 清除括号内备注内容
  220. $cleanPart = preg_replace('/[((][^))]*[))]/u', '', $part);
  221. $cleanPart = trim($cleanPart);
  222. if ($cleanPart === '') continue;
  223. // 提取品牌前缀(系列/小盒之前文字)
  224. if (preg_match('/^(.+?)(?:系列|小盒|条盒|裱|做底|\+)/u', $cleanPart, $match)) {
  225. $brand = trim($match[1]);
  226. if ($brand !== '' && !in_array($brand, $ignoreWords, true)) {
  227. $keywords[] = $brand;
  228. }
  229. }
  230. // 提取2~4位中文核心关键词
  231. if (preg_match('/^([\x{4e00}-\x{9fa5}]{2,})/u', $cleanPart, $match)) {
  232. $maxLen = min(4, mb_strlen($match[1], 'UTF-8'));
  233. for ($i = 2; $i <= $maxLen; $i++) {
  234. $kw = mb_substr($match[1], 0, $i, 'UTF-8');
  235. if (!in_array($kw, $ignoreWords, true)) {
  236. $keywords[] = $kw;
  237. }
  238. }
  239. }
  240. }
  241. // 去重返回
  242. return array_values(array_unique($keywords));
  243. }
  244. }