|
|
@@ -0,0 +1,191 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace app\api\controller;
|
|
|
+
|
|
|
+use app\common\controller\Api;
|
|
|
+use think\Db;
|
|
|
+
|
|
|
+class Aicompute extends Api
|
|
|
+{
|
|
|
+ protected $noNeedLogin = ['*'];
|
|
|
+ protected $noNeedRight = ['*'];
|
|
|
+
|
|
|
+ /**
|
|
|
+ * AI计算产品计件单价
|
|
|
+ */
|
|
|
+ public function salarybatchcalc()
|
|
|
+ {
|
|
|
+ //1、接受参数
|
|
|
+ $param = $this->request->param();
|
|
|
+ if (empty($param['Gd_gdbh'])) {
|
|
|
+ $this->error('工单编号错误');
|
|
|
+ }
|
|
|
+ $gdGdbh = trim($param['Gd_gdbh']);
|
|
|
+
|
|
|
+ //2、查询工单_基本资料
|
|
|
+ $OrderList = Db::name('工单_基本资料')
|
|
|
+ ->field('Gd_gdbh,Gd_cpmc')
|
|
|
+ ->where('Gd_gdbh', $gdGdbh)
|
|
|
+ ->find();
|
|
|
+ if (!$OrderList || empty(trim($OrderList['Gd_cpmc']))) {
|
|
|
+ $this->error('未找到对应工单编号或产品名称为空');
|
|
|
+ }
|
|
|
+ $orderProductName = trim($OrderList['Gd_cpmc']);
|
|
|
+ // 去除括号、空格用于匹配
|
|
|
+ $orderMatchName = preg_replace('/[()()\s]/u', '', $orderProductName);
|
|
|
+
|
|
|
+ // 3、读取单价库
|
|
|
+ $allRates = Db::name('workshop_box_piece_rate_final')
|
|
|
+ ->field('
|
|
|
+ id,product_name,piece_price
|
|
|
+ ')
|
|
|
+ ->where('product_name', '<>', '')
|
|
|
+ ->where('piece_price', '>', 0)
|
|
|
+ ->order('id desc')
|
|
|
+ ->limit(20)
|
|
|
+ ->select();
|
|
|
+
|
|
|
+ // 4、关键字匹配筛选候选数据
|
|
|
+ $list = [];
|
|
|
+ foreach ($allRates as $row) {
|
|
|
+ $keywords = $this->extractRateKeywords($row['product_name']);
|
|
|
+ // 关键字按长度倒序,优先精准长词匹配
|
|
|
+ usort($keywords, function ($a, $b) {
|
|
|
+ return mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8');
|
|
|
+ });
|
|
|
+ foreach ($keywords as $kw) {
|
|
|
+ if ($kw === '') continue;
|
|
|
+ // 原始名称 / 清洗后名称任一匹配即命中
|
|
|
+ if (mb_strpos($orderProductName, $kw) !== false
|
|
|
+ || mb_strpos($orderMatchName, $kw) !== false) {
|
|
|
+ $list[] = $row;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $candidateList = !empty($list) ? $list : $allRates;
|
|
|
+
|
|
|
+ // 5、组装AI提示词,去除格式化空格减少token
|
|
|
+ $rateJson = json_encode($candidateList, JSON_UNESCAPED_UNICODE);
|
|
|
+ $prompt = "角色:计件单价分析助手。
|
|
|
+ 背景:单价表由人工维护,覆盖不全。请参考给定单价数据,评估当前工单产品最合理的计件单价(不是简单照搬某一条记录)。
|
|
|
+ 【工单】
|
|
|
+ 工单号:{$gdGdbh}
|
|
|
+ 产品名称:{$orderProductName}
|
|
|
+ 【参考单价数据】
|
|
|
+ {$rateJson}
|
|
|
+ 【规则】
|
|
|
+ 1. 综合产品名称、系列、款式、工艺等信息,判断该工单产品应对应哪类单价最合理。
|
|
|
+ 2. 参考相近产品的单价水平进行评估,给出最合理的计件单价。
|
|
|
+ 3. 必须返回数字单价,不得为 null 或空。
|
|
|
+ 【输出】
|
|
|
+ 仅输出 JSON,无其他文字:
|
|
|
+ {\"product_name\":\"{$orderProductName}\",\"piece_price\":数字}";
|
|
|
+
|
|
|
+ $apiKey = 'sk-fxlawqVtbbQbNW0wInR3E4wsLo5JHozDC2XOHzMa711su6ss';
|
|
|
+ $apiUrl = 'https://chatapi.onechats.top/v1/chat/completions';
|
|
|
+ $modelName = 'gpt-4.1';
|
|
|
+ $messages = [
|
|
|
+ [
|
|
|
+ 'role' => 'user',
|
|
|
+ 'content' => $prompt,
|
|
|
+ ],
|
|
|
+ ];
|
|
|
+ $postData = [
|
|
|
+ 'model' => $modelName,
|
|
|
+ 'messages' => $messages,
|
|
|
+ 'max_tokens' => 1024,
|
|
|
+ 'temperature' => 0.2,
|
|
|
+ ];
|
|
|
+
|
|
|
+ //CURL请求AI接口,增加超时控制
|
|
|
+ $ch = curl_init($apiUrl);
|
|
|
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
|
+ curl_setopt($ch, CURLOPT_POST, true);
|
|
|
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData, JSON_UNESCAPED_UNICODE));
|
|
|
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
|
+ 'Content-Type: application/json',
|
|
|
+ 'Authorization: Bearer ' . $apiKey,
|
|
|
+ ]);
|
|
|
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 连接超时10秒
|
|
|
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30); // 整体请求超时30秒
|
|
|
+ // 测试环境关闭SSL校验,生产环境注释此行开启证书校验
|
|
|
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
|
+ $response = curl_exec($ch);
|
|
|
+ // CURL异常捕获
|
|
|
+ if (curl_errno($ch)) {
|
|
|
+ $errMsg = curl_error($ch);
|
|
|
+ curl_close($ch);
|
|
|
+ // 此处可写入AI调用日志
|
|
|
+ $this->error('AI接口请求失败:' . $errMsg);
|
|
|
+ }
|
|
|
+ curl_close($ch);
|
|
|
+
|
|
|
+ $responseData = json_decode($response, true);
|
|
|
+ if (!isset($responseData['choices'][0]['message']['content'])) {
|
|
|
+ // 此处可写入AI调用日志
|
|
|
+ $this->error('未能获取AI有效回复', $responseData);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理markdown代码块标记
|
|
|
+ $gptReply = trim($responseData['choices'][0]['message']['content']);
|
|
|
+ $gptReply = preg_replace('/^```(?:json)?\s*|\s*```$/u', '', $gptReply);
|
|
|
+ $result = json_decode($gptReply, true);
|
|
|
+
|
|
|
+ // 校验AI返回格式与单价合法性
|
|
|
+ if (!is_array($result) || !isset($result['piece_price']) || !is_numeric($result['piece_price'])) {
|
|
|
+ // 此处可写入AI调用日志
|
|
|
+ $this->error('AI返回数据格式异常', ['raw_reply' => $gptReply]);
|
|
|
+ }
|
|
|
+ $finalPrice = (float)$result['piece_price'];
|
|
|
+// // 业务价格区间风控,根据实际业务调整上下限
|
|
|
+// if ($finalPrice <= 0 || $finalPrice > 1.0) {
|
|
|
+// $this->error('AI估算单价超出业务合理区间', ['price' => $finalPrice]);
|
|
|
+// }
|
|
|
+
|
|
|
+ $this->success('计算成功', [
|
|
|
+ 'product_name' => $orderProductName,
|
|
|
+ 'piece_price' => $finalPrice,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从单价表产品名提取匹配关键字
|
|
|
+ * @param string $productName 产品名称
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ protected function extractRateKeywords(string $productName): array
|
|
|
+ {
|
|
|
+ $ignoreWords = ['小盒', '条盒', '系列', '彩样', '打样', '裱上盖', '拆片', '做底'];
|
|
|
+ $keywords = [];
|
|
|
+ // 斜杠分割多产品名称
|
|
|
+ $parts = preg_split('/[\/]/u', $productName);
|
|
|
+ foreach ($parts as $part) {
|
|
|
+ // 清除括号内备注内容
|
|
|
+ $cleanPart = preg_replace('/[((][^))]*[))]/u', '', $part);
|
|
|
+ $cleanPart = trim($cleanPart);
|
|
|
+ if ($cleanPart === '') continue;
|
|
|
+
|
|
|
+ // 提取品牌前缀(系列/小盒之前文字)
|
|
|
+ if (preg_match('/^(.+?)(?:系列|小盒|条盒|裱|做底|\+)/u', $cleanPart, $match)) {
|
|
|
+ $brand = trim($match[1]);
|
|
|
+ if ($brand !== '' && !in_array($brand, $ignoreWords, true)) {
|
|
|
+ $keywords[] = $brand;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取2~4位中文核心关键词
|
|
|
+ if (preg_match('/^([\x{4e00}-\x{9fa5}]{2,})/u', $cleanPart, $match)) {
|
|
|
+ $maxLen = min(4, mb_strlen($match[1], 'UTF-8'));
|
|
|
+ for ($i = 2; $i <= $maxLen; $i++) {
|
|
|
+ $kw = mb_substr($match[1], 0, $i, 'UTF-8');
|
|
|
+ if (!in_array($kw, $ignoreWords, true)) {
|
|
|
+ $keywords[] = $kw;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 去重返回
|
|
|
+ return array_values(array_unique($keywords));
|
|
|
+ }
|
|
|
+}
|