Aicompute.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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()
  13. {
  14. //1、接受参数
  15. $param = $this->request->param();
  16. if (empty($param['Gd_gdbh'])) {
  17. $this->error('工单编号错误');
  18. }
  19. $gdGdbh = trim($param['Gd_gdbh']);
  20. //2、查询工单_基本资料
  21. $OrderList = Db::name('工单_基本资料')
  22. ->field('Gd_gdbh,Gd_cpmc')
  23. ->where('Gd_gdbh', $gdGdbh)
  24. ->find();
  25. if (!$OrderList || empty(trim($OrderList['Gd_cpmc']))) {
  26. $this->error('未找到对应工单编号或产品名称为空');
  27. }
  28. $orderProductName = trim($OrderList['Gd_cpmc']);
  29. // 去除括号、空格用于匹配
  30. $orderMatchName = preg_replace('/[()()\s]/u', '', $orderProductName);
  31. // 3、读取单价库
  32. $allRates = Db::name('workshop_box_piece_rate_final')
  33. ->field('
  34. id,product_name,piece_price
  35. ')
  36. ->where('product_name', '<>', '')
  37. ->where('piece_price', '>', 0)
  38. ->order('id desc')
  39. ->limit(20)
  40. ->select();
  41. // 4、关键字匹配筛选候选数据
  42. $list = [];
  43. foreach ($allRates as $row) {
  44. $keywords = $this->extractRateKeywords($row['product_name']);
  45. // 关键字按长度倒序,优先精准长词匹配
  46. usort($keywords, function ($a, $b) {
  47. return mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8');
  48. });
  49. foreach ($keywords as $kw) {
  50. if ($kw === '') continue;
  51. // 原始名称 / 清洗后名称任一匹配即命中
  52. if (mb_strpos($orderProductName, $kw) !== false
  53. || mb_strpos($orderMatchName, $kw) !== false) {
  54. $list[] = $row;
  55. break;
  56. }
  57. }
  58. }
  59. $candidateList = !empty($list) ? $list : $allRates;
  60. // 5、组装AI提示词,去除格式化空格减少token
  61. $rateJson = json_encode($candidateList, JSON_UNESCAPED_UNICODE);
  62. $prompt = "角色:计件单价分析助手。
  63. 背景:单价表由人工维护,覆盖不全。请参考给定单价数据,评估当前工单产品最合理的计件单价(不是简单照搬某一条记录)。
  64. 【工单】
  65. 工单号:{$gdGdbh}
  66. 产品名称:{$orderProductName}
  67. 【参考单价数据】
  68. {$rateJson}
  69. 【规则】
  70. 1. 综合产品名称、系列、款式、工艺等信息,判断该工单产品应对应哪类单价最合理。
  71. 2. 参考相近产品的单价水平进行评估,给出最合理的计件单价。
  72. 3. 必须返回数字单价,不得为 null 或空。
  73. 【输出】
  74. 仅输出 JSON,无其他文字:
  75. {\"product_name\":\"{$orderProductName}\",\"piece_price\":数字}";
  76. $apiKey = 'sk-fxlawqVtbbQbNW0wInR3E4wsLo5JHozDC2XOHzMa711su6ss';
  77. $apiUrl = 'https://chatapi.onechats.top/v1/chat/completions';
  78. $modelName = 'gpt-4.1';
  79. $messages = [
  80. [
  81. 'role' => 'user',
  82. 'content' => $prompt,
  83. ],
  84. ];
  85. $postData = [
  86. 'model' => $modelName,
  87. 'messages' => $messages,
  88. 'max_tokens' => 1024,
  89. 'temperature' => 0.2,
  90. ];
  91. //CURL请求AI接口,增加超时控制
  92. $ch = curl_init($apiUrl);
  93. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  94. curl_setopt($ch, CURLOPT_POST, true);
  95. curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData, JSON_UNESCAPED_UNICODE));
  96. curl_setopt($ch, CURLOPT_HTTPHEADER, [
  97. 'Content-Type: application/json',
  98. 'Authorization: Bearer ' . $apiKey,
  99. ]);
  100. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 连接超时10秒
  101. curl_setopt($ch, CURLOPT_TIMEOUT, 30); // 整体请求超时30秒
  102. // 测试环境关闭SSL校验,生产环境注释此行开启证书校验
  103. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  104. $response = curl_exec($ch);
  105. // CURL异常捕获
  106. if (curl_errno($ch)) {
  107. $errMsg = curl_error($ch);
  108. curl_close($ch);
  109. // 此处可写入AI调用日志
  110. $this->error('AI接口请求失败:' . $errMsg);
  111. }
  112. curl_close($ch);
  113. $responseData = json_decode($response, true);
  114. if (!isset($responseData['choices'][0]['message']['content'])) {
  115. // 此处可写入AI调用日志
  116. $this->error('未能获取AI有效回复', $responseData);
  117. }
  118. // 清理markdown代码块标记
  119. $gptReply = trim($responseData['choices'][0]['message']['content']);
  120. $gptReply = preg_replace('/^```(?:json)?\s*|\s*```$/u', '', $gptReply);
  121. $result = json_decode($gptReply, true);
  122. // 校验AI返回格式与单价合法性
  123. if (!is_array($result) || !isset($result['piece_price']) || !is_numeric($result['piece_price'])) {
  124. // 此处可写入AI调用日志
  125. $this->error('AI返回数据格式异常', ['raw_reply' => $gptReply]);
  126. }
  127. $finalPrice = (float)$result['piece_price'];
  128. // // 业务价格区间风控,根据实际业务调整上下限
  129. // if ($finalPrice <= 0 || $finalPrice > 1.0) {
  130. // $this->error('AI估算单价超出业务合理区间', ['price' => $finalPrice]);
  131. // }
  132. $this->success('计算成功', [
  133. 'product_name' => $orderProductName,
  134. 'piece_price' => $finalPrice,
  135. ]);
  136. }
  137. /**
  138. * 从单价表产品名提取匹配关键字
  139. * @param string $productName 产品名称
  140. * @return array
  141. */
  142. protected function extractRateKeywords(string $productName): array
  143. {
  144. $ignoreWords = ['小盒', '条盒', '系列', '彩样', '打样', '裱上盖', '拆片', '做底'];
  145. $keywords = [];
  146. // 斜杠分割多产品名称
  147. $parts = preg_split('/[\/]/u', $productName);
  148. foreach ($parts as $part) {
  149. // 清除括号内备注内容
  150. $cleanPart = preg_replace('/[((][^))]*[))]/u', '', $part);
  151. $cleanPart = trim($cleanPart);
  152. if ($cleanPart === '') continue;
  153. // 提取品牌前缀(系列/小盒之前文字)
  154. if (preg_match('/^(.+?)(?:系列|小盒|条盒|裱|做底|\+)/u', $cleanPart, $match)) {
  155. $brand = trim($match[1]);
  156. if ($brand !== '' && !in_array($brand, $ignoreWords, true)) {
  157. $keywords[] = $brand;
  158. }
  159. }
  160. // 提取2~4位中文核心关键词
  161. if (preg_match('/^([\x{4e00}-\x{9fa5}]{2,})/u', $cleanPart, $match)) {
  162. $maxLen = min(4, mb_strlen($match[1], 'UTF-8'));
  163. for ($i = 2; $i <= $maxLen; $i++) {
  164. $kw = mb_substr($match[1], 0, $i, 'UTF-8');
  165. if (!in_array($kw, $ignoreWords, true)) {
  166. $keywords[] = $kw;
  167. }
  168. }
  169. }
  170. }
  171. // 去重返回
  172. return array_values(array_unique($keywords));
  173. }
  174. }