.gitignore
@@ -44,3 +44,7 @@ !*/build/*.java !*/build/*.html !*/build/*.xml ###################################################################### # Claude Code .claude/ doc/20260608_ÔÁÏÖʼìÈë¿â±ÈÀý×Ö¶Îǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,178 @@ # åæè´¨æ£å¢å å ¥åºæ¯ä¾å段å端èè°ææ¡£ ## ä¸ãåè½è¯´æ åæè´¨æ£å¢å "å ¥åºæ¯ä¾"åæ®µï¼ç¾åæ¯ï¼ãå½è´¨æ£å ¥åºæ¶ï¼å®é å ¥åºæ°é = åæ ¼æ°é Ã å ¥åºæ¯ä¾ / 100ã **示ä¾**ï¼ - åæ ¼æ°éï¼100 - å ¥åºæ¯ä¾ï¼80% - å®é å ¥åºæ°éï¼100 à 80 / 100 = 80 ## äºãæ°æ®åºåæ´ | åæ®µå | ç±»å | é»è®¤å¼ | 说æ | |--------|------|--------|------| | stock_in_ratio | DECIMAL(5,2) | 100.00 | å ¥åºæ¯ä¾(ç¾åæ¯)ï¼èå´0.00~100.00 | ## ä¸ãæ¥å£åæ´ ### 3.1 æ°å¢è´¨æ£å **æ¥å£å°å**ï¼`POST /quality/qualityInspect/add` **请æ±åæ°æ°å¢**ï¼ ```json { "stockInRatio": 80.00 } ``` | åæ°å | ç±»å | å¿ å¡« | 说æ | |--------|------|------|------| | stockInRatio | BigDecimal | å¦ | å ¥åºæ¯ä¾(ç¾åæ¯)ï¼é»è®¤100.00 | ### 3.2 ä¿®æ¹è´¨æ£å **æ¥å£å°å**ï¼`POST /quality/qualityInspect/update` **请æ±åæ°æ°å¢**ï¼åæ°å¢æ¥å£ ### 3.3 è¯¦æ æ¥è¯¢ **æ¥å£å°å**ï¼`GET /quality/qualityInspect/{id}` **ååºåæ°æ°å¢**ï¼ ```json { "data": { "id": 1, "stockInRatio": 80.00, "quantity": 100.00, "qualifiedQuantity": 100.00, "unqualifiedQuantity": 0.00, ... } } ``` ### 3.4 å页æ¥è¯¢ **æ¥å£å°å**ï¼`GET /quality/qualityInspect/listPage` **ååºåæ°æ°å¢**ï¼åè¯¦æ æ¥è¯¢ ### 3.5 æäº¤æ£éªï¼å ¥åºï¼ **æ¥å£å°å**ï¼`POST /quality/qualityInspect/submit` **请æ±åæ°**ï¼ ```json { "id": 1, "stockInRatio": 80.00 } ``` **å ¥åºé»è¾åæ´**ï¼ - åé»è¾ï¼å ¥åºæ°é = åæ ¼æ°é - æ°é»è¾ï¼å ¥åºæ°é = åæ ¼æ°é Ã å ¥åºæ¯ä¾ / 100 ### 3.6 æ¹éå¿«éæ£éª **æ¥å£å°å**ï¼`POST /quality/qualityInspect/batchQuickInspect` æ¹éå¿«éæ£éªæ¶ï¼å ¥åºæ¯ä¾ä½¿ç¨æ£éªåèªèº«ä¿åç `stockInRatio` å¼ï¼å¦æªè®¾ç½®åé»è®¤100%ã ## åãåç«¯åæ®µé ç½® ### 4.1 表ååæ®µï¼æ°å¢/ç¼è¾é¡µé¢ï¼ ```vue <el-form-item label="å ¥åºæ¯ä¾(%)" prop="stockInRatio"> <el-input-number v-model="form.stockInRatio" :precision="2" :min="0" :max="100" :step="1" placeholder="请è¾å ¥å ¥åºæ¯ä¾" /> </el-form-item> ``` **åæ®µé»è®¤å¼**ï¼ ```javascript data() { return { form: { stockInRatio: 100.00, // é»è®¤100% // ... å ¶ä»å段 } } } ``` ### 4.2 å表å±ç¤º ```vue <el-table-column label="å ¥åºæ¯ä¾" prop="stockInRatio" width="100"> <template #default="{ row }"> {{ row.stockInRatio ? row.stockInRatio + '%' : '100%' }} </template> </el-table-column> ``` ### 4.3 æ ¡éªè§å ```javascript rules: { stockInRatio: [ { required: false }, { validator: (rule, value, callback) => { if (value !== null && value !== undefined) { if (value < 0 || value > 100) { callback(new Error('å ¥åºæ¯ä¾èå´0~100')); } else { callback(); } } else { callback(); } }, trigger: 'blur' } ] } ``` ### 4.4 详æ å±ç¤º ```vue <el-descriptions-item label="å ¥åºæ¯ä¾"> {{ detail.stockInRatio ? detail.stockInRatio + '%' : '100%' }} </el-descriptions-item> ``` ## äºãä¸å¡è§å 1. **é»è®¤å¼**ï¼å ¥åºæ¯ä¾é»è®¤ä¸º100%ï¼å³å ¨é¨å ¥åº 2. **èå´éå¶**ï¼0.00 ~ 100.00ï¼æ¯æä¸¤ä½å°æ°ï¼ 3. **å ¥åºæ¶æº**ï¼è´¨æ£æäº¤æ¶è®¡ç®å ¥åºæ°é 4. **计ç®å ¬å¼**ï¼`å®é å ¥åºæ°é = åæ ¼æ°é Ã å ¥åºæ¯ä¾ / 100` 5. **精度å¤ç**ï¼åèäºå ¥ä¿ç2ä½å°æ° 6. **ä» å¯¹åæè´¨æ£çæ**ï¼inspectType = 0ï¼åæææ£éªï¼ ## å ãæ³¨æäºé¡¹ 1. å ¥åºæ¯ä¾ä» å½±åå ¥åºæ°éï¼ä¸å½±ååæ ¼æ°éåä¸åæ ¼æ°éçç»è®¡ 2. å·²æäº¤çè´¨æ£åä¸å¯ä¿®æ¹å ¥åºæ¯ä¾ 3. å¯¼åºæ¥è¡¨æ¶éå±ç¤ºå ¥åºæ¯ä¾å段 4. å ¥åºæ¯ä¾ä¸ºç©ºæå°äºçäº0æ¶ï¼èªå¨æ100%å¤ç ## ä¸ãåæ®µæ å° | åç«¯åæ®µ | åç«¯åæ®µ | æ°æ®åºå段 | ç±»å | |----------|----------|------------|------| | stockInRatio | stockInRatio | stock_in_ratio | BigDecimal(5,2) | doc/20260608_֪ʶ¿âRAGÏòÁ¿¼ìË÷¹¦ÄÜǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,626 @@ # ç¥è¯åºæä»¶ä¸ä¼ ä¸RAGåéæ£ç´¢åè½å端èè°ææ¡£ ## ä¸ãåè½æ¦è¿° ç¥è¯åºæ¨¡åæ°å¢æä»¶ä¸ä¼ åè½ï¼ä¸ä¼ çæä»¶ä¼å®æ¶è¿å ¥åéåºè¿è¡åçå¤çï¼é åAI模åå®ç°RAGï¼æ£ç´¢å¢å¼ºçæï¼é®çåè½ã ### åè½æ¨¡å 1. **ç¥è¯åºæä»¶ç®¡ç**ï¼ä½¿ç¨ç³»ç»å·²æçéä»¶ç®¡çæºå¶ä¸ä¼ æä»¶ 2. **åéæ£ç´¢å¤ç**ï¼æä»¶å 容èªå¨åçå¹¶åå ¥åéåºï¼Pineconeï¼ 3. **ç¥è¯åºé®ç**ï¼åºäºä¸ä¼ æä»¶å 容è¿è¡æºè½é®ç ## äºãæ°æ®åºåæ´ ### 2.1 æ°å¢ç¥è¯åºæä»¶åéè®°å½è¡¨ ```sql -- ç¥è¯åºæä»¶åéè®°å½è¡¨ï¼ç¨äºè·è¸ªåéåç¶æï¼ CREATE TABLE IF NOT EXISTS knowledge_base_vector ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主é®ID', knowledge_base_id BIGINT NOT NULL COMMENT 'å ³èç¥è¯åºID', storage_blob_id BIGINT NOT NULL COMMENT 'å ³èæä»¶blob ID', file_name VARCHAR(255) NOT NULL COMMENT 'æä»¶åç§°', file_type VARCHAR(50) NOT NULL COMMENT 'æä»¶ç±»å(docx/pdf/xlsx/txtç)', vector_status TINYINT DEFAULT 0 COMMENT 'åéåç¶æ: 0-å¾ å¤ç, 1-å¤çä¸, 2-已宿, 3-失败', vector_error VARCHAR(500) COMMENT 'åéå失败åå ', chunk_count INT DEFAULT 0 COMMENT 'åçæ°é', namespace VARCHAR(100) COMMENT 'åéå½å空é´', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', create_user INT COMMENT 'å建人', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', update_user INT COMMENT 'æ´æ°äºº', tenant_id BIGINT COMMENT 'ç§æ·ID', dept_id BIGINT COMMENT 'é¨é¨ID', INDEX idx_knowledge_base_id (knowledge_base_id), INDEX idx_storage_blob_id (storage_blob_id), INDEX idx_vector_status (vector_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ç¥è¯åºæä»¶åéè®°å½è¡¨'; ``` ### 2.2 ä¿®æ¹ç¥è¯åºè¡¨ ```sql -- ç¥è¯åºè¡¨å¢å åæ®µ ALTER TABLE knowledge_base ADD COLUMN file_count INT DEFAULT 0 COMMENT 'æä»¶æ°é', ADD COLUMN total_chunk_count INT DEFAULT 0 COMMENT 'æ»åçæ°é', ADD COLUMN description VARCHAR(500) COMMENT 'ç¥è¯åºæè¿°'; ``` ## ä¸ãæ¥å£è®¾è®¡ ### 3.1 æä»¶ä¸ä¼ ï¼ä½¿ç¨ç³»ç»å·²ææ¥å£ï¼ **æ¥å£å°å**ï¼`POST /common/upload` **è¯·æ±æ¹å¼**ï¼multipart/form-data **请æ±åæ°**ï¼ | åæ°å | ç±»å | å¿ å¡« | 说æ | |--------|------|------|------| | files | MultipartFile[] | æ¯ | ä¸ä¼ çæä»¶å表 | **ååºç»æ**ï¼ ```json { "code": 200, "msg": "æä½æå", "data": [ { "id": 123, "name": "æä½æå.docx", "url": "/profile/upload/20260608/xxx.docx", "previewURL": "/common/preview/xxx?token=yyy", "downloadURL": "/common/download/xxx?token=yyy", "storageAttachmentId": null } ] } ``` ### 3.2 ä¿åç¥è¯åºæä»¶å ³è ä¸ä¼ 宿åï¼è°ç¨éä»¶ä¿åæ¥å£å ³èæä»¶å°ç¥è¯åºã **æ¥å£å°å**ï¼`POST /storageAttachment/add` **请æ±åæ°**ï¼ ```json { "recordType": "knowledge_base", "recordId": 10, "application": "rag_file", "storageBlobDTOs": [ { "id": 123 }, { "id": 124 } ] } ``` | åæ°å | ç±»å | å¿ å¡« | 说æ | |--------|------|------|------| | recordType | String | æ¯ | åºå®å¼ `knowledge_base` | | recordId | Long | æ¯ | ç¥è¯åºID | | application | String | æ¯ | åºå®å¼ `rag_file`ï¼æ è¯RAGæä»¶ | | storageBlobDTOs | List | æ¯ | ä¸ä¼ è¿åçæä»¶blobå表 | **ååºç»æ**ï¼ ```json { "code": 200, "msg": "æä½æå" } ``` ### 3.3 ç¥è¯åºæä»¶å表 **æ¥å£å°å**ï¼`GET /storageAttachment/list` **请æ±åæ°**ï¼ | åæ°å | ç±»å | å¿ å¡« | 说æ | |--------|------|------|------| | recordType | String | æ¯ | åºå®å¼ `knowledge_base` | | recordId | Long | æ¯ | ç¥è¯åºID | | application | String | å¦ | åºå®å¼ `rag_file` | **ååºç»æ**ï¼ ```json { "code": 200, "data": [ { "id": 1, "storageBlobId": 123, "name": "æä½æå.docx", "url": "/profile/upload/20260608/xxx.docx", "previewURL": "/common/preview/xxx?token=yyy", "downloadURL": "/common/download/xxx?token=yyy", "createTime": "2026-06-08 10:00:00" } ] } ``` ### 3.4 ç¥è¯åºæä»¶å é¤ **æ¥å£å°å**ï¼`DELETE /storageAttachment/delete` **请æ±åæ°**ï¼ ```json { "ids": [1, 2, 3] } ``` **ååºç»æ**ï¼ ```json { "code": 200, "msg": "æä½æå" } ``` ### 3.5 æ¥è¯¢åéåç¶æï¼æ°å¢æ¥å£ï¼ **æ¥å£å°å**ï¼`GET /knowledgeBase/vector/status/{knowledgeBaseId}` **ååºç»æ**ï¼ ```json { "code": 200, "data": [ { "id": 1, "storageBlobId": 123, "fileName": "æä½æå.docx", "fileType": "docx", "vectorStatus": 2, "chunkCount": 15, "namespace": "kb-10" } ] } ``` ### 3.6 éæ°åéåæä»¶ **æ¥å£å°å**ï¼`POST /knowledgeBase/vector/reprocess/{vectorId}` **ååºç»æ**ï¼ ```json { "code": 200, "msg": "已鿰æäº¤åéåä»»å¡" } ``` ### 3.7 ç¥è¯åºé®çæ¥å£ **æ¥å£å°å**ï¼`POST /ai/knowledge/chat`ï¼æµå¼è¿åï¼ **请æ±åæ°**ï¼ ```json { "knowledgeBaseId": 10, "memoryId": "session-xxx", "question": "å¦ä½æä½å®¡æ¹æµç¨ï¼" } ``` | åæ°å | ç±»å | å¿ å¡« | 说æ | |--------|------|------|------| | knowledgeBaseId | Long | æ¯ | ç¥è¯åºID | | memoryId | String | æ¯ | ä¼è¯IDï¼ç¨äºä¿æä¸ä¸æ | | question | String | æ¯ | ç¨æ·æé®å 容 | **ååºç»æ**ï¼æµå¼è¿å `text/stream;charset=utf-8`ï¼ï¼ ``` æ ¹æ®ç¥è¯åºå 容ï¼å®¡æ¹æµç¨çæä½æ¥éª¤å¦ä¸ï¼ 1. ç»å½ç³»ç»åè¿å ¥å®¡æ¹ç®¡ç模å... ``` ### 3.8 ç¥è¯åºé®çä¼è¯åå² **æ¥å£å°å**ï¼`GET /ai/knowledge/history/{memoryId}` **ååºç»æ**ï¼ ```json { "code": 200, "data": [ { "role": "user", "content": "å¦ä½æä½å®¡æ¹æµç¨ï¼", "createTime": "2026-06-08 10:00:00" }, { "role": "assistant", "content": "æ ¹æ®ç¥è¯åºå 容...", "createTime": "2026-06-08 10:01:00" } ] } ``` ## åãæä»¶ç±»åæ¯æ | æä»¶ç±»å | æ©å±å | 说æ | |----------|--------|------| | Wordææ¡£ | .docx | æ¯æææ¬æå | | Excelè¡¨æ ¼ | .xlsx, .xls | æ¯æè¡¨æ ¼å 容æå | | PDFææ¡£ | .pdf | æ¯æPDFææ¬æå | | ææ¬æä»¶ | .txt, .md, .json, .csv | ç´æ¥è¯»åå 容 | | ä»£ç æä»¶ | .java, .js, .vue, .html, .sqlç | ç´æ¥è¯»åå 容 | **æä»¶å¤§å°éå¶**ï¼åæä»¶æå¤§10MBï¼ç³»ç»é»è®¤éå¶ï¼ ## äºãåéåç¶æè¯´æ | ç¶æå¼ | ç¶æåç§° | 说æ | |--------|----------|------| | 0 | å¾ å¤ç | æä»¶å·²ä¸ä¼ ï¼çå¾ åéåå¤ç | | 1 | å¤çä¸ | æ£å¨è¿è¡åéåçå¤ç | | 2 | 已宿 | åéå宿ï¼å¯è¿è¡æ£ç´¢é®ç | | 3 | 失败 | åéå失败ï¼ééæ°å¤ç | ## å ãå端ç»ä»¶è®¾è®¡ ### 6.1 æä»¶ä¸ä¼ ç»ä»¶ï¼ä½¿ç¨ç³»ç»å·²æä¸ä¼ ï¼ ```vue <template> <div class="knowledge-file-upload"> <!-- ç¥è¯åºéæ© --> <el-select v-model="selectedKnowledgeBase" placeholder="éæ©ç¥è¯åº" style="width: 200px"> <el-option v-for="kb in knowledgeBaseList" :key="kb.id" :label="kb.title" :value="kb.id" /> </el-select> <!-- 使ç¨ç³»ç»å·²æä¸ä¼ ç»ä»¶ --> <el-upload :action="uploadUrl" :headers="uploadHeaders" :on-success="handleUploadSuccess" :before-upload="beforeUpload" :accept="acceptTypes" :limit="10" :file-list="fileList" multiple > <el-button type="primary">ç¹å»ä¸ä¼ </el-button> <template #tip> <div class="el-upload__tip"> æ¯æ docxãxlsxãpdfãtxt çæ ¼å¼ï¼åæä»¶ä¸è¶ è¿10MB </div> </template> </el-upload> </div> </template> <script setup> import { ref } from 'vue' import { getToken } from '@/utils/auth' import request from '@/utils/request' const uploadUrl = '/common/upload' const uploadHeaders = { Authorization: 'Bearer ' + getToken() } const acceptTypes = '.docx,.xlsx,.xls,.pdf,.txt,.md,.json,.csv' const uploadedBlobs = ref([]) const beforeUpload = (file) => { const maxSize = 10 * 1024 * 1024 if (file.size > maxSize) { ElMessage.error('æä»¶å¤§å°ä¸è½è¶ è¿10MB') return false } return true } // ä¸ä¼ æååä¿åéä»¶å ³è const handleUploadSuccess = async (response, file) => { if (response.code === 200) { uploadedBlobs.value.push(...response.data) // è°ç¨éä»¶ä¿åæ¥å£ï¼å ³èå°ç¥è¯åº await saveAttachment() ElMessage.success('æä»¶ä¸ä¼ æåï¼æ£å¨å¤çåéå...') refreshVectorStatus() } else { ElMessage.error(response.msg) } } // ä¿åéä»¶å ³èå°ç¥è¯åº const saveAttachment = async () => { await request.post('/storageAttachment/add', { recordType: 'knowledge_base', recordId: selectedKnowledgeBase.value, application: 'rag_file', storageBlobDTOs: uploadedBlobs.value.map(b => ({ id: b.id })) }) uploadedBlobs.value = [] } </script> ``` ### 6.2 æä»¶å表ç»ä»¶ ```vue <template> <el-table :data="fileList" v-loading="loading"> <el-table-column prop="name" label="æä»¶å" width="200" /> <el-table-column prop="fileType" label="ç±»å" width="80"> <template #default="{ row }"> {{ getFileType(row.name) }} </template> </el-table-column> <el-table-column prop="vectorStatus" label="åéåç¶æ" width="120"> <template #default="{ row }"> <el-tag :type="getVectorStatusType(row.vectorStatus)"> {{ getVectorStatusText(row.vectorStatus) }} </el-tag> </template> </el-table-column> <el-table-column prop="chunkCount" label="åçæ°" width="80" /> <el-table-column prop="createTime" label="ä¸ä¼ æ¶é´" width="160" /> <el-table-column label="æä½" width="180"> <template #default="{ row }"> <el-button type="primary" size="small" link @click="previewFile(row)">é¢è§</el-button> <el-button type="primary" size="small" link @click="downloadFile(row)">ä¸è½½</el-button> <el-button v-if="row.vectorStatus === 3" type="warning" size="small" link @click="revectorFile(row)" >éæ°å¤ç</el-button> <el-button type="danger" size="small" link @click="deleteFile(row)">å é¤</el-button> </template> </el-table-column> </el-table> </template> <script setup> import request from '@/utils/request' const getFileType = (name) => { return name?.split('.').pop() || '' } const getVectorStatusText = (status) => { const statusMap = { 0: 'å¾ å¤ç', 1: 'å¤çä¸', 2: '已宿', 3: '失败' } return statusMap[status] || 'æªç¥' } const getVectorStatusType = (status) => { const typeMap = { 0: 'info', 1: 'warning', 2: 'success', 3: 'danger' } return typeMap[status] || 'info' } const previewFile = (row) => { window.open(row.previewURL, '_blank') } const downloadFile = (row) => { window.open(row.downloadURL, '_blank') } const deleteFile = async (row) => { await request.delete('/storageAttachment/delete', { data: [row.id] }) ElMessage.success('å 餿å') refreshFileList() } const revectorFile = async (row) => { await request.post(`/knowledgeBase/vector/reprocess/${row.vectorId}`) ElMessage.success('已鿰æäº¤åéåä»»å¡') refreshVectorStatus() } </script> ``` ### 6.3 ç¥è¯åºé®çç»ä»¶ ```vue <template> <div class="knowledge-chat"> <!-- ç¥è¯åºéæ© --> <div class="kb-selector"> <el-select v-model="selectedKnowledgeBase" placeholder="éæ©ç¥è¯åº"> <el-option v-for="kb in knowledgeBaseList" :key="kb.id" :label="kb.title" :value="kb.id" /> </el-select> </div> <!-- è天åºå --> <div class="chat-container"> <div class="message-list" ref="messageList"> <div v-for="msg in messages" :key="msg.id" :class="['message', msg.role]"> <div class="message-content">{{ msg.content }}</div> </div> <div v-if="streamingContent" class="message assistant"> <div class="message-content">{{ streamingContent }}</div> </div> </div> <!-- è¾å ¥åºå --> <div class="input-area"> <el-input v-model="question" placeholder="è¾å ¥é®é¢ï¼åºäºç¥è¯åºå 容åç..." @keyup.enter="sendQuestion" /> <el-button type="primary" @click="sendQuestion" :loading="sending">åé</el-button> </div> </div> </div> </template> <script setup> import { ref, nextTick } from 'vue' import { getToken } from '@/utils/auth' const selectedKnowledgeBase = ref(null) const question = ref('') const messages = ref([]) const sending = ref(false) const streamingContent = ref('') const memoryId = ref('kb-chat-' + Date.now()) const sendQuestion = async () => { if (!question.value.trim() || !selectedKnowledgeBase.value) return sending.value = true streamingContent.value = '' messages.value.push({ id: Date.now(), role: 'user', content: question.value }) try { const response = await fetch('/ai/knowledge/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + getToken() }, body: JSON.stringify({ knowledgeBaseId: selectedKnowledgeBase.value, memoryId: memoryId.value, question: question.value }) }) // æµå¼è¯»åååº const reader = response.body.getReader() const decoder = new TextDecoder() while (true) { const { done, value } = await reader.read() if (done) break streamingContent.value += decoder.decode(value, { stream: true }) // æ»å¨å°åºé¨ nextTick(() => { const list = document.querySelector('.message-list') if (list) list.scrollTop = list.scrollHeight }) } messages.value.push({ id: Date.now(), role: 'assistant', content: streamingContent.value }) streamingContent.value = '' } catch (error) { ElMessage.error('é®ç请æ±å¤±è´¥') } finally { sending.value = false question.value = '' } } </script> <style scoped> .knowledge-chat { height: 100%; display: flex; flex-direction: column; } .kb-selector { padding: 10px; border-bottom: 1px solid #eee; } .chat-container { flex: 1; display: flex; flex-direction: column; } .message-list { flex: 1; overflow-y: auto; padding: 10px; } .message { margin-bottom: 10px; padding: 10px; border-radius: 8px; } .message.user { background: #e6f7ff; text-align: right; } .message.assistant { background: #f5f5f5; } .input-area { padding: 10px; display: flex; gap: 10px; } </style> ``` ## ä¸ãä¸å¡æµç¨ ### 7.1 æä»¶ä¸ä¼ æµç¨ ``` å端è°ç¨ /common/upload ä¸ä¼ æä»¶ â è·å StorageBlobVO å表ï¼å å«blobIdãé¢è§URLãä¸è½½URLï¼ â è°ç¨ /storageAttachment/add å ³èæä»¶å°ç¥è¯åº â å端çå¬éä»¶ä¿åäºä»¶ â æåæä»¶ææ¬ â åç â åéå â åå ¥ Pinecone åéåºï¼å½å空é´: kb-{knowledgeBaseId}) â æ´æ° knowledge_base_vector è¡¨ç¶æä¸ºå·²å®æ ``` ### 7.2 ç¥è¯åºé®çæµç¨ ``` ç¨æ·æé® â è°ç¨ Embedding 模å对é®é¢åéå â å¨ Pinecone 䏿£ç´¢ï¼å½å空é´: kb-{knowledgeBaseId}) â è·åç¸å ³åçææ¬ â ä½ä¸ºä¸ä¸æ + ç¨æ·é®é¢åç» AI 模å â AI æµå¼çæåç â è¿åå端 ``` ## å «ãææ¯å®ç°è¦ç¹ ### 8.1 ææ¬åççç¥ - **åç大å°**ï¼é»è®¤æ¯ç 500 å符 - **éå 大å°**ï¼é»è®¤ 100 å符éå ï¼ä¿è¯è¯ä¹è¿è´¯ - **åçå æ°æ®**ï¼å 嫿件IDãç¥è¯åºIDãåçç´¢å¼ ### 8.2 åéå½åç©ºé´ æ¯ä¸ªç¥è¯åºä½¿ç¨ç¬ç«å½å空é´ï¼`kb-{knowledgeBaseId}` ### 8.3 éä»¶å ³èåæ° | åæ° | å¼ | 说æ | |------|-----|------| | recordType | `knowledge_base` | 使ç¨å·²ææä¸¾ | | application | `rag_file` | æ è¯ä¸ºRAGæä»¶ | ## ä¹ã注æäºé¡¹ 1. æä»¶ä¸ä¼ 使ç¨ç³»ç»å·²æç `/common/upload` å `/storageAttachment/add` æ¥å£ 2. å 餿件æ¶åæ¥å é¤åéåºä¸çç¸å ³åç 3. 大æä»¶åéåå¯è½èæ¶è¾é¿ï¼å端éè½®è¯¢ç¶æææ¾ç¤ºè¿åº¦ 4. ç¥è¯åºé®çä¾èµåéæ£ç´¢è´¨éï¼å»ºè®®ä¼ååççç¥ 5. ä¸åç¥è¯åºä½¿ç¨ä¸åå½å空é´ï¼é¿å æ°æ®æ··æ· ## åãé误ç 说æ | é误ç | 说æ | |--------|------| | 40001 | æä»¶ç±»å䏿¯æ | | 40002 | æä»¶å¤§å°è¶ åºéå¶ | | 40003 | ç¥è¯åºä¸åå¨ | | 50001 | æä»¶ä¸ä¼ 失败 | | 50002 | æä»¶å 容æå失败 | | 50003 | åéåå¤ç失败 | | 50004 | åéæ£ç´¢å¤±è´¥ | doc/20260608_֪ʶ¿âÏòÁ¿¼ìË÷¹¦ÄÜ.sql
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,34 @@ -- 20260608_ç¥è¯åºåéæ£ç´¢åè½.sql -- ç¥è¯åºRAGåéæ£ç´¢åè½æ°æ®åºåæ´ -- å建ç¥è¯åºæä»¶åéè®°å½è¡¨ï¼ç¨äºè·è¸ªåéåç¶æï¼ CREATE TABLE IF NOT EXISTS knowledge_base_vector ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主é®ID', knowledge_base_id BIGINT NOT NULL COMMENT 'å ³èç¥è¯åºID', storage_blob_id BIGINT NOT NULL COMMENT 'å ³èæä»¶blob ID', file_name VARCHAR(255) NOT NULL COMMENT 'æä»¶åç§°', file_type VARCHAR(50) NOT NULL COMMENT 'æä»¶ç±»å(docx/pdf/xlsx/txtç)', vector_status TINYINT DEFAULT 0 COMMENT 'åéåç¶æ: 0-å¾ å¤ç, 1-å¤çä¸, 2-已宿, 3-失败', vector_error VARCHAR(500) COMMENT 'åéå失败åå ', chunk_count INT DEFAULT 0 COMMENT 'åçæ°é', namespace VARCHAR(100) COMMENT 'åéå½å空é´', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', create_user INT COMMENT 'å建人', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', update_user INT COMMENT 'æ´æ°äºº', tenant_id BIGINT COMMENT 'ç§æ·ID', dept_id BIGINT COMMENT 'é¨é¨ID', INDEX idx_knowledge_base_id (knowledge_base_id), INDEX idx_storage_blob_id (storage_blob_id), INDEX idx_vector_status (vector_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ç¥è¯åºæä»¶åéè®°å½è¡¨'; -- ç¥è¯åºè¡¨å¢å åæ®µ ALTER TABLE knowledge_base ADD COLUMN file_count INT DEFAULT 0 COMMENT 'æä»¶æ°é', ADD COLUMN total_chunk_count INT DEFAULT 0 COMMENT 'æ»åçæ°é', ADD COLUMN description VARCHAR(500) COMMENT 'ç¥è¯åºæè¿°'; -- 注æï¼éä»¶å ³è使ç¨ç³»ç»å·²æç storage_attachment 表 -- recordType: knowledge_baseï¼å·²å¨ RecordTypeEnum ä¸å®ä¹ï¼ -- application: rag_fileï¼åç«¯ä¼ åæ¶ä½¿ç¨ï¼ src/main/java/com/ruoyi/ai/assistant/KnowledgeChatAgent.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,35 @@ package com.ruoyi.ai.assistant; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.spring.AiService; import reactor.core.publisher.Flux; import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT; /** * ç¥è¯åºé®çAgent * åºäºRAGæ£ç´¢å¢å¼ºçæ */ @AiService( wiringMode = EXPLICIT, streamingChatModel = "qwenStreamingChatModel", chatMemoryProvider = "chatMemoryProvider" ) public interface KnowledgeChatAgent { @SystemMessage(""" ä½ æ¯ä¼ä¸ç¥è¯åºé®ç婿ã ä½ éè¦åºäºæä¾çç¥è¯åºå 容åçç¨æ·é®é¢ã éµå¾ªä»¥ä¸è§åï¼ 1. ä¸¥æ ¼åºäºç¥è¯åºå 容åçï¼ä¸è¦ç¼é ä¿¡æ¯ 2. 妿ç¥è¯åºä¸æ²¡æç¸å ³ä¿¡æ¯ï¼æç¡®åç¥ç¨æ· 3. åçè¦åç¡®ãç®æ´ãææ¡ç 4. 妿å 容è¾å¤ï¼ä½¿ç¨åç¹åè¡¨å½¢å¼ 5. å¼ç¨æ¥æºæ¶æ³¨æ"æ ¹æ®ç¥è¯åºå 容" """) Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage); } src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java
@@ -12,6 +12,9 @@ import org.springframework.context.annotation.Configuration; /** * AI Agent é 置类 * ç¥è¯åºæ£ç´¢ä½¿ç¨æ°æ®åºç®¡ççåéæ°æ®ï¼éè¿ KnowledgeBaseVector 表管çæä»¶åéè®°å½ * * @author :yys * @date : 2025/5/2 20:01 */ @@ -26,15 +29,6 @@ @Autowired private EmbeddingModel embeddingModel; // @Value("${knowledge.one}") // private String one; // // @Value("${knowledge.two}") // private String two; // // @Value("${knowledge.three}") // private String three; @Bean ChatMemoryProvider chatMemoryProviderXiaozhi() { return memoryId -> MessageWindowChatMemory.builder() @@ -44,48 +38,19 @@ .build(); } // @Bean // ContentRetriever contentRetrieverXiaozhi() { // //使ç¨FileSystemDocumentLoader读åæå®ç®å½ä¸çç¥è¯åºææ¡£ // //并使ç¨é»è®¤çææ¡£è§£æå¨å¯¹ææ¡£è¿è¡è§£æ // Document document1 = FileSystemDocumentLoader.loadDocument(one); //// Document document2 = FileSystemDocumentLoader.loadDocument(two); //// Document document3 = FileSystemDocumentLoader.loadDocument(three); //// List<Document> documents = Arrays.asList(document1, document2, document3); // // List<Document> documents = Collections.singletonList(document1); //// 2. å°æ°æ®åºæ°æ®è½¬ä¸ºLangChain4jçDocument对象 //// List<Document> documents = new ArrayList<>(); // // //使ç¨å ååéåå¨ // InMemoryEmbeddingStore<TextSegment> inMemoryEmbeddingStore = new InMemoryEmbeddingStore<>(); // //使ç¨é»è®¤çææ¡£åå²å¨ // EmbeddingStoreIngestor.builder() // .embeddingModel(embeddingModel) // .embeddingStore(inMemoryEmbeddingStore) // .build() // .ingest(documents); // //ä»åµå ¥åå¨ï¼EmbeddingStoreï¼éæ£ç´¢åæ¥è¯¢å 容ç¸å ³çä¿¡æ¯ // return EmbeddingStoreContentRetriever.builder() // .embeddingModel(embeddingModel) // .embeddingStore(inMemoryEmbeddingStore) // .build(); // } /** * ç¥è¯åºå 容æ£ç´¢å¨ * ä»åéæ°æ®åºï¼Pineconeï¼æ£ç´¢å·²åéåçç¥è¯åºå 容 * ç¥è¯åºæä»¶éè¿ KnowledgeBaseVector 表管çï¼ç± KnowledgeRagService å¤çåéå */ @Bean ContentRetriever contentRetrieverXiaozhiPincone() { // å建ä¸ä¸ª EmbeddingStoreContentRetriever 对象ï¼ç¨äºä»åµå ¥åå¨ä¸æ£ç´¢å 容 ContentRetriever contentRetrieverXiaozhi() { return EmbeddingStoreContentRetriever .builder() // 设置ç¨äºçæåµå ¥åéçåµå ¥æ¨¡å .embeddingModel(embeddingModel) // æå®è¦ä½¿ç¨çåµå ¥åå¨ .embeddingStore(embeddingStore) // 设置æå¤§æ£ç´¢ç»ææ°éï¼è¿é表示æå¤è¿å 1 æ¡å¹é ç»æ .maxResults(1) // 设置æå°å¾åéå¼ï¼åªæå¾å大äºçäº 0.8 çç»ææä¼è¢«è¿å .minScore(0.8) // æå»ºæç»ç EmbeddingStoreContentRetriever å®ä¾ .build(); } } src/main/java/com/ruoyi/ai/controller/KnowledgeChatController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,91 @@ package com.ruoyi.ai.controller; import com.ruoyi.ai.assistant.KnowledgeChatAgent; import com.ruoyi.ai.dto.KnowledgeChatRequest; import com.ruoyi.ai.service.KnowledgeRagService; import com.ruoyi.approve.pojo.KnowledgeBase; import com.ruoyi.approve.service.KnowledgeBaseService; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.framework.security.LoginUser; import com.ruoyi.framework.web.domain.AjaxResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import java.util.List; /** * ç¥è¯åºé®çController */ @Slf4j @RestController @RequestMapping("/ai/knowledge") @RequiredArgsConstructor @Tag(name = "ç¥è¯åºé®ç") public class KnowledgeChatController { private final KnowledgeChatAgent knowledgeChatAgent; private final KnowledgeRagService knowledgeRagService; private final KnowledgeBaseService knowledgeBaseService; /** * ç¥è¯åºé®çï¼æµå¼è¿åï¼ */ @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8") @Operation(summary = "ç¥è¯åºé®ç") public Flux<String> chat(@RequestBody KnowledgeChatRequest request) { if (request.getKnowledgeBaseId() == null) { return Flux.just("ç¥è¯åºIDä¸è½ä¸ºç©º"); } if (!StringUtils.hasText(request.getMemoryId())) { return Flux.just("ä¼è¯IDä¸è½ä¸ºç©º"); } if (!StringUtils.hasText(request.getQuestion())) { return Flux.just("é®é¢ä¸è½ä¸ºç©º"); } // æ£æ¥ç¥è¯åºæ¯å¦åå¨ KnowledgeBase knowledgeBase = knowledgeBaseService.getById(request.getKnowledgeBaseId()); if (knowledgeBase == null) { return Flux.just("ç¥è¯åºä¸åå¨"); } // æå»ºå½åç©ºé´ String namespace = "kb-" + request.getKnowledgeBaseId(); // æ£ç´¢ç¸å ³å 容 List<String> relevantContents = knowledgeRagService.searchRelevantContent( namespace, request.getQuestion(), 5); if (relevantContents.isEmpty()) { return Flux.just("ç¥è¯åºä¸æªæ¾å°ç¸å ³å 容ï¼è¯·å ä¸ä¼ ç¸å ³ææ¡£ã"); } // æå»ºä¸ä¸æ StringBuilder contextBuilder = new StringBuilder(); contextBuilder.append("以䏿¯ä»ç¥è¯åºä¸æ£ç´¢å°çç¸å ³å 容ï¼\n\n"); for (int i = 0; i < relevantContents.size(); i++) { contextBuilder.append("ãå 容").append(i + 1).append("ã\n"); contextBuilder.append(relevantContents.get(i)).append("\n\n"); } contextBuilder.append("---\n"); contextBuilder.append("请åºäºä»¥ä¸ç¥è¯åºå 容åçç¨æ·é®é¢ï¼\n"); contextBuilder.append(request.getQuestion()); // è°ç¨AIçæåç return knowledgeChatAgent.chat(request.getMemoryId(), contextBuilder.toString()); } /** * ç¥è¯åºå表ï¼ç¨äºéæ©ç¥è¯åºï¼ */ @GetMapping("/list") @Operation(summary = "ç¥è¯åºå表") public AjaxResult listKnowledgeBases() { List<KnowledgeBase> list = knowledgeBaseService.list(); return AjaxResult.success(list); } } src/main/java/com/ruoyi/ai/dto/KnowledgeChatRequest.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,21 @@ package com.ruoyi.ai.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** * ç¥è¯åºé®çè¯·æ± */ @Data @Schema(description = "ç¥è¯åºé®ç请æ±") public class KnowledgeChatRequest { @Schema(description = "ç¥è¯åºID", required = true) private Long knowledgeBaseId; @Schema(description = "ä¼è¯IDï¼ç¨äºä¿æä¸ä¸æ", required = true) private String memoryId; @Schema(description = "ç¨æ·æé®å 容", required = true) private String question; } src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,34 @@ package com.ruoyi.ai.service; import java.util.List; /** * ç¥è¯åºRAGæå¡ * è´è´£æä»¶åéåå¤çåæ£ç´¢ */ public interface KnowledgeRagService { /** * 弿¥å¤çåéå */ void processVectorAsync(Long vectorId); /** * 忥å¤çåéå */ void processVector(Long vectorId); /** * æ£ç´¢ç¸å ³å 容 * @param namespace å½åç©ºé´ * @param query æ¥è¯¢ææ¬ * @param maxResults æå¤§ç»ææ° * @return ç¸å ³å 容å表 */ List<String> searchRelevantContent(String namespace, String query, int maxResults); /** * å 餿宿件çåéæ°æ® */ void deleteEmbeddings(String namespace, Long storageBlobId); } src/main/java/com/ruoyi/ai/service/impl/KnowledgeRagServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,274 @@ package com.ruoyi.ai.service.impl; import com.ruoyi.ai.service.AiFileTextExtractor; import com.ruoyi.ai.service.KnowledgeRagService; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import com.ruoyi.approve.service.KnowledgeBaseVectorService; import com.ruoyi.basic.pojo.StorageBlob; import com.ruoyi.basic.service.StorageBlobService; import com.ruoyi.common.config.FileProperties; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingMatch; import dev.langchain4j.store.embedding.EmbeddingSearchRequest; import dev.langchain4j.store.embedding.EmbeddingSearchResult; import dev.langchain4j.store.embedding.EmbeddingStore; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.io.File; import java.nio.file.Files; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * ç¥è¯åºRAGæå¡å®ç° */ @Slf4j @Service @RequiredArgsConstructor public class KnowledgeRagServiceImpl implements KnowledgeRagService { private final KnowledgeBaseVectorService knowledgeBaseVectorService; private final StorageBlobService storageBlobService; private final AiFileTextExtractor aiFileTextExtractor; private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; private final FileProperties fileProperties; private static final int CHUNK_SIZE = 500; private static final int CHUNK_OVERLAP = 100; @Override @Async public void processVectorAsync(Long vectorId) { processVector(vectorId); } @Override public void processVector(Long vectorId) { KnowledgeBaseVector vector = knowledgeBaseVectorService.getById(vectorId); if (vector == null) { log.error("åéè®°å½ä¸åå¨: {}", vectorId); return; } try { // æ´æ°ç¶æä¸ºå¤çä¸ knowledgeBaseVectorService.updateVectorStatus(vectorId, KnowledgeBaseVector.STATUS_PROCESSING, null, null); // è·åæä»¶å 容 StorageBlob blob = storageBlobService.getById(vector.getStorageBlobId()); if (blob == null) { throw new RuntimeException("æä»¶ä¸åå¨: " + vector.getStorageBlobId()); } File file = getFile(blob); // ç´æ¥è¯»åæä»¶å 容ï¼ä¸ä½¿ç¨ MultipartFile å è£ String content = extractFileContent(file, vector.getFileName(), blob.getContentType()); if (content == null || content.trim().isEmpty()) { throw new RuntimeException("æä»¶å 容为空"); } // ææ¬åç List<TextSegment> chunks = splitText(content, vector); // æ¹éçæåµå ¥åéå¹¶åå¨ int chunkCount = 0; for (TextSegment chunk : chunks) { Embedding embedding = embeddingModel.embed(chunk).content(); embeddingStore.add(embedding, chunk); chunkCount++; } // æ´æ°ç¶æä¸ºå®æ knowledgeBaseVectorService.updateVectorStatus(vectorId, KnowledgeBaseVector.STATUS_COMPLETED, chunkCount, null); log.info("åéåå¤ç宿: vectorId={}, chunkCount={}", vectorId, chunkCount); } catch (Exception e) { log.error("åéåå¤ç失败: vectorId={}", vectorId, e); knowledgeBaseVectorService.updateVectorStatus(vectorId, KnowledgeBaseVector.STATUS_FAILED, null, e.getMessage()); } } @Override public List<String> searchRelevantContent(String namespace, String query, int maxResults) { try { // çææ¥è¯¢åé Embedding queryEmbedding = embeddingModel.embed(query).content(); // æå»ºæç´¢è¯·æ±ï¼ä½¿ç¨å æ°æ®è¿æ»¤ EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(maxResults) .minScore(0.7) .build(); EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest); return searchResult.matches().stream() .map(match -> match.embedded().text()) .collect(Collectors.toList()); } catch (Exception e) { log.error("åéæ£ç´¢å¤±è´¥: namespace={}", namespace, e); return new ArrayList<>(); } } @Override public void deleteEmbeddings(String namespace, Long storageBlobId) { // Pinecone æå½å空é´å é¤éè¦ç¹å®å®ç° // å½åå®ç°ï¼éè¿ metadata è¿æ»¤å é¤ log.info("å é¤åéæ°æ®: namespace={}, storageBlobId={}", namespace, storageBlobId); // 注æï¼Pinecone çå 餿ä½éè¦å¨ EmbeddingStore å±å®ç° // å½åä½¿ç¨ PineconeEmbeddingStoreï¼å¯è½éè¦è°ç¨ Pinecone 客æ·ç«¯ç´æ¥å é¤ } private File getFile(StorageBlob blob) { String path = blob.getPath(); if (path != null && !path.isEmpty()) { return new File(new File(fileProperties.getPath(), path), blob.getUidFilename()); } return new File(fileProperties.getPath(), blob.getUidFilename()); } /** * æåæä»¶å 容 */ private String extractFileContent(File file, String fileName, String contentType) throws Exception { String ext = getFileExtension(fileName); // æ ¹æ®æä»¶ç±»åæåå 容 if (isPlainText(ext)) { return Files.readString(file.toPath()); } if ("docx".equals(ext)) { return extractDocx(file); } if ("xlsx".equals(ext)) { return extractXlsx(file); } if ("xls".equals(ext)) { return extractXls(file); } // é»è®¤å°è¯è¯»åææ¬ return Files.readString(file.toPath()); } private String getFileExtension(String fileName) { if (fileName == null || !fileName.contains(".")) { return ""; } return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); } private boolean isPlainText(String ext) { return "txt".equals(ext) || "md".equals(ext) || "json".equals(ext) || "csv".equals(ext) || "xml".equals(ext) || "yaml".equals(ext) || "yml".equals(ext); } private String extractDocx(File file) throws Exception { try (var doc = new org.apache.poi.xwpf.usermodel.XWPFDocument(new java.io.FileInputStream(file)); var extractor = new org.apache.poi.xwpf.extractor.XWPFWordExtractor(doc)) { return extractor.getText(); } } private String extractXlsx(File file) throws Exception { try (var workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook(file)) { return extractWorkbook(workbook); } } private String extractXls(File file) throws Exception { try (var workbook = new org.apache.poi.hssf.usermodel.HSSFWorkbook(new java.io.FileInputStream(file))) { return extractWorkbook(workbook); } } private String extractWorkbook(org.apache.poi.ss.usermodel.Workbook workbook) { StringBuilder text = new StringBuilder(); var formatter = new org.apache.poi.ss.usermodel.DataFormatter(); for (int i = 0; i < workbook.getNumberOfSheets(); i++) { var sheet = workbook.getSheetAt(i); text.append("Sheet: ").append(sheet.getSheetName()).append("\n"); for (var row : sheet) { for (var cell : row) { text.append(formatter.formatCellValue(cell)).append("\t"); } text.append("\n"); } } return text.toString(); } /** * ææ¬åç */ private List<TextSegment> splitText(String content, KnowledgeBaseVector vector) { List<TextSegment> chunks = new ArrayList<>(); if (content.length() <= CHUNK_SIZE) { Map<String, Object> metadata = buildMetadata(vector); chunks.add(TextSegment.from(content, new dev.langchain4j.data.document.Metadata(metadata))); return chunks; } int start = 0; int chunkIndex = 0; while (start < content.length()) { int end = Math.min(start + CHUNK_SIZE, content.length()); // å°è¯å¨å¥åè¾¹çåå if (end < content.length()) { int lastPeriod = content.lastIndexOf('ã', end); int lastNewline = content.lastIndexOf('\n', end); int boundary = Math.max(lastPeriod, lastNewline); if (boundary > start + CHUNK_SIZE / 2) { end = boundary + 1; } } String chunkText = content.substring(start, end).trim(); if (!chunkText.isEmpty()) { Map<String, Object> metadata = buildMetadata(vector); metadata.put("chunkIndex", chunkIndex); chunks.add(TextSegment.from(chunkText, new dev.langchain4j.data.document.Metadata(metadata))); chunkIndex++; } start = end - CHUNK_OVERLAP; if (start < 0) start = 0; if (start >= content.length() - CHUNK_OVERLAP) break; } return chunks; } private Map<String, Object> buildMetadata(KnowledgeBaseVector vector) { Map<String, Object> metadata = new HashMap<>(); metadata.put("knowledgeBaseId", vector.getKnowledgeBaseId()); metadata.put("storageBlobId", vector.getStorageBlobId()); metadata.put("fileName", vector.getFileName()); metadata.put("namespace", vector.getNamespace()); return metadata; } } src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java
@@ -2,16 +2,25 @@ import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.approve.dto.KnowledgeBaseVectorVO; import com.ruoyi.approve.pojo.KnowledgeBase; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import com.ruoyi.approve.service.KnowledgeBaseService; import com.ruoyi.approve.service.KnowledgeBaseVectorService; import com.ruoyi.basic.dto.StorageAttachmentDTO; import com.ruoyi.basic.dto.StorageBlobDTO; import com.ruoyi.basic.pojo.StorageAttachment; import com.ruoyi.basic.service.StorageAttachmentService; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.framework.web.domain.AjaxResult; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.List; @RestController @@ -20,40 +29,41 @@ @Tag(name = "ç¥è¯åºç®¡ç") public class KnowledgeBaseController { private KnowledgeBaseService knowledgeBaseService; private KnowledgeBaseVectorService knowledgeBaseVectorService; private StorageAttachmentService storageAttachmentService; /**ã /** * è·åå表 * @return */ @GetMapping("/getList") public AjaxResult getList(@RequestParam(defaultValue = "1") long current, @RequestParam(defaultValue = "10") long size, KnowledgeBase knowledgeBase) { Page page = new Page(current, size); return AjaxResult.success(knowledgeBaseService.listpage(page,knowledgeBase)); return AjaxResult.success(knowledgeBaseService.listpage(page, knowledgeBase)); } /**ã /** * 墿·» * @return */ @PostMapping("/add") public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase){ public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase) { return AjaxResult.success(knowledgeBaseService.save(knowledgeBase)); } /** * æ´æ° * @return */ @PostMapping("/update") public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase){ public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase) { return AjaxResult.success(knowledgeBaseService.updateById(knowledgeBase)); } /** * å é¤ * @return */ @DeleteMapping("/delete") public AjaxResult delete(@RequestBody List<Long> ids){ if(CollectionUtils.isEmpty(ids)) return AjaxResult.error("è¯·ä¼ å ¥è¦å é¤çID"); public AjaxResult delete(@RequestBody List<Long> ids) { if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("è¯·ä¼ å ¥è¦å é¤çID"); return AjaxResult.success(knowledgeBaseService.removeByIds(ids)); } @@ -65,4 +75,103 @@ util.exportExcel(response, accountExpenses, "ç¥è¯åºç®¡ç导åº"); } } /** * æ¥è¯¢ç¥è¯åºæä»¶åéåç¶æ */ @GetMapping("/vector/status/{knowledgeBaseId}") @Operation(summary = "æ¥è¯¢ç¥è¯åºæä»¶åéåç¶æ") public AjaxResult getVectorStatus(@PathVariable Long knowledgeBaseId) { List<KnowledgeBaseVectorVO> list = knowledgeBaseVectorService.getVectorStatusByKnowledgeBaseId(knowledgeBaseId); return AjaxResult.success(list); } /** * éæ°åéåæä»¶ */ @PostMapping("/vector/reprocess/{vectorId}") @Operation(summary = "éæ°åéåæä»¶") public AjaxResult reprocessVector(@PathVariable Long vectorId) { knowledgeBaseVectorService.reprocessVector(vectorId); return AjaxResult.success("已鿰æäº¤åéåä»»å¡"); } /** * ä¿åç¥è¯åºæä»¶å ³èï¼æä»¶ä¸ä¼ åè°ç¨ï¼ * ä¸ä¼ æµç¨ï¼ * 1. å è°ç¨ /common/upload ä¸ä¼ æä»¶ï¼è·å storageBlobDTOs * 2. åè°ç¨æ¤æ¥å£å ³èæä»¶å°ç¥è¯åºå¹¶è§¦ååéå */ @PostMapping("/file/save") @Operation(summary = "ä¿åç¥è¯åºæä»¶å ³è") public AjaxResult saveKnowledgeBaseFiles(@RequestBody KnowledgeBaseFileDTO dto) { if (dto.getKnowledgeBaseId() == null) { return AjaxResult.error("ç¥è¯åºIDä¸è½ä¸ºç©º"); } if (CollectionUtils.isEmpty(dto.getStorageBlobIds())) { return AjaxResult.error("æä»¶IDä¸è½ä¸ºç©º"); } // ä¿åéä»¶å ³è StorageAttachmentDTO attachmentDTO = new StorageAttachmentDTO(); attachmentDTO.setRecordType("knowledge_base"); attachmentDTO.setRecordId(dto.getKnowledgeBaseId()); attachmentDTO.setApplication("rag_file"); List<StorageBlobDTO> blobDTOs = new ArrayList<>(); for (Long blobId : dto.getStorageBlobIds()) { StorageBlobDTO blobDTO = new StorageBlobDTO(); blobDTO.setId(blobId); blobDTOs.add(blobDTO); } attachmentDTO.setStorageBlobDTOs(blobDTOs); storageAttachmentService.saveStorageAttachment(attachmentDTO); // å建åéè®°å½å¹¶è§¦ååéå for (Long blobId : dto.getStorageBlobIds()) { // è·åæä»¶ä¿¡æ¯ var blob = storageAttachmentService.getBaseMapper() .selectOne(com.baomidou.mybatisplus.core.toolkit.Wrappers.<StorageAttachment>lambdaQuery() .eq(StorageAttachment::getStorageBlobId, blobId) .eq(StorageAttachment::getRecordType, "knowledge_base") .eq(StorageAttachment::getRecordId, dto.getKnowledgeBaseId()) .last("limit 1")); if (blob != null) { // è·åæä»¶åï¼éè¦ä» storage_blob 表è·å // è¿éç®åå¤çï¼å®é éè¦æ¥è¯¢ storage_blob 表 String fileName = "file_" + blobId; String fileType = "unknown"; knowledgeBaseVectorService.createVectorRecord( dto.getKnowledgeBaseId(), blobId, fileName, fileType ); } } return AjaxResult.success(); } /** * å é¤ç¥è¯åºæä»¶ */ @DeleteMapping("/file/delete") @Operation(summary = "å é¤ç¥è¯åºæä»¶") public AjaxResult deleteKnowledgeBaseFiles(@RequestBody List<Long> vectorIds) { if (CollectionUtils.isEmpty(vectorIds)) { return AjaxResult.error("è¯·éæ©è¦å é¤çæä»¶"); } knowledgeBaseVectorService.deleteVectors(vectorIds); return AjaxResult.success(); } /** * ç¥è¯åºæä»¶DTO */ @lombok.Data public static class KnowledgeBaseFileDTO { private Long knowledgeBaseId; private List<Long> storageBlobIds; } } src/main/java/com/ruoyi/approve/dto/KnowledgeBaseVectorVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,21 @@ package com.ruoyi.approve.dto; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; /** * ç¥è¯åºæä»¶åéç¶æVO */ @Data @EqualsAndHashCode(callSuper = true) @Schema(description = "ç¥è¯åºæä»¶åéç¶æVO") public class KnowledgeBaseVectorVO extends KnowledgeBaseVector { @Schema(description = "æä»¶é¢è§URL") private String previewUrl; @Schema(description = "æä»¶ä¸è½½URL") private String downloadUrl; } src/main/java/com/ruoyi/approve/mapper/KnowledgeBaseVectorMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,39 @@ package com.ruoyi.approve.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.approve.dto.KnowledgeBaseVectorVO; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; /** * ç¥è¯åºæä»¶åéè®°å½ Mapper */ @Mapper public interface KnowledgeBaseVectorMapper extends BaseMapper<KnowledgeBaseVector> { /** * æ¥è¯¢ç¥è¯åºçæä»¶åéç¶æå表 */ @Select("SELECT v.*, b.path as previewUrl " + "FROM knowledge_base_vector v " + "LEFT JOIN storage_blob b ON v.storage_blob_id = b.id " + "WHERE v.knowledge_base_id = #{knowledgeBaseId} " + "ORDER BY v.create_time DESC") List<KnowledgeBaseVectorVO> selectByKnowledgeBaseId(@Param("knowledgeBaseId") Long knowledgeBaseId); /** * ç»è®¡ç¥è¯åºçæä»¶æ°é */ @Select("SELECT COUNT(*) FROM knowledge_base_vector WHERE knowledge_base_id = #{knowledgeBaseId}") int countByKnowledgeBaseId(@Param("knowledgeBaseId") Long knowledgeBaseId); /** * ç»è®¡ç¥è¯åºçæ»åçæ°é */ @Select("SELECT SUM(chunk_count) FROM knowledge_base_vector WHERE knowledge_base_id = #{knowledgeBaseId} AND vector_status = 2") int sumChunkCountByKnowledgeBaseId(@Param("knowledgeBaseId") Long knowledgeBaseId); } src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java
@@ -91,4 +91,19 @@ @TableField(fill = FieldFill.INSERT) private Long deptId; /** * æä»¶æ°é */ private Integer fileCount; /** * æ»åçæ°é */ private Integer totalChunkCount; /** * ç¥è¯åºæè¿° */ private String description; } src/main/java/com/ruoyi/approve/pojo/KnowledgeBaseVector.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,80 @@ package com.ruoyi.approve.pojo; import com.baomidou.mybatisplus.annotation.*; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; /** * ç¥è¯åºæä»¶åéè®°å½è¡¨ * knowledge_base_vector */ @Data @TableName("knowledge_base_vector") @Schema(description = "ç¥è¯åºæä»¶åéè®°å½") public class KnowledgeBaseVector implements Serializable { private static final long serialVersionUID = 1L; @TableId(type = IdType.AUTO) @Schema(description = "主é®ID") private Long id; @Schema(description = "å ³èç¥è¯åºID") private Long knowledgeBaseId; @Schema(description = "å ³èæä»¶blob ID") private Long storageBlobId; @Schema(description = "æä»¶åç§°") private String fileName; @Schema(description = "æä»¶ç±»å(docx/pdf/xlsx/txtç)") private String fileType; @Schema(description = "åéåç¶æ: 0-å¾ å¤ç, 1-å¤çä¸, 2-已宿, 3-失败") private Integer vectorStatus; @Schema(description = "åéå失败åå ") private String vectorError; @Schema(description = "åçæ°é") private Integer chunkCount; @Schema(description = "åéå½å空é´") private String namespace; @TableField(fill = FieldFill.INSERT) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Schema(description = "å建æ¶é´") private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT) @Schema(description = "å建人") private Integer createUser; @TableField(fill = FieldFill.INSERT_UPDATE) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Schema(description = "æ´æ°æ¶é´") private LocalDateTime updateTime; @TableField(fill = FieldFill.INSERT_UPDATE) @Schema(description = "æ´æ°äºº") private Integer updateUser; @TableField(fill = FieldFill.INSERT) @Schema(description = "ç§æ·ID") private Long tenantId; @TableField(fill = FieldFill.INSERT) @Schema(description = "é¨é¨ID") private Long deptId; // åéåç¶æå¸¸é public static final int STATUS_PENDING = 0; public static final int STATUS_PROCESSING = 1; public static final int STATUS_COMPLETED = 2; public static final int STATUS_FAILED = 3; } src/main/java/com/ruoyi/approve/service/KnowledgeBaseVectorService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,43 @@ package com.ruoyi.approve.service; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.approve.dto.KnowledgeBaseVectorVO; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import java.util.List; /** * ç¥è¯åºæä»¶åéè®°å½ Service */ public interface KnowledgeBaseVectorService extends IService<KnowledgeBaseVector> { /** * æ¥è¯¢ç¥è¯åºçæä»¶åéç¶æå表 */ List<KnowledgeBaseVectorVO> getVectorStatusByKnowledgeBaseId(Long knowledgeBaseId); /** * å建åéè®°å½ï¼æä»¶ä¸ä¼ åè°ç¨ï¼ */ KnowledgeBaseVector createVectorRecord(Long knowledgeBaseId, Long storageBlobId, String fileName, String fileType); /** * æ´æ°åéåç¶æ */ void updateVectorStatus(Long id, Integer status, Integer chunkCount, String error); /** * éæ°å¤çåéå */ void reprocessVector(Long id); /** * å é¤åéè®°å½ååéåºæ°æ® */ void deleteVector(Long id); /** * æ¹éå é¤åéè®°å½ */ void deleteVectors(List<Long> ids); } src/main/java/com/ruoyi/approve/service/impl/KnowledgeBaseVectorServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,144 @@ package com.ruoyi.approve.service.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.approve.dto.KnowledgeBaseVectorVO; import com.ruoyi.approve.mapper.KnowledgeBaseVectorMapper; import com.ruoyi.approve.pojo.KnowledgeBase; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import com.ruoyi.approve.service.KnowledgeBaseService; import com.ruoyi.approve.service.KnowledgeBaseVectorService; import com.ruoyi.ai.service.KnowledgeRagService; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * ç¥è¯åºæä»¶åéè®°å½ Serviceå®ç° */ @Slf4j @Service public class KnowledgeBaseVectorServiceImpl extends ServiceImpl<KnowledgeBaseVectorMapper, KnowledgeBaseVector> implements KnowledgeBaseVectorService { private final KnowledgeBaseService knowledgeBaseService; private final KnowledgeRagService knowledgeRagService; public KnowledgeBaseVectorServiceImpl( KnowledgeBaseService knowledgeBaseService, @Lazy KnowledgeRagService knowledgeRagService) { this.knowledgeBaseService = knowledgeBaseService; this.knowledgeRagService = knowledgeRagService; } @Override public List<KnowledgeBaseVectorVO> getVectorStatusByKnowledgeBaseId(Long knowledgeBaseId) { return baseMapper.selectByKnowledgeBaseId(knowledgeBaseId); } @Override public KnowledgeBaseVector createVectorRecord(Long knowledgeBaseId, Long storageBlobId, String fileName, String fileType) { KnowledgeBase knowledgeBase = knowledgeBaseService.getById(knowledgeBaseId); if (knowledgeBase == null) { throw new RuntimeException("ç¥è¯åºä¸åå¨: " + knowledgeBaseId); } KnowledgeBaseVector vector = new KnowledgeBaseVector(); vector.setKnowledgeBaseId(knowledgeBaseId); vector.setStorageBlobId(storageBlobId); vector.setFileName(fileName); vector.setFileType(fileType); vector.setVectorStatus(KnowledgeBaseVector.STATUS_PENDING); vector.setNamespace("kb-" + knowledgeBaseId); vector.setChunkCount(0); save(vector); // 弿¥è§¦ååéåå¤ç knowledgeRagService.processVectorAsync(vector.getId()); return vector; } @Override public void updateVectorStatus(Long id, Integer status, Integer chunkCount, String error) { KnowledgeBaseVector vector = getById(id); if (vector == null) { return; } vector.setVectorStatus(status); if (chunkCount != null) { vector.setChunkCount(chunkCount); } if (error != null) { vector.setVectorError(error); } updateById(vector); // å¦æå®æï¼æ´æ°ç¥è¯åºç»è®¡ if (status == KnowledgeBaseVector.STATUS_COMPLETED) { updateKnowledgeBaseStats(vector.getKnowledgeBaseId()); } } @Override @Transactional(rollbackFor = Exception.class) public void reprocessVector(Long id) { KnowledgeBaseVector vector = getById(id); if (vector == null) { throw new RuntimeException("åéè®°å½ä¸åå¨: " + id); } vector.setVectorStatus(KnowledgeBaseVector.STATUS_PENDING); vector.setVectorError(null); vector.setChunkCount(0); updateById(vector); // 弿¥éæ°å¤ç knowledgeRagService.processVectorAsync(id); } @Override @Transactional(rollbackFor = Exception.class) public void deleteVector(Long id) { KnowledgeBaseVector vector = getById(id); if (vector == null) { return; } // å é¤åéåºä¸çæ°æ® try { knowledgeRagService.deleteEmbeddings(vector.getNamespace(), vector.getStorageBlobId()); } catch (Exception e) { log.error("å é¤åéåºæ°æ®å¤±è´¥", e); } // å é¤è®°å½ removeById(id); // æ´æ°ç¥è¯åºç»è®¡ updateKnowledgeBaseStats(vector.getKnowledgeBaseId()); } @Override @Transactional(rollbackFor = Exception.class) public void deleteVectors(List<Long> ids) { for (Long id : ids) { deleteVector(id); } } private void updateKnowledgeBaseStats(Long knowledgeBaseId) { KnowledgeBase knowledgeBase = knowledgeBaseService.getById(knowledgeBaseId); if (knowledgeBase == null) { return; } int fileCount = baseMapper.countByKnowledgeBaseId(knowledgeBaseId); int totalChunkCount = baseMapper.sumChunkCountByKnowledgeBaseId(knowledgeBaseId); knowledgeBase.setFileCount(fileCount); knowledgeBase.setTotalChunkCount(totalChunkCount); knowledgeBaseService.updateById(knowledgeBase); } } src/main/java/com/ruoyi/basic/dto/StorageBlobDTO.java
@@ -1,5 +1,7 @@ package com.ruoyi.basic.dto; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import com.ruoyi.basic.pojo.StorageBlob; import lombok.Data; @@ -19,4 +21,17 @@ * æä»¶ç±»å */ private String application; /** * æ¯æä»æ°åIDååºååï¼å端å¯è½åªä¼ IDï¼ */ @JsonCreator public static StorageBlobDTO from(Object value) { if (value instanceof Number) { StorageBlobDTO dto = new StorageBlobDTO(); dto.setId(((Number) value).longValue()); return dto; } throw new IllegalArgumentException("æ æ³ååºåå StorageBlobDTO: " + value); } } src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java
@@ -5,7 +5,8 @@ FILE("file"), AFTER_FILE("after_file"), BEFORE_FILE("before_file"), APK("apk"); APK("apk"), RAG_FILE("rag_file"); private final String type; src/main/java/com/ruoyi/basic/utils/FileUtil.java
@@ -111,6 +111,7 @@ if (CollectionUtils.isEmpty(storageBlobDTOS)) { deleteStorageAttachmentsByRecordTypeAndRecordId(recordType, recordId); return; } List<StorageAttachment> storageAttachments = new ArrayList<>(); src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -164,6 +164,12 @@ @Schema(description = "å ³èæ£æµæ å主表id") private Long testStandardId; /** * å ¥åºæ¯ä¾(ç¾åæ¯)ï¼è´¨æ£å ¥åºæ¶å ¥åºæ°é=åæ ¼æ°é*å ¥åºæ¯ä¾/100 */ @Excel(name = "å ¥åºæ¯ä¾(%)") @Schema(description = "å ¥åºæ¯ä¾(ç¾åæ¯)ï¼é»è®¤100") private BigDecimal stockInRatio; @TableField(fill = FieldFill.INSERT) private Long deptId; src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -137,7 +137,15 @@ } stockInventoryDto.setRecordId(qualityInspect.getId()); stockInventoryDto.setProductModelId(qualityInspect.getProductModelId()); stockInventoryDto.setQualitity(qualityInspect.getQualifiedQuantity()); // å ¥åºæ°é = åæ ¼æ°é * å ¥åºæ¯ä¾ / 100ï¼å ¥åºæ¯ä¾é»è®¤100% BigDecimal stockInRatio = qualityInspect.getStockInRatio(); if (stockInRatio == null || stockInRatio.compareTo(BigDecimal.ZERO) <= 0) { stockInRatio = new BigDecimal("100.00"); } BigDecimal actualStockInQuantity = qualityInspect.getQualifiedQuantity() .multiply(stockInRatio) .divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP); stockInventoryDto.setQualitity(actualStockInQuantity); if (qualityInspect.getCheckTime() != null) { LocalDate stockCreateDate = DateUtils.toLocalDate(qualityInspect.getCheckTime()).plusDays(1); stockInventoryDto.setCreateTime(LocalDateTime.of(stockCreateDate, java.time.LocalTime.MIDNIGHT)); @@ -294,7 +302,15 @@ } stockInventoryDto.setRecordId(qualityInspect.getId()); stockInventoryDto.setProductModelId(qualityInspect.getProductModelId()); stockInventoryDto.setQualitity(qualified); // å ¥åºæ°é = åæ ¼æ°é * å ¥åºæ¯ä¾ / 100ï¼å ¥åºæ¯ä¾é»è®¤100% BigDecimal stockInRatio = qualityInspect.getStockInRatio(); if (stockInRatio == null || stockInRatio.compareTo(BigDecimal.ZERO) <= 0) { stockInRatio = new BigDecimal("100.00"); } BigDecimal actualStockInQuantity = qualified .multiply(stockInRatio) .divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP); stockInventoryDto.setQualitity(actualStockInQuantity); if (qualityInspect.getCheckTime() != null) { LocalDate stockCreateDate = DateUtils.toLocalDate(qualityInspect.getCheckTime()).plusDays(1); stockInventoryDto.setCreateTime(LocalDateTime.of(stockCreateDate, java.time.LocalTime.MIDNIGHT)); src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
@@ -55,7 +55,9 @@ @Override @Transactional(rollbackFor = Exception.class) public int add(StockInRecordDto stockInRecordDto) { String no = OrderUtils.countTodayByCreateTime(stockInRecordMapper, "RK","inbound_batches", stockInRecordDto.getCreateTime() != null ? stockInRecordDto.getCreateTime() : LocalDateTime.now()); LocalDateTime createTime = stockInRecordDto.getCreateTime() != null ? stockInRecordDto.getCreateTime() : LocalDateTime.now(); stockInRecordDto.setCreateTime(createTime); String no = OrderUtils.countTodayByCreateTime(stockInRecordMapper, "RK","inbound_batches", createTime); stockInRecordDto.setInboundBatches(no); StockInRecord stockInRecord = new StockInRecord(); BeanUtils.copyProperties(stockInRecordDto, stockInRecord); src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java
@@ -60,9 +60,11 @@ @Override public int add(StockOutRecordDto stockOutRecordDto) { LocalDateTime createTime = stockOutRecordDto.getCreateTime() != null ? stockOutRecordDto.getCreateTime() : LocalDateTime.now(); stockOutRecordDto.setCreateTime(createTime); // å¦æä¼ å ¥äºoutboundBatcheså使ç¨ï¼å¦åèªå¨çæ if (stockOutRecordDto.getOutboundBatches() == null || stockOutRecordDto.getOutboundBatches().isEmpty()) { String no = OrderUtils.countTodayByCreateTime(stockOutRecordMapper, "CK","outbound_batches", stockOutRecordDto.getCreateTime() != null ? stockOutRecordDto.getCreateTime() : LocalDateTime.now()); String no = OrderUtils.countTodayByCreateTime(stockOutRecordMapper, "CK","outbound_batches", createTime); stockOutRecordDto.setOutboundBatches(no); } if (StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode().equals(stockOutRecordDto.getRecordType())){ src/main/resources/application.yml
@@ -41,5 +41,3 @@ model-name: "deepseek-r1:1.5b" log-requests: true log-responses: true knowledge: one: D:\æ°ç大ç½ç´ ä¼ä¸äº§åä½ç³»è¯´æææ¡£.md