|
|
|
|
@ -9,6 +9,7 @@ import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
|
|
|
|
|
import com.tencentcloudapi.lkeap.v20240522.models.Usage;
|
|
|
|
|
import org.dromara.ai.domain.AiFormSettingDetail;
|
|
|
|
|
import org.dromara.ai.domain.AiModel;
|
|
|
|
|
import org.dromara.ai.domain.dto.AIReportResponse;
|
|
|
|
|
import org.dromara.ai.domain.dto.AiTableConditionWrapper;
|
|
|
|
|
import org.dromara.ai.domain.dto.AiTableData;
|
|
|
|
|
import org.dromara.ai.domain.dto.AiTableQueryCondition;
|
|
|
|
|
@ -22,6 +23,7 @@ import org.dromara.ai.service.IAIAssistantService;
|
|
|
|
|
import org.dromara.ai.vectordb.service.IVectorDBService;
|
|
|
|
|
import org.dromara.common.constant.HwMomAiConstants;
|
|
|
|
|
import org.dromara.common.encrypt.utils.EncryptUtils;
|
|
|
|
|
import org.dromara.common.json.utils.JsonUtils;
|
|
|
|
|
import org.dromara.common.satoken.utils.LoginHelper;
|
|
|
|
|
import org.dromara.system.api.model.LoginUser;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
@ -31,6 +33,7 @@ import reactor.core.publisher.Flux;
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
import reactor.core.publisher.Mono;
|
|
|
|
|
|
|
|
|
|
import java.time.Duration;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
@ -63,6 +66,417 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
|
|
|
|
|
@Autowired
|
|
|
|
|
private SQLServerDatabaseMetaMapper sQLServerDatabaseMetaMapper;
|
|
|
|
|
|
|
|
|
|
private final String material_inventory_sql = "SELECT\n" +
|
|
|
|
|
"\tTOP 1000 wi.inventory_id,\n" +
|
|
|
|
|
"\twi.tenant_id,\n" +
|
|
|
|
|
"\twi.batch_code,\n" +
|
|
|
|
|
"\twi.material_id,\n" +
|
|
|
|
|
"\twi.location_code,\n" +
|
|
|
|
|
"\twi.material_categories,\n" +
|
|
|
|
|
"\twi.inventory_qty,\n" +
|
|
|
|
|
"\twi.update_time,\n" +
|
|
|
|
|
"\twi.lock_state,\n" +
|
|
|
|
|
"\twi.inventory_status,\n" +
|
|
|
|
|
"\twi.store_id,\n" +
|
|
|
|
|
"\twi.create_by,\n" +
|
|
|
|
|
"\twi.create_time,\n" +
|
|
|
|
|
"\twi.update_by,\n" +
|
|
|
|
|
"\twi.material_code,\n" +
|
|
|
|
|
"\twi.warehouse_id,\n" +
|
|
|
|
|
"\tbmi.material_name,\n" +
|
|
|
|
|
"\tbmi.material_spec,\n" +
|
|
|
|
|
"\tbmi.material_unit,\n" +
|
|
|
|
|
"\tbmt.matrial_type_name,\n" +
|
|
|
|
|
"\twpsp.store_place_name,\n" +
|
|
|
|
|
"\twpsp.store_place_code\n" +
|
|
|
|
|
"FROM\n" +
|
|
|
|
|
"\twms_inventory wi WITH(NOLOCK)\n" +
|
|
|
|
|
"LEFT JOIN base_material_info bmi WITH(NOLOCK) ON\n" +
|
|
|
|
|
"\tbmi.material_id = wi.material_id\n" +
|
|
|
|
|
"\tAND bmi.tenant_id = wi.tenant_id\n" +
|
|
|
|
|
"\tAND bmi.del_flag = '0'\n" +
|
|
|
|
|
"LEFT JOIN base_material_type bmt WITH(NOLOCK) ON\n" +
|
|
|
|
|
"\tbmt.matrial_type_id = bmi.material_type_id\n" +
|
|
|
|
|
"\tAND bmt.tenant_id = wi.tenant_id\n" +
|
|
|
|
|
"\tAND bmt.del_flag = '0'\n" +
|
|
|
|
|
"LEFT JOIN wms_psm_store_place wpsp WITH(NOLOCK) ON\n" +
|
|
|
|
|
"\twpsp.store_place_code = wi.location_code\n" +
|
|
|
|
|
"\tAND wpsp.tenant_id = wi.tenant_id\n" +
|
|
|
|
|
"WHERE\n" +
|
|
|
|
|
"\twi.inventory_status = '1'";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private String reportHtml = "<html>\n" +
|
|
|
|
|
"<head>\n" +
|
|
|
|
|
" <style>\n" +
|
|
|
|
|
" body {\n" +
|
|
|
|
|
" font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n" +
|
|
|
|
|
" margin: 20px;\n" +
|
|
|
|
|
" background-color: #f5f7fa;\n" +
|
|
|
|
|
" color: #333;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .container {\n" +
|
|
|
|
|
" max-width: 1400px;\n" +
|
|
|
|
|
" margin: 0 auto;\n" +
|
|
|
|
|
" background-color: white;\n" +
|
|
|
|
|
" padding: 25px;\n" +
|
|
|
|
|
" border-radius: 12px;\n" +
|
|
|
|
|
" box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" h1 {\n" +
|
|
|
|
|
" color: #2c3e50;\n" +
|
|
|
|
|
" border-bottom: 3px solid #3498db;\n" +
|
|
|
|
|
" padding-bottom: 10px;\n" +
|
|
|
|
|
" margin-bottom: 25px;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" h2, h3 {\n" +
|
|
|
|
|
" color: #34495e;\n" +
|
|
|
|
|
" margin-top: 30px;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .stats-container {\n" +
|
|
|
|
|
" display: flex;\n" +
|
|
|
|
|
" flex-wrap: wrap;\n" +
|
|
|
|
|
" gap: 20px;\n" +
|
|
|
|
|
" margin-bottom: 30px;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .stat-card {\n" +
|
|
|
|
|
" flex: 1;\n" +
|
|
|
|
|
" min-width: 200px;\n" +
|
|
|
|
|
" background: linear-gradient(135deg, #f8f9fa, #e9ecef);\n" +
|
|
|
|
|
" padding: 20px;\n" +
|
|
|
|
|
" border-radius: 10px;\n" +
|
|
|
|
|
" box-shadow: 0 3px 10px rgba(0,0,0,0.05);\n" +
|
|
|
|
|
" border-left: 5px solid #3498db;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .stat-card h3 {\n" +
|
|
|
|
|
" margin-top: 0;\n" +
|
|
|
|
|
" font-size: 1.1em;\n" +
|
|
|
|
|
" color: #7f8c8d;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .stat-value {\n" +
|
|
|
|
|
" font-size: 2.2em;\n" +
|
|
|
|
|
" font-weight: bold;\n" +
|
|
|
|
|
" color: #2c3e50;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .stat-unit {\n" +
|
|
|
|
|
" font-size: 0.9em;\n" +
|
|
|
|
|
" color: #95a5a6;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .chart-container {\n" +
|
|
|
|
|
" height: 400px;\n" +
|
|
|
|
|
" margin: 25px 0;\n" +
|
|
|
|
|
" border-radius: 10px;\n" +
|
|
|
|
|
" border: 1px solid #e1e8ed;\n" +
|
|
|
|
|
" padding: 15px;\n" +
|
|
|
|
|
" background-color: #fff;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" table {\n" +
|
|
|
|
|
" width: 100%;\n" +
|
|
|
|
|
" border-collapse: collapse;\n" +
|
|
|
|
|
" margin-top: 20px;\n" +
|
|
|
|
|
" box-shadow: 0 2px 8px rgba(0,0,0,0.05);\n" +
|
|
|
|
|
" border-radius: 8px;\n" +
|
|
|
|
|
" overflow: hidden;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" th {\n" +
|
|
|
|
|
" background-color: #2c3e50;\n" +
|
|
|
|
|
" color: white;\n" +
|
|
|
|
|
" text-align: left;\n" +
|
|
|
|
|
" padding: 15px;\n" +
|
|
|
|
|
" font-weight: 600;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" td {\n" +
|
|
|
|
|
" padding: 12px 15px;\n" +
|
|
|
|
|
" border-bottom: 1px solid #eee;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" tr:nth-child(even) {\n" +
|
|
|
|
|
" background-color: #f9f9f9;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" tr:hover {\n" +
|
|
|
|
|
" background-color: #f1f7fd;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .status-1 {\n" +
|
|
|
|
|
" color: #27ae60;\n" +
|
|
|
|
|
" font-weight: bold;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .status-0 {\n" +
|
|
|
|
|
" color: #e74c3c;\n" +
|
|
|
|
|
" font-weight: bold;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .top10-list {\n" +
|
|
|
|
|
" background: #f8f9fa;\n" +
|
|
|
|
|
" padding: 20px;\n" +
|
|
|
|
|
" border-radius: 10px;\n" +
|
|
|
|
|
" margin: 20px 0;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .top10-item {\n" +
|
|
|
|
|
" display: flex;\n" +
|
|
|
|
|
" justify-content: space-between;\n" +
|
|
|
|
|
" padding: 10px;\n" +
|
|
|
|
|
" border-bottom: 1px dashed #ddd;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .top10-item:last-child {\n" +
|
|
|
|
|
" border-bottom: none;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .footer {\n" +
|
|
|
|
|
" text-align: center;\n" +
|
|
|
|
|
" margin-top: 40px;\n" +
|
|
|
|
|
" color: #95a5a6;\n" +
|
|
|
|
|
" font-size: 0.9em;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" </style>\n" +
|
|
|
|
|
"</head>\n" +
|
|
|
|
|
"<body>\n" +
|
|
|
|
|
" <div class=\"container\">\n" +
|
|
|
|
|
" <h1>\uD83D\uDCCA WMS库存分析报表</h1>\n" +
|
|
|
|
|
" \n" +
|
|
|
|
|
" <div class=\"stats-container\">\n" +
|
|
|
|
|
" <div class=\"stat-card\">\n" +
|
|
|
|
|
" <h3>物料种类数</h3>\n" +
|
|
|
|
|
" <div class=\"stat-value\" id=\"totalMaterials\">0</div>\n" +
|
|
|
|
|
" <div class=\"stat-unit\">种</div>\n" +
|
|
|
|
|
" </div>\n" +
|
|
|
|
|
" <div class=\"stat-card\">\n" +
|
|
|
|
|
" <h3>总库存量</h3>\n" +
|
|
|
|
|
" <div class=\"stat-value\" id=\"totalQuantity\">0.00</div>\n" +
|
|
|
|
|
" <div class=\"stat-unit\">单位</div>\n" +
|
|
|
|
|
" </div>\n" +
|
|
|
|
|
" <div class=\"stat-card\">\n" +
|
|
|
|
|
" <h3>平均库存量</h3>\n" +
|
|
|
|
|
" <div class=\"stat-value\" id=\"avgQuantity\">0.00</div>\n" +
|
|
|
|
|
" <div class=\"stat-unit\">单位/物料</div>\n" +
|
|
|
|
|
" </div>\n" +
|
|
|
|
|
" <div class=\"stat-card\">\n" +
|
|
|
|
|
" <h3>数据记录数</h3>\n" +
|
|
|
|
|
" <div class=\"stat-value\" id=\"totalRecords\">0</div>\n" +
|
|
|
|
|
" <div class=\"stat-unit\">条</div>\n" +
|
|
|
|
|
" </div>\n" +
|
|
|
|
|
" </div>\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" <h2>物料库存分布</h2>\n" +
|
|
|
|
|
" <div id=\"chartDistribution\" class=\"chart-container\"></div>\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" <h2>库存量TOP物料</h2>\n" +
|
|
|
|
|
" <div id=\"chartTop\" class=\"chart-container\"></div>\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" <h2>物料分类统计</h2>\n" +
|
|
|
|
|
" <div id=\"chartByMaterial\" class=\"chart-container\"></div>\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" <h2>详细库存数据</h2>\n" +
|
|
|
|
|
" <table id=\"detailTable\">\n" +
|
|
|
|
|
" <thead>\n" +
|
|
|
|
|
" <tr>\n" +
|
|
|
|
|
" <th>库存ID</th>\n" +
|
|
|
|
|
" <th>物料编码</th>\n" +
|
|
|
|
|
" <th>库位编码</th>\n" +
|
|
|
|
|
" <th>库存数量</th>\n" +
|
|
|
|
|
" <th>批次码</th>\n" +
|
|
|
|
|
" <th>库存状态</th>\n" +
|
|
|
|
|
" <th>锁定状态</th>\n" +
|
|
|
|
|
" <th>更新时间</th>\n" +
|
|
|
|
|
" </tr>\n" +
|
|
|
|
|
" </thead>\n" +
|
|
|
|
|
" <tbody>\n" +
|
|
|
|
|
" <!-- 数据将由JS填充 -->\n" +
|
|
|
|
|
" </tbody>\n" +
|
|
|
|
|
" </table>\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" <div class=\"footer\">\n" +
|
|
|
|
|
" 报表生成时间: <span id=\"reportTime\"></span> | 数据来源: WMS系统\n" +
|
|
|
|
|
" </div>\n" +
|
|
|
|
|
" </div>\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" <script>\n" +
|
|
|
|
|
" let echarts = window.parent.echarts;\n" +
|
|
|
|
|
" // 原始数据\n" +
|
|
|
|
|
" const stockData = [{\"库位编码\":\"BB-1-1-1\",\"库存状态\":\"1\",\"库存ID\":2018,\"库存数量\":\"30.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250915002M10053001\",\"更新时间\":\"2025-09-19 16:00:00\"},{\"库位编码\":\"BB-1-1-3\",\"库存状态\":\"1\",\"库存ID\":2017,\"物料编码\":\"WMS-003\",\"库存数量\":\"10.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250917001WMS-003001\",\"更新时间\":\"2025-09-17 18:10:00\"},{\"库位编码\":\"BB-1-1-1\",\"库存状态\":\"1\",\"库存ID\":2016,\"物料编码\":\"WMS-002\",\"库存数量\":\"120.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250916001WMS-002001\",\"更新时间\":\"2025-09-16 18:45:00\"},{\"库位编码\":\"BB-1-1-1\",\"库存状态\":\"1\",\"库存ID\":2015,\"物料编码\":\"WMS-001\",\"库存数量\":\"15.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250915001WMS-001001\",\"更新时间\":\"2025-09-15 16:30:00\"},{\"库位编码\":\"DJ-01\",\"库存状态\":\"1\",\"库存ID\":1014,\"物料编码\":\"WMS-003\",\"库存数量\":\"7.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250903001WMS-003001\",\"更新时间\":\"2025-09-05 17:50:02\"},{\"库位编码\":\"BB-1-1-3\",\"库存状态\":\"1\",\"库存ID\":1012,\"物料编码\":\"WMS-001\",\"库存数量\":\"5.000000\",\"锁定状态\":\"0\",\"批次码\":\"test123\",\"更新时间\":\"2025-09-04 17:31:11\"},{\"库位编码\":\"BB-1-1-1\",\"库存状态\":\"1\",\"库存ID\":1013,\"物料编码\":\"WMS-002\",\"库存数量\":\"5.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250903001WMS-002001002\",\"更新时间\":\"2025-09-04 10:11:40\"}];\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 数据处理\n" +
|
|
|
|
|
" function processData(data) {\n" +
|
|
|
|
|
" // 确保物料编码存在\n" +
|
|
|
|
|
" data.forEach(item => {\n" +
|
|
|
|
|
" if (!item.物料编码) item.物料编码 = '未知物料';\n" +
|
|
|
|
|
" item.库存数量 = parseFloat(item.库存数量);\n" +
|
|
|
|
|
" });\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 按物料编码分组统计\n" +
|
|
|
|
|
" const materialMap = {};\n" +
|
|
|
|
|
" data.forEach(item => {\n" +
|
|
|
|
|
" const matCode = item.物料编码;\n" +
|
|
|
|
|
" if (!materialMap[matCode]) {\n" +
|
|
|
|
|
" materialMap[matCode] = {\n" +
|
|
|
|
|
" code: matCode,\n" +
|
|
|
|
|
" total: 0,\n" +
|
|
|
|
|
" count: 0,\n" +
|
|
|
|
|
" locations: new Set()\n" +
|
|
|
|
|
" };\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" materialMap[matCode].total += item.库存数量;\n" +
|
|
|
|
|
" materialMap[matCode].count++;\n" +
|
|
|
|
|
" materialMap[matCode].locations.add(item.库位编码);\n" +
|
|
|
|
|
" });\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 转换为数组并排序\n" +
|
|
|
|
|
" const materialStats = Object.values(materialMap).map(mat => ({\n" +
|
|
|
|
|
" ...mat,\n" +
|
|
|
|
|
" locations: Array.from(mat.locatures),\n" +
|
|
|
|
|
" avg: mat.total / mat.count\n" +
|
|
|
|
|
" }));\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 按库存总量排序\n" +
|
|
|
|
|
" materialStats.sort((a, b) => b.total - a.total);\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" return {\n" +
|
|
|
|
|
" rawData: data,\n" +
|
|
|
|
|
" materialStats: materialStats,\n" +
|
|
|
|
|
" totalQuantity: data.reduce((sum, item) => sum + item.库存数量, 0),\n" +
|
|
|
|
|
" totalMaterials: materialStats.length,\n" +
|
|
|
|
|
" totalRecords: data.length,\n" +
|
|
|
|
|
" avgQuantity: data.reduce((sum, item) => sum + item.库存数量, 0) / data.length\n" +
|
|
|
|
|
" };\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 更新统计卡片\n" +
|
|
|
|
|
" function updateStats(processed) {\n" +
|
|
|
|
|
" document.getElementById('totalMaterials').textContent = processed.totalMaterials;\n" +
|
|
|
|
|
" document.getElementById('totalQuantity').textContent = processed.totalQuantity.toFixed(2);\n" +
|
|
|
|
|
" document.getElementById('avgQuantity').textContent = processed.avgQuantity.toFixed(2);\n" +
|
|
|
|
|
" document.getElementById('totalRecords').textContent = processed.totalRecords;\n" +
|
|
|
|
|
" document.getElementById('reportTime').textContent = new Date().toLocaleString('zh-CN');\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 填充详细表格\n" +
|
|
|
|
|
" function renderDetailTable(data) {\n" +
|
|
|
|
|
" const tbody = document.querySelector('#detailTable tbody');\n" +
|
|
|
|
|
" tbody.innerHTML = '';\n" +
|
|
|
|
|
" \n" +
|
|
|
|
|
" data.forEach(item => {\n" +
|
|
|
|
|
" const row = document.createElement('tr');\n" +
|
|
|
|
|
" row.innerHTML = `\n" +
|
|
|
|
|
" <td>${item.库存ID}</td>\n" +
|
|
|
|
|
" <td>${item.物料编码 || '未知物料'}</td>\n" +
|
|
|
|
|
" <td>${item.库位编码}</td>\n" +
|
|
|
|
|
" <td>${item.库存数量.toFixed(2)}</td>\n" +
|
|
|
|
|
" <td>${item.批次码}</td>\n" +
|
|
|
|
|
" <td class=\"status-${item.库存状态}\">${item.库存状态 === '1' ? '正常' : '异常'}</td>\n" +
|
|
|
|
|
" <td class=\"status-${item.锁定状态}\">${item.锁定状态 === '0' ? '未锁定' : '已锁定'}</td>\n" +
|
|
|
|
|
" <td>${item.更新时间}</td>\n" +
|
|
|
|
|
" `;\n" +
|
|
|
|
|
" tbody.appendChild(row);\n" +
|
|
|
|
|
" });\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 初始化图表\n" +
|
|
|
|
|
" function initCharts(processed) {\n" +
|
|
|
|
|
" const { materialStats, rawData } = processed;\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 图表1: 库存分布(按库位)\n" +
|
|
|
|
|
" const locationMap = {};\n" +
|
|
|
|
|
" rawData.forEach(item => {\n" +
|
|
|
|
|
" const loc = item.库位编码;\n" +
|
|
|
|
|
" locationMap[loc] = (locationMap[loc] || 0) + item.库存数量;\n" +
|
|
|
|
|
" });\n" +
|
|
|
|
|
" \n" +
|
|
|
|
|
" const chart1 = echarts.init(document.getElementById('chartDistribution'));\n" +
|
|
|
|
|
" chart1.setOption({\n" +
|
|
|
|
|
" title: { text: '各库位库存量分布', left: 'center' },\n" +
|
|
|
|
|
" tooltip: { trigger: 'item' },\n" +
|
|
|
|
|
" legend: { orient: 'vertical', left: 'left' },\n" +
|
|
|
|
|
" series: [{\n" +
|
|
|
|
|
" name: '库存量',\n" +
|
|
|
|
|
" type: 'pie',\n" +
|
|
|
|
|
" radius: '60%',\n" +
|
|
|
|
|
" data: Object.entries(locationMap).map(([name, value]) => ({\n" +
|
|
|
|
|
" name: `${name} (${value.toFixed(1)})`,\n" +
|
|
|
|
|
" value: value\n" +
|
|
|
|
|
" })),\n" +
|
|
|
|
|
" emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }\n" +
|
|
|
|
|
" }]\n" +
|
|
|
|
|
" });\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 图表2: 库存量TOP物料\n" +
|
|
|
|
|
" const topData = materialStats.slice(0, Math.min(10, materialStats.length));\n" +
|
|
|
|
|
" const chart2 = echarts.init(document.getElementById('chartTop'));\n" +
|
|
|
|
|
" chart2.setOption({\n" +
|
|
|
|
|
" title: { text: '库存量TOP物料', left: 'center' },\n" +
|
|
|
|
|
" tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },\n" +
|
|
|
|
|
" grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },\n" +
|
|
|
|
|
" xAxis: {\n" +
|
|
|
|
|
" type: 'category',\n" +
|
|
|
|
|
" data: topData.map(item => item.code),\n" +
|
|
|
|
|
" axisLabel: { rotate: 45 }\n" +
|
|
|
|
|
" },\n" +
|
|
|
|
|
" yAxis: { type: 'value', name: '库存量' },\n" +
|
|
|
|
|
" series: [{\n" +
|
|
|
|
|
" name: '库存总量',\n" +
|
|
|
|
|
" type: 'bar',\n" +
|
|
|
|
|
" data: topData.map(item => item.total),\n" +
|
|
|
|
|
" itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [\n" +
|
|
|
|
|
" { offset: 0, color: '#83bff6' },\n" +
|
|
|
|
|
" { offset: 0.5, color: '#188df0' },\n" +
|
|
|
|
|
" { offset: 1, color: '#188df0' }\n" +
|
|
|
|
|
" ]) }\n" +
|
|
|
|
|
" }]\n" +
|
|
|
|
|
" });\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 图表3: 物料分类统计(库存量与记录数)\n" +
|
|
|
|
|
" const chart3 = echarts.init(document.getElementById('chartByMaterial'));\n" +
|
|
|
|
|
" chart3.setOption({\n" +
|
|
|
|
|
" title: { text: '物料库存统计', left: 'center' },\n" +
|
|
|
|
|
" tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },\n" +
|
|
|
|
|
" legend: { data: ['库存总量', '记录数'], top: '10%' },\n" +
|
|
|
|
|
" grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },\n" +
|
|
|
|
|
" xAxis: {\n" +
|
|
|
|
|
" type: 'category',\n" +
|
|
|
|
|
" data: materialStats.map(item => item.code),\n" +
|
|
|
|
|
" axisLabel: { rotate: 45 }\n" +
|
|
|
|
|
" },\n" +
|
|
|
|
|
" yAxis: [\n" +
|
|
|
|
|
" { type: 'value', name: '库存总量', position: 'left' },\n" +
|
|
|
|
|
" { type: 'value', name: '记录数', position: 'right' }\n" +
|
|
|
|
|
" ],\n" +
|
|
|
|
|
" series: [\n" +
|
|
|
|
|
" {\n" +
|
|
|
|
|
" name: '库存总量',\n" +
|
|
|
|
|
" type: 'bar',\n" +
|
|
|
|
|
" data: materialStats.map(item => item.total),\n" +
|
|
|
|
|
" yAxisIndex: 0,\n" +
|
|
|
|
|
" itemStyle: { color: '#3498db' }\n" +
|
|
|
|
|
" },\n" +
|
|
|
|
|
" {\n" +
|
|
|
|
|
" name: '记录数',\n" +
|
|
|
|
|
" type: 'line',\n" +
|
|
|
|
|
" data: materialStats.map(item => item.count),\n" +
|
|
|
|
|
" yAxisIndex: 1,\n" +
|
|
|
|
|
" symbol: 'circle',\n" +
|
|
|
|
|
" symbolSize: 8,\n" +
|
|
|
|
|
" lineStyle: { color: '#e74c3c', width: 3 },\n" +
|
|
|
|
|
" itemStyle: { color: '#e74c3c' }\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" ]\n" +
|
|
|
|
|
" });\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
"\n" +
|
|
|
|
|
" // 页面加载完成后执行\n" +
|
|
|
|
|
" document.addEventListener('DOMContentLoaded', function() {\n" +
|
|
|
|
|
" const processed = processData(stockData);\n" +
|
|
|
|
|
" updateStats(processed);\n" +
|
|
|
|
|
" renderDetailTable(processed.rawData);\n" +
|
|
|
|
|
" initCharts(processed);\n" +
|
|
|
|
|
" \n" +
|
|
|
|
|
" // 响应窗口大小变化\n" +
|
|
|
|
|
" window.addEventListener('resize', function() {\n" +
|
|
|
|
|
" echarts.getInstanceByDom(document.getElementById('chartDistribution'))?.resize();\n" +
|
|
|
|
|
" echarts.getInstanceByDom(document.getElementById('chartTop'))?.resize();\n" +
|
|
|
|
|
" echarts.getInstanceByDom(document.getElementById('chartByMaterial'))?.resize();\n" +
|
|
|
|
|
" });\n" +
|
|
|
|
|
" });\n" +
|
|
|
|
|
" </script>\n" +
|
|
|
|
|
"</body>\n" +
|
|
|
|
|
"</html>";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 流式聊天接口
|
|
|
|
|
*
|
|
|
|
|
@ -129,7 +543,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
|
|
|
|
|
String messageContent = message.getContent().toString();
|
|
|
|
|
aiRequest.setText(messageContent);
|
|
|
|
|
aiRequest.setTexts(new String[]{messageContent});
|
|
|
|
|
StringBuilder sb = new StringBuilder(messageContent);
|
|
|
|
|
// StringBuilder sb = new StringBuilder(messageContent);
|
|
|
|
|
|
|
|
|
|
Long embeddingModelId = aiRequest.getEmbeddingModelId();
|
|
|
|
|
AiModel aiModel = aiModelMapper.selectById(embeddingModelId);
|
|
|
|
|
@ -165,16 +579,19 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
|
|
|
|
|
throw new RuntimeException("未找到相关知识库内容");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sb.append("\n####,请从以下知识库内容中获取答案:");
|
|
|
|
|
for (String content : searchResultList) {
|
|
|
|
|
sb.append("\n####").append(content);
|
|
|
|
|
}
|
|
|
|
|
// sb.append("\n####,请从以下知识库内容中获取答案:");
|
|
|
|
|
// for (String content : searchResultList) {
|
|
|
|
|
// sb.append("\n####").append(content);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// sb.append("\n####,LLM是人工智能大模型。agent是一个智能助手,agent的任务是回答用户的问题");
|
|
|
|
|
// sb.append(("\n\n注意:回答问题时,须严格根据我给你的系统上下文内容原文进行回答,请不要自己发挥,回答时保持原来文本的段落层级"));
|
|
|
|
|
|
|
|
|
|
sb.append((!searchResultList.isEmpty() ? "\n\n注意:回答问题时,须严格根据我给你的系统上下文内容原文进行回答,请不要自己发挥,回答时保持原来文本的段落层级" : ""));
|
|
|
|
|
message.setContent(sb.toString());
|
|
|
|
|
// sb.append((!searchResultList.isEmpty() ? "\n\n注意:回答问题时,须严格根据我给你的系统上下文内容原文进行回答,请不要自己发挥,回答时保持原来文本的段落层级" : ""));
|
|
|
|
|
|
|
|
|
|
String optimizedPrompt = buildKnowledgeBasedPrompt(messageContent, searchResultList);
|
|
|
|
|
|
|
|
|
|
message.setContent(optimizedPrompt);
|
|
|
|
|
|
|
|
|
|
return aiRequest;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
@ -183,6 +600,49 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构建知识库提示词
|
|
|
|
|
*/
|
|
|
|
|
private String buildKnowledgeBasedPrompt(String userQuestion, List<String> searchResults) {
|
|
|
|
|
StringBuilder promptBuilder = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
if (searchResults.isEmpty()) {
|
|
|
|
|
// 没有找到相关知识库内容
|
|
|
|
|
promptBuilder.append("用户问题:").append(userQuestion).append("\n\n");
|
|
|
|
|
promptBuilder.append("【重要提示】当前知识库中没有找到相关内容。\n");
|
|
|
|
|
promptBuilder.append("请直接回复:\"根据现有资料,无法回答此问题。\"");
|
|
|
|
|
return promptBuilder.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建结构化的prompt
|
|
|
|
|
promptBuilder.append("【指令说明】\n");
|
|
|
|
|
promptBuilder.append("你是一个专业的信息检索助手,需要严格根据提供的知识库内容回答问题。\n\n");
|
|
|
|
|
|
|
|
|
|
promptBuilder.append("【知识库内容】\n");
|
|
|
|
|
promptBuilder.append("以下是从知识库中检索到的相关信息:\n\n");
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < searchResults.size(); i++) {
|
|
|
|
|
promptBuilder.append("--- 知识片段 ").append(i + 1).append(" ---\n");
|
|
|
|
|
promptBuilder.append(searchResults.get(i)).append("\n\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
promptBuilder.append("【用户问题】\n");
|
|
|
|
|
promptBuilder.append(userQuestion).append("\n\n");
|
|
|
|
|
|
|
|
|
|
promptBuilder.append("【回答要求】\n");
|
|
|
|
|
promptBuilder.append("1. 必须严格基于上述知识库内容回答\n");
|
|
|
|
|
promptBuilder.append("2. 如果知识库内容不足以回答问题,请明确说明\n");
|
|
|
|
|
promptBuilder.append("3. 保持原文的准确性和细节,不要添加任何外部知识\n");
|
|
|
|
|
promptBuilder.append("4. 如果知识库中有多个相关内容,请整合信息进行回答\n");
|
|
|
|
|
promptBuilder.append("5. 使用清晰、有条理的方式组织答案\n\n");
|
|
|
|
|
|
|
|
|
|
promptBuilder.append("请基于知识库内容回答用户问题:");
|
|
|
|
|
|
|
|
|
|
return promptBuilder.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 创建标准化的错误事件JSON字符串
|
|
|
|
|
*/
|
|
|
|
|
@ -220,6 +680,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
|
|
|
|
|
@Override
|
|
|
|
|
public String generateSQL(AIRequest aiRequest) {
|
|
|
|
|
String naturalLanguageQuery = aiRequest.getText();
|
|
|
|
|
|
|
|
|
|
// 1. 获取数据库结构
|
|
|
|
|
String dataName = StringUtils.isNotBlank(aiRequest.getDataName()) ? aiRequest.getDataName() : "master";
|
|
|
|
|
String schemaDescription = redisTemplate.opsForValue().get(HwMomAiConstants.AI_DATABASE_SCHEMA_KEY_PREFIX + dataName);
|
|
|
|
|
@ -228,72 +689,165 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JSONObject schemaJson = JSONObject.parseObject(schemaDescription);
|
|
|
|
|
StringBuilder sb = new StringBuilder("SQL Server 数据库结构:\n\n");
|
|
|
|
|
schemaJson.entrySet().forEach(entry -> {
|
|
|
|
|
// sb.append(entry.getKey()).append("\n");
|
|
|
|
|
sb.append(entry.getValue()).append("\n\n");
|
|
|
|
|
});
|
|
|
|
|
// sb.append(schemaDescription);
|
|
|
|
|
String formattedSchema = formatDatabaseSchema(schemaJson);
|
|
|
|
|
|
|
|
|
|
// 2. 构建 AI 提示
|
|
|
|
|
String prompt = String.format(
|
|
|
|
|
"你是一个专业的 SQL Server 数据库专家。必须基于表结构生成SQL,其中的表名和字段名必须来自表结构信息(如果以下数据库结构中没有则返回select * from):\n\n%s\n\n" +
|
|
|
|
|
"请将以下自然语言查询转换为优化的 SQL Server T-SQL 语句:\n" +
|
|
|
|
|
"---\n%s\n---\n\n" +
|
|
|
|
|
"要求:\n" +
|
|
|
|
|
"1. 只返回 SQL 语句,不要包含解释\n" +
|
|
|
|
|
"2. 使用 SQL Server 特有的语法(如 TOP 而不是 LIMIT)\n" +
|
|
|
|
|
"3. 考虑性能优化\n" +
|
|
|
|
|
"4. 使用合适的索引提示(如果需要)\n" +
|
|
|
|
|
"5. 包含必要的 WITH(NOLOCK) 提示(适用于高并发环境)\n" +
|
|
|
|
|
"6. 使用 ANSI 标准的 JOIN 语法 \n",
|
|
|
|
|
sb.toString(), naturalLanguageQuery
|
|
|
|
|
);
|
|
|
|
|
// 2. 构建AI提示词(使用优化后的模板)
|
|
|
|
|
String prompt = buildAIPrompt(formattedSchema, naturalLanguageQuery);
|
|
|
|
|
|
|
|
|
|
// 3. 调用AI服务
|
|
|
|
|
IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory.getProcessorByPlatformId(aiRequest.getPlatformId());
|
|
|
|
|
AIMessage aiMessage = new AIMessage();
|
|
|
|
|
aiMessage.setRole("user");
|
|
|
|
|
aiMessage.setContent(prompt);
|
|
|
|
|
aiRequest.setMessages(Collections.singletonList(aiMessage));
|
|
|
|
|
|
|
|
|
|
Mono<AIResponse> response = processor.chat(aiRequest);
|
|
|
|
|
if (Objects.requireNonNull(response.block()).isSuccess()) {
|
|
|
|
|
String content = response.block().getContent().toString();
|
|
|
|
|
//content内容需要转换,通过deepseek返回的如:
|
|
|
|
|
// ```sql
|
|
|
|
|
// SELECT
|
|
|
|
|
// dept_id,
|
|
|
|
|
// tenant_id,
|
|
|
|
|
// parent_id,
|
|
|
|
|
// ancestors,
|
|
|
|
|
// dept_name,
|
|
|
|
|
// dept_category,
|
|
|
|
|
// order_num,
|
|
|
|
|
// leader,
|
|
|
|
|
// phone,
|
|
|
|
|
// email,
|
|
|
|
|
// status,
|
|
|
|
|
// del_flag,
|
|
|
|
|
// create_dept,
|
|
|
|
|
// create_by,
|
|
|
|
|
// create_time,
|
|
|
|
|
// update_by,
|
|
|
|
|
// update_time
|
|
|
|
|
// FROM sys_dept WITH(NOLOCK)
|
|
|
|
|
// WHERE del_flag = '0'
|
|
|
|
|
// ORDER BY order_num, dept_id;
|
|
|
|
|
//```
|
|
|
|
|
processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_SQL,prompt, content,response.block().getTokenUsage(),
|
|
|
|
|
aiRequest.getModelId(), null,null,
|
|
|
|
|
null,null,"0","1",LoginHelper.getUserId(),LoginHelper.getTenantId(),LoginHelper.getDeptId());
|
|
|
|
|
return extractSqlFromContent(content);
|
|
|
|
|
|
|
|
|
|
// 4. 处理响应
|
|
|
|
|
return handleAIResponse(response, aiRequest, processor, prompt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 格式化数据库结构
|
|
|
|
|
*/
|
|
|
|
|
private String formatDatabaseSchema(JSONObject schemaJson) {
|
|
|
|
|
StringBuilder sb = new StringBuilder("SQL Server 数据库结构:\n\n");
|
|
|
|
|
|
|
|
|
|
// 统计各类表,便于AI分析
|
|
|
|
|
Map<String, List<String>> tableCategories = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
schemaJson.entrySet().forEach(entry -> {
|
|
|
|
|
String tableName = entry.getKey();
|
|
|
|
|
String tableInfo = entry.getValue().toString();
|
|
|
|
|
sb.append(tableInfo).append("\n\n");
|
|
|
|
|
|
|
|
|
|
// 自动分类表(简化版,可根据实际需求扩展)
|
|
|
|
|
if (tableName.startsWith("wms_")) {
|
|
|
|
|
tableCategories.computeIfAbsent("仓储管理相关表", k -> new ArrayList<>()).add(tableName);
|
|
|
|
|
} else if (tableName.startsWith("base_")) {
|
|
|
|
|
tableCategories.computeIfAbsent("基础信息表", k -> new ArrayList<>()).add(tableName);
|
|
|
|
|
} else if (tableName.startsWith("dms_") || tableName.contains("sys_user")) {
|
|
|
|
|
tableCategories.computeIfAbsent("设备管理相关表", k -> new ArrayList<>()).add(tableName);
|
|
|
|
|
} else if (tableName.startsWith("qc_") || tableName.contains("inspection")) {
|
|
|
|
|
tableCategories.computeIfAbsent("质量管理相关表", k -> new ArrayList<>()).add(tableName);
|
|
|
|
|
} else if (tableName.startsWith("prod_") || tableName.contains("log")) {
|
|
|
|
|
tableCategories.computeIfAbsent("MES相关表", k -> new ArrayList<>()).add(tableName);
|
|
|
|
|
} else if (tableName.startsWith("sys_")) {
|
|
|
|
|
tableCategories.computeIfAbsent("系统管理相关表", k -> new ArrayList<>()).add(tableName);
|
|
|
|
|
} else {
|
|
|
|
|
throw new RuntimeException("生成sql语句失败" + response.block().getErrorMessage());
|
|
|
|
|
tableCategories.computeIfAbsent("其他表", k -> new ArrayList<>()).add(tableName);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 添加表分类信息,帮助AI理解
|
|
|
|
|
sb.append("【表分类参考】\n");
|
|
|
|
|
tableCategories.forEach((category, tables) -> {
|
|
|
|
|
sb.append(category).append(": ").append(String.join(", ", tables)).append("\n");
|
|
|
|
|
});
|
|
|
|
|
sb.append("\n");
|
|
|
|
|
|
|
|
|
|
return sb.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构建AI提示词
|
|
|
|
|
*/
|
|
|
|
|
private String buildAIPrompt(String schemaDescription, String naturalLanguageQuery) {
|
|
|
|
|
return String.format("""
|
|
|
|
|
你是一个专业的SQL Server数据库专家,需要根据用户查询意图和提供的表结构,智能选择最合适的表和字段生成SQL。
|
|
|
|
|
|
|
|
|
|
# 核心规则
|
|
|
|
|
1. **查询意图分析**:
|
|
|
|
|
- 分析用户查询的自然语言,识别关键词
|
|
|
|
|
- 根据关键词选择最相关的表
|
|
|
|
|
|
|
|
|
|
2. **表选择策略**:
|
|
|
|
|
- 如果查询意图明确(如"库存"、"物料"、"用户"),选择对应的核心表
|
|
|
|
|
- 如果查询需要关联信息,进行必要的JOIN
|
|
|
|
|
- 优先选择包含核心业务数据的表
|
|
|
|
|
|
|
|
|
|
3. **字段选择原则**:
|
|
|
|
|
- 选择与查询意图最相关的字段
|
|
|
|
|
- 包含必要的标识字段(如ID、编码)
|
|
|
|
|
- 使用有意义的字段别名(中文优先)
|
|
|
|
|
|
|
|
|
|
4. **SQL规范**:
|
|
|
|
|
- 使用SQL Server特有语法(TOP、WITH(NOLOCK))
|
|
|
|
|
- 使用ANSI JOIN语法
|
|
|
|
|
- 默认添加合理的时间排序:ORDER BY create_time DESC 或 update_time DESC
|
|
|
|
|
- 如果表有状态字段(status),默认只查询正常状态的数据
|
|
|
|
|
- 如果表有删除标记(del_flag),默认过滤已删除的数据(del_flag = '0');注意有的表没有del_flag
|
|
|
|
|
- 不要添加未明确要求的过滤条件
|
|
|
|
|
- 每个字段必须有AS别名
|
|
|
|
|
- 每个表必须有表别名
|
|
|
|
|
- 在JOIN和WHERE条件中必须使用表别名.字段名格式
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5. **性能优化**:
|
|
|
|
|
- 添加WITH(NOLOCK)提示(适用于高并发环境)
|
|
|
|
|
- 使用TOP限制结果集(默认TOP 1000)
|
|
|
|
|
- 在JOIN条件字段上应该建立索引
|
|
|
|
|
|
|
|
|
|
# 数据库表结构
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
|
|
# 用户查询
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
|
|
# 生成要求
|
|
|
|
|
基于以上规则,请:
|
|
|
|
|
1. 分析用户查询意图
|
|
|
|
|
2. 选择最合适的表和字段
|
|
|
|
|
3. 生成优化后的T-SQL语句
|
|
|
|
|
4. 只返回SQL语句,不要任何解释
|
|
|
|
|
|
|
|
|
|
请生成SQL:""", schemaDescription, naturalLanguageQuery);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理AI响应
|
|
|
|
|
*/
|
|
|
|
|
private String handleAIResponse(Mono<AIResponse> response, AIRequest aiRequest,
|
|
|
|
|
IUnifiedAIProviderProcessor processor, String prompt) {
|
|
|
|
|
AIResponse aiResponse = response.block();
|
|
|
|
|
if (aiResponse == null) {
|
|
|
|
|
throw new RuntimeException("AI服务无响应");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!aiResponse.isSuccess()) {
|
|
|
|
|
throw new RuntimeException("生成SQL语句失败:" + aiResponse.getErrorMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String content = aiResponse.getContent().toString();
|
|
|
|
|
|
|
|
|
|
// 提取SQL语句
|
|
|
|
|
String sqlContent = extractSqlFromContent(content);
|
|
|
|
|
|
|
|
|
|
// 保存token使用记录
|
|
|
|
|
try {
|
|
|
|
|
processor.saveTokenUsage(
|
|
|
|
|
aiRequest.getMessageDetailType(),
|
|
|
|
|
prompt,
|
|
|
|
|
sqlContent,
|
|
|
|
|
aiResponse.getTokenUsage(),
|
|
|
|
|
aiRequest.getModelId(),
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
aiRequest.getSessionId(),
|
|
|
|
|
"0",
|
|
|
|
|
"1",
|
|
|
|
|
LoginHelper.getUserId(),
|
|
|
|
|
LoginHelper.getTenantId(),
|
|
|
|
|
LoginHelper.getDeptId()
|
|
|
|
|
);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
throw new RuntimeException("保存token使用记录失败" + e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sqlContent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -354,37 +908,120 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 专门处理 Markdown SQL 代码块
|
|
|
|
|
private static String extractSqlFromContent(String content) {
|
|
|
|
|
// 情况1:如果包含 ```sql ... ``` 格式
|
|
|
|
|
if (content.contains("```sql")) {
|
|
|
|
|
int start = content.indexOf("```sql") + 6; // 跳过 ```sql
|
|
|
|
|
int end = content.lastIndexOf("```");
|
|
|
|
|
/**
|
|
|
|
|
* 从AI响应内容中提取SQL语句
|
|
|
|
|
*/
|
|
|
|
|
private String extractSqlFromContent(String content) {
|
|
|
|
|
if (StringUtils.isBlank(content)) {
|
|
|
|
|
return "";
|
|
|
|
|
// return "SELECT 1 AS Error WHERE 1=0; -- 未生成SQL";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清理内容
|
|
|
|
|
String cleanedContent = content.trim();
|
|
|
|
|
|
|
|
|
|
// 1. 如果包含 ```sql ``` 标记,提取中间内容
|
|
|
|
|
if (cleanedContent.contains("```sql")) {
|
|
|
|
|
int start = cleanedContent.indexOf("```sql") + 6;
|
|
|
|
|
int end = cleanedContent.lastIndexOf("```");
|
|
|
|
|
if (end > start) {
|
|
|
|
|
content = content.substring(start, end).trim();
|
|
|
|
|
cleanedContent = cleanedContent.substring(start, end).trim();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 情况2:如果只有 ``` ... ```(没有 sql 标注)
|
|
|
|
|
if (content.contains("```")) {
|
|
|
|
|
int start = content.indexOf("```") + 3;
|
|
|
|
|
int end = content.lastIndexOf("```");
|
|
|
|
|
// 2. 如果包含 ``` 标记(无语言标识),提取中间内容
|
|
|
|
|
if (cleanedContent.contains("```")) {
|
|
|
|
|
int start = cleanedContent.indexOf("```") + 3;
|
|
|
|
|
int end = cleanedContent.lastIndexOf("```");
|
|
|
|
|
if (end > start) {
|
|
|
|
|
content = content.substring(start, end).trim();
|
|
|
|
|
cleanedContent = cleanedContent.substring(start, end).trim();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// content = extractSqlFromText(content);
|
|
|
|
|
content = content.replace("\\n", "\n") // 处理转义的换行符
|
|
|
|
|
.replace("\n", " ");
|
|
|
|
|
// 3. 移除可能的SQL解释或说明文本
|
|
|
|
|
String[] lines = cleanedContent.split("\n");
|
|
|
|
|
StringBuilder sqlBuilder = new StringBuilder();
|
|
|
|
|
boolean sqlStarted = false;
|
|
|
|
|
|
|
|
|
|
content = content.trim();
|
|
|
|
|
if ((content.startsWith("\"") && content.endsWith("\"")) ||
|
|
|
|
|
(content.startsWith("'") && content.endsWith("'"))) {
|
|
|
|
|
content = content.substring(1, content.length() - 1);
|
|
|
|
|
for (String line : lines) {
|
|
|
|
|
String trimmedLine = line.trim();
|
|
|
|
|
|
|
|
|
|
// 跳过空行和明显的非SQL行
|
|
|
|
|
if (trimmedLine.isEmpty() ||
|
|
|
|
|
trimmedLine.startsWith("--") ||
|
|
|
|
|
trimmedLine.startsWith("/*") ||
|
|
|
|
|
trimmedLine.startsWith("解释") ||
|
|
|
|
|
trimmedLine.startsWith("说明") ||
|
|
|
|
|
trimmedLine.startsWith("分析") ||
|
|
|
|
|
trimmedLine.toLowerCase().startsWith("here") ||
|
|
|
|
|
trimmedLine.toLowerCase().startsWith("this sql") ||
|
|
|
|
|
trimmedLine.toLowerCase().startsWith("the sql")) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return content.trim();
|
|
|
|
|
// 如果遇到SELECT/INSERT/UPDATE/DELETE,开始收集
|
|
|
|
|
if (!sqlStarted &&
|
|
|
|
|
(trimmedLine.toUpperCase().startsWith("SELECT") ||
|
|
|
|
|
trimmedLine.toUpperCase().startsWith("WITH") ||
|
|
|
|
|
trimmedLine.toUpperCase().startsWith("INSERT") ||
|
|
|
|
|
trimmedLine.toUpperCase().startsWith("UPDATE") ||
|
|
|
|
|
trimmedLine.toUpperCase().startsWith("DELETE"))) {
|
|
|
|
|
sqlStarted = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sqlStarted) {
|
|
|
|
|
sqlBuilder.append(line).append("\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String finalSql = sqlBuilder.toString().trim();
|
|
|
|
|
|
|
|
|
|
// 4. 确保SQL以分号结束
|
|
|
|
|
if (!finalSql.endsWith(";")) {
|
|
|
|
|
finalSql += ";";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. 如果是空SQL,返回安全查询
|
|
|
|
|
if (StringUtils.isBlank(finalSql) || finalSql.equals(";")) {
|
|
|
|
|
return "";
|
|
|
|
|
// return "SELECT 1 AS NoSQLGenerated WHERE 1=0; -- 未能生成有效SQL";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return finalSql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 可选:添加SQL验证方法(可选增强)
|
|
|
|
|
*/
|
|
|
|
|
private boolean validateGeneratedSQL(String sql) {
|
|
|
|
|
if (StringUtils.isBlank(sql)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 基础验证
|
|
|
|
|
String upperSql = sql.toUpperCase();
|
|
|
|
|
|
|
|
|
|
// 检查是否包含潜在危险操作
|
|
|
|
|
if (upperSql.contains("DROP ") ||
|
|
|
|
|
upperSql.contains("TRUNCATE ") ||
|
|
|
|
|
upperSql.contains("ALTER ") ||
|
|
|
|
|
upperSql.contains("CREATE ") ||
|
|
|
|
|
upperSql.contains("EXEC ") ||
|
|
|
|
|
upperSql.contains("EXECUTE ") ||
|
|
|
|
|
upperSql.contains("SP_") ||
|
|
|
|
|
upperSql.contains("XP_")) {
|
|
|
|
|
// log.warn("生成的SQL可能包含危险操作: {}", sql.substring(0, Math.min(sql.length(), 100)));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否是有效的SELECT查询(如果是查询场景)
|
|
|
|
|
if (!upperSql.contains("SELECT")) {
|
|
|
|
|
// log.warn("生成的SQL不是SELECT查询: {}", sql.substring(0, Math.min(sql.length(), 100)));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -407,34 +1044,38 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String naturalLanguageQuery = aiFillFormRequest.getNaturalLanguageQuery()+".";
|
|
|
|
|
String naturalLanguageQuery = aiFillFormRequest.getNaturalLanguageQuery();
|
|
|
|
|
List<AiFormSettingDetail> aiFormSettingDetailList = aiFillFormRequest.getFormSettingDetailList();
|
|
|
|
|
StringBuilder sb = new StringBuilder("你是一个智能表单填充助手。请根据我的要求、提供的数据库表结构信息,生成一份用于直接填充Vue3前端表单的JSON数据。\n\n");
|
|
|
|
|
|
|
|
|
|
sb.append("【要求】\n");
|
|
|
|
|
sb.append(naturalLanguageQuery).append(",\n\n");
|
|
|
|
|
|
|
|
|
|
sb.append("【数据库表信息】\n");
|
|
|
|
|
sb.append(getFormattedFormSettingDetails(aiFormSettingDetailList));
|
|
|
|
|
|
|
|
|
|
sb.append("\n【输出要求】\n");
|
|
|
|
|
sb.append("1. 请根据我的核心要求,智能推断并填充所有相关字段的值\n");
|
|
|
|
|
sb.append("2. 生成一个纯净的JSON对象,不要任何额外的解释、文本或markdown代码块标记,键是字段名,值是根据我的要求推断出来的值\n");
|
|
|
|
|
// sb.append("3. 请返回**纯净的JSON格式**数据,不要任何额外的解释、文本或markdown代码块标记\n");
|
|
|
|
|
sb.append("3. 仅返回一个JSON对象,不要返回JSON数组,不要返回任何测试数据和推测的数据\n");
|
|
|
|
|
sb.append("4. 其中的表名和字段名必须来自数据库表结构信息\n");
|
|
|
|
|
sb.append("5. 其他没有要求返回的数据返回空字符串\n");
|
|
|
|
|
String prompt = buildAiFillFormPrompt(
|
|
|
|
|
naturalLanguageQuery,
|
|
|
|
|
aiFormSettingDetailList
|
|
|
|
|
);
|
|
|
|
|
// StringBuilder sb = new StringBuilder("你是一个智能表单填充助手。请根据我的要求、提供的数据库表结构信息,生成一份用于直接填充Vue3前端表单的JSON数据。\n\n");
|
|
|
|
|
//
|
|
|
|
|
// sb.append("【要求】\n");
|
|
|
|
|
// sb.append(naturalLanguageQuery).append("\n\n");
|
|
|
|
|
//
|
|
|
|
|
// sb.append("【数据库表信息】\n");
|
|
|
|
|
// sb.append(getFormattedFormSettingDetails(aiFormSettingDetailList));
|
|
|
|
|
//
|
|
|
|
|
// sb.append("\n【输出要求】\n");
|
|
|
|
|
// sb.append("1. 请根据我的核心要求,智能推断并填充所有相关字段的值\n");
|
|
|
|
|
// sb.append("2. 生成一个纯净的JSON对象,不要任何额外的解释、文本或markdown代码块标记,键是字段名,值是根据我的要求推断出来的值\n");
|
|
|
|
|
//// sb.append("3. 请返回**纯净的JSON格式**数据,不要任何额外的解释、文本或markdown代码块标记\n");
|
|
|
|
|
// sb.append("3. 仅返回一个JSON对象,不要返回JSON数组,不要返回任何测试数据和推测的数据\n");
|
|
|
|
|
// sb.append("4. 其中的表名、字段名和条件中的字段名必须来自数据库表结构信息\n");
|
|
|
|
|
// sb.append("5. 其他没有要求返回的数据返回空字符串\n");
|
|
|
|
|
|
|
|
|
|
IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory
|
|
|
|
|
.getProcessorByPlatformId(aiFillFormRequest.getPlatformId());
|
|
|
|
|
AIMessage aiMessage = new AIMessage();
|
|
|
|
|
aiMessage.setRole("user");
|
|
|
|
|
aiMessage.setContent(sb.toString());
|
|
|
|
|
aiMessage.setContent(prompt);
|
|
|
|
|
|
|
|
|
|
Long modelId = aiFillFormRequest.getModelId();
|
|
|
|
|
AIRequest aiRequest = new AIRequest();
|
|
|
|
|
aiRequest.setMessages(Collections.singletonList(aiMessage));
|
|
|
|
|
aiRequest.setText(sb.toString());
|
|
|
|
|
aiRequest.setText(prompt);
|
|
|
|
|
aiRequest.setModelId(modelId);
|
|
|
|
|
|
|
|
|
|
Mono<AIResponse> response = processor.chat(aiRequest);
|
|
|
|
|
@ -442,7 +1083,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
|
|
|
|
|
String content = response.block().getContent().toString();
|
|
|
|
|
JSONObject contentJson = JSONObject.parseObject(content);
|
|
|
|
|
parseRelateTable(aiFormSettingDetailList, contentJson);
|
|
|
|
|
processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_FORM,sb.toString(), content, response.block().getTokenUsage(),
|
|
|
|
|
processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_FORM, prompt, content, response.block().getTokenUsage(),
|
|
|
|
|
modelId, null, null,
|
|
|
|
|
null, null, "0", "1", LoginHelper.getUserId(), LoginHelper.getTenantId(), LoginHelper.getDeptId());
|
|
|
|
|
|
|
|
|
|
@ -455,6 +1096,32 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构建AI提示词
|
|
|
|
|
*/
|
|
|
|
|
private String buildAiFillFormPrompt(String naturalLanguageQuery,
|
|
|
|
|
List<AiFormSettingDetail> formDetails) {
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
sb.append("你是一个智能表单填充助手。请根据以下要求填充表单:\n\n");
|
|
|
|
|
|
|
|
|
|
sb.append("【用户需求】\n")
|
|
|
|
|
.append(naturalLanguageQuery)
|
|
|
|
|
.append("\n\n");
|
|
|
|
|
|
|
|
|
|
sb.append("【表单结构】\n")
|
|
|
|
|
.append(getFormattedFormSettingDetails(formDetails))
|
|
|
|
|
.append("\n\n");
|
|
|
|
|
|
|
|
|
|
sb.append("【输出要求】\n")
|
|
|
|
|
.append("1. 根据需求推断并填充所有相关字段的值\n")
|
|
|
|
|
.append("2. 仅返回一个JSON对象,不要任何额外文本\n")
|
|
|
|
|
.append("3. 键名必须来自表单结构中的字段名\n")
|
|
|
|
|
.append("4. 未指定的字段返回空字符串\n")
|
|
|
|
|
.append("5. 不要随意添加标点符号");
|
|
|
|
|
|
|
|
|
|
return sb.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void parseRelateTable(List<AiFormSettingDetail> aiFormSettingDetailList, JSONObject contentJson) {
|
|
|
|
|
for (AiFormSettingDetail aiFormSettingDetail : aiFormSettingDetailList) {
|
|
|
|
|
if (aiFormSettingDetail.getSettingFlag().equals("1")) {
|
|
|
|
|
|