From 29ccd9919082e0157f57989ae83b303f314bad6b Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 08 六月 2026 16:01:55 +0800
Subject: [PATCH] feat(financial): 新增凭证分录科目明细字段
---
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java | 20
doc/20260608_原料质检入库比例字段前端联调文档.md | 178 +++++
.gitignore | 4
src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java | 15
src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java | 135 +++
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java | 4
src/main/java/com/ruoyi/approve/pojo/KnowledgeBaseVector.java | 80 ++
src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java | 34 +
src/main/java/com/ruoyi/approve/service/impl/KnowledgeBaseVectorServiceImpl.java | 144 ++++
src/main/java/com/ruoyi/ai/assistant/KnowledgeChatAgent.java | 35 +
src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java | 53 -
src/main/java/com/ruoyi/approve/dto/KnowledgeBaseVectorVO.java | 21
src/main/java/com/ruoyi/approve/service/KnowledgeBaseVectorService.java | 43 +
src/main/java/com/ruoyi/ai/service/impl/KnowledgeRagServiceImpl.java | 274 ++++++++
src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java | 3
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java | 4
src/main/java/com/ruoyi/approve/mapper/KnowledgeBaseVectorMapper.java | 39 +
doc/20260608_知识库RAG向量检索功能前端联调文档.md | 626 +++++++++++++++++++
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java | 6
src/main/java/com/ruoyi/ai/controller/KnowledgeChatController.java | 91 ++
src/main/java/com/ruoyi/basic/dto/StorageBlobDTO.java | 15
src/main/java/com/ruoyi/basic/utils/FileUtil.java | 1
src/main/java/com/ruoyi/ai/dto/KnowledgeChatRequest.java | 21
src/main/resources/application.yml | 2
doc/20260608_知识库向量检索功能.sql | 34 +
25 files changed, 1,818 insertions(+), 64 deletions(-)
diff --git a/.gitignore b/.gitignore
index d3f800c..f04e21a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,7 @@
!*/build/*.java
!*/build/*.html
!*/build/*.xml
+
+######################################################################
+# Claude Code
+.claude/
diff --git "a/doc/20260608_\345\216\237\346\226\231\350\264\250\346\243\200\345\205\245\345\272\223\346\257\224\344\276\213\345\255\227\346\256\265\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md" "b/doc/20260608_\345\216\237\346\226\231\350\264\250\346\243\200\345\205\245\345\272\223\346\257\224\344\276\213\345\255\227\346\256\265\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..043659b
--- /dev/null
+++ "b/doc/20260608_\345\216\237\346\226\231\350\264\250\346\243\200\345\205\245\345\272\223\346\257\224\344\276\213\345\255\227\346\256\265\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
@@ -0,0 +1,178 @@
+# 鍘熸枡璐ㄦ澧炲姞鍏ュ簱姣斾緥瀛楁鍓嶇鑱旇皟鏂囨。
+
+## 涓�銆佸姛鑳借鏄�
+
+鍘熸枡璐ㄦ澧炲姞"鍏ュ簱姣斾緥"瀛楁锛堢櫨鍒嗘瘮锛夈�傚綋璐ㄦ鍏ュ簱鏃讹紝瀹為檯鍏ュ簱鏁伴噺 = 鍚堟牸鏁伴噺 脳 鍏ュ簱姣斾緥 / 100銆�
+
+**绀轰緥**锛�
+- 鍚堟牸鏁伴噺锛�100
+- 鍏ュ簱姣斾緥锛�80%
+- 瀹為檯鍏ュ簱鏁伴噺锛�100 脳 80 / 100 = 80
+
+## 浜屻�佹暟鎹簱鍙樻洿
+
+| 瀛楁鍚� | 绫诲瀷 | 榛樿鍊� | 璇存槑 |
+|--------|------|--------|------|
+| stock_in_ratio | DECIMAL(5,2) | 100.00 | 鍏ュ簱姣斾緥(鐧惧垎姣�)锛岃寖鍥�0.00~100.00 |
+
+## 涓夈�佹帴鍙e彉鏇�
+
+### 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. **浠呭鍘熸枡璐ㄦ鐢熸晥**锛歩nspectType = 0锛堝師鏉愭枡妫�楠岋級
+
+## 鍏�佹敞鎰忎簨椤�
+
+1. 鍏ュ簱姣斾緥浠呭奖鍝嶅叆搴撴暟閲忥紝涓嶅奖鍝嶅悎鏍兼暟閲忓拰涓嶅悎鏍兼暟閲忕殑缁熻
+2. 宸叉彁浜ょ殑璐ㄦ鍗曚笉鍙慨鏀瑰叆搴撴瘮渚�
+3. 瀵煎嚭鎶ヨ〃鏃堕渶灞曠ず鍏ュ簱姣斾緥瀛楁
+4. 鍏ュ簱姣斾緥涓虹┖鎴栧皬浜庣瓑浜�0鏃讹紝鑷姩鎸�100%澶勭悊
+
+## 涓冦�佸瓧娈垫槧灏�
+
+| 鍓嶇瀛楁 | 鍚庣瀛楁 | 鏁版嵁搴撳瓧娈� | 绫诲瀷 |
+|----------|----------|------------|------|
+| stockInRatio | stockInRatio | stock_in_ratio | BigDecimal(5,2) |
\ No newline at end of file
diff --git "a/doc/20260608_\347\237\245\350\257\206\345\272\223RAG\345\220\221\351\207\217\346\243\200\347\264\242\345\212\237\350\203\275\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md" "b/doc/20260608_\347\237\245\350\257\206\345\272\223RAG\345\220\221\351\207\217\346\243\200\347\264\242\345\212\237\350\203\275\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..739f572
--- /dev/null
+++ "b/doc/20260608_\347\237\245\350\257\206\345\272\223RAG\345\220\221\351\207\217\346\243\200\347\264\242\345\212\237\350\203\275\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.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 '鍏宠仈鐭ヨ瘑搴揑D',
+ 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 '鐭ヨ瘑搴撴弿杩�';
+```
+
+## 涓夈�佹帴鍙h璁�
+
+### 3.1 鏂囦欢涓婁紶锛堜娇鐢ㄧ郴缁熷凡鏈夋帴鍙o級
+
+**鎺ュ彛鍦板潃**锛歚POST /common/upload`
+
+**璇锋眰鏂瑰紡**锛歮ultipart/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 | 鏄� | 鐭ヨ瘑搴揑D |
+| application | String | 鏄� | 鍥哄畾鍊� `rag_file`锛屾爣璇哛AG鏂囦欢 |
+| storageBlobDTOs | List | 鏄� | 涓婁紶杩斿洖鐨勬枃浠禸lob鍒楄〃 |
+
+**鍝嶅簲缁撴灉**锛�
+```json
+{
+ "code": 200,
+ "msg": "鎿嶄綔鎴愬姛"
+}
+```
+
+### 3.3 鐭ヨ瘑搴撴枃浠跺垪琛�
+
+**鎺ュ彛鍦板潃**锛歚GET /storageAttachment/list`
+
+**璇锋眰鍙傛暟**锛�
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|--------|------|------|------|
+| recordType | String | 鏄� | 鍥哄畾鍊� `knowledge_base` |
+| recordId | Long | 鏄� | 鐭ヨ瘑搴揑D |
+| 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 | 鏄� | 鐭ヨ瘑搴揑D |
+| 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 | 鐩存帴璇诲彇鍐呭 |
+| 浠g爜鏂囦欢 | .java, .js, .vue, .html, .sql绛� | 鐩存帴璇诲彇鍐呭 |
+
+**鏂囦欢澶у皬闄愬埗**锛氬崟鏂囦欢鏈�澶�10MB锛堢郴缁熼粯璁ら檺鍒讹級
+
+## 浜斻�佸悜閲忓寲鐘舵�佽鏄�
+
+| 鐘舵�佸�� | 鐘舵�佸悕绉� | 璇存槑 |
+|--------|----------|------|
+| 0 | 寰呭鐞� | 鏂囦欢宸蹭笂浼狅紝绛夊緟鍚戦噺鍖栧鐞� |
+| 1 | 澶勭悊涓� | 姝e湪杩涜鍚戦噺鍒囩墖澶勭悊 |
+| 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銆亁lsx銆乸df銆乼xt 绛夋牸寮忥紝鍗曟枃浠朵笉瓒呰繃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 鍒楄〃锛堝寘鍚玝lobId銆侀瑙圲RL銆佷笅杞経RL锛�
+ 鈫�
+璋冪敤 /storageAttachment/add 鍏宠仈鏂囦欢鍒扮煡璇嗗簱
+ 鈫�
+鍚庣鐩戝惉闄勪欢淇濆瓨浜嬩欢 鈫� 鎻愬彇鏂囦欢鏂囨湰 鈫� 鍒囩墖 鈫� 鍚戦噺鍖�
+ 鈫�
+瀛樺叆 Pinecone 鍚戦噺搴擄紙鍛藉悕绌洪棿: kb-{knowledgeBaseId})
+ 鈫�
+鏇存柊 knowledge_base_vector 琛ㄧ姸鎬佷负宸插畬鎴�
+```
+
+### 7.2 鐭ヨ瘑搴撻棶绛旀祦绋�
+
+```
+鐢ㄦ埛鎻愰棶 鈫� 璋冪敤 Embedding 妯″瀷瀵归棶棰樺悜閲忓寲
+ 鈫�
+鍦� Pinecone 涓绱紙鍛藉悕绌洪棿: kb-{knowledgeBaseId})
+ 鈫�
+鑾峰彇鐩稿叧鍒囩墖鏂囨湰 鈫� 浣滀负涓婁笅鏂� + 鐢ㄦ埛闂鍙戠粰 AI 妯″瀷
+ 鈫�
+AI 娴佸紡鐢熸垚鍥炵瓟 鈫� 杩斿洖鍓嶇
+```
+
+## 鍏�佹妧鏈疄鐜拌鐐�
+
+### 8.1 鏂囨湰鍒囩墖绛栫暐
+
+- **鍒囩墖澶у皬**锛氶粯璁ゆ瘡鐗� 500 瀛楃
+- **閲嶅彔澶у皬**锛氶粯璁� 100 瀛楃閲嶅彔锛屼繚璇佽涔夎繛璐�
+- **鍒囩墖鍏冩暟鎹�**锛氬寘鍚枃浠禝D銆佺煡璇嗗簱ID銆佸垏鐗囩储寮�
+
+### 8.2 鍚戦噺鍛藉悕绌洪棿
+
+姣忎釜鐭ヨ瘑搴撲娇鐢ㄧ嫭绔嬪懡鍚嶇┖闂达細`kb-{knowledgeBaseId}`
+
+### 8.3 闄勪欢鍏宠仈鍙傛暟
+
+| 鍙傛暟 | 鍊� | 璇存槑 |
+|------|-----|------|
+| recordType | `knowledge_base` | 浣跨敤宸叉湁鏋氫妇 |
+| application | `rag_file` | 鏍囪瘑涓篟AG鏂囦欢 |
+
+## 涔濄�佹敞鎰忎簨椤�
+
+1. 鏂囦欢涓婁紶浣跨敤绯荤粺宸叉湁鐨� `/common/upload` 鍜� `/storageAttachment/add` 鎺ュ彛
+2. 鍒犻櫎鏂囦欢鏃跺悓姝ュ垹闄ゅ悜閲忓簱涓殑鐩稿叧鍒囩墖
+3. 澶ф枃浠跺悜閲忓寲鍙兘鑰楁椂杈冮暱锛屽墠绔渶杞鐘舵�佹垨鏄剧ず杩涘害
+4. 鐭ヨ瘑搴撻棶绛斾緷璧栧悜閲忔绱㈣川閲忥紝寤鸿浼樺寲鍒囩墖绛栫暐
+5. 涓嶅悓鐭ヨ瘑搴撲娇鐢ㄤ笉鍚屽懡鍚嶇┖闂达紝閬垮厤鏁版嵁娣锋穯
+
+## 鍗併�侀敊璇爜璇存槑
+
+| 閿欒鐮� | 璇存槑 |
+|--------|------|
+| 40001 | 鏂囦欢绫诲瀷涓嶆敮鎸� |
+| 40002 | 鏂囦欢澶у皬瓒呭嚭闄愬埗 |
+| 40003 | 鐭ヨ瘑搴撲笉瀛樺湪 |
+| 50001 | 鏂囦欢涓婁紶澶辫触 |
+| 50002 | 鏂囦欢鍐呭鎻愬彇澶辫触 |
+| 50003 | 鍚戦噺鍖栧鐞嗗け璐� |
+| 50004 | 鍚戦噺妫�绱㈠け璐� |
\ No newline at end of file
diff --git "a/doc/20260608_\347\237\245\350\257\206\345\272\223\345\220\221\351\207\217\346\243\200\347\264\242\345\212\237\350\203\275.sql" "b/doc/20260608_\347\237\245\350\257\206\345\272\223\345\220\221\351\207\217\346\243\200\347\264\242\345\212\237\350\203\275.sql"
new file mode 100644
index 0000000..04eaf89
--- /dev/null
+++ "b/doc/20260608_\347\237\245\350\257\206\345\272\223\345\220\221\351\207\217\346\243\200\347\264\242\345\212\237\350\203\275.sql"
@@ -0,0 +1,34 @@
+-- 20260608_鐭ヨ瘑搴撳悜閲忔绱㈠姛鑳�.sql
+-- 鐭ヨ瘑搴揜AG鍚戦噺妫�绱㈠姛鑳芥暟鎹簱鍙樻洿
+
+-- 鍒涘缓鐭ヨ瘑搴撴枃浠跺悜閲忚褰曡〃锛堢敤浜庤窡韪悜閲忓寲鐘舵�侊級
+CREATE TABLE IF NOT EXISTS knowledge_base_vector (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '涓婚敭ID',
+ knowledge_base_id BIGINT NOT NULL COMMENT '鍏宠仈鐭ヨ瘑搴揑D',
+ 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锛堝墠绔紶鍙傛椂浣跨敤锛�
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/assistant/KnowledgeChatAgent.java b/src/main/java/com/ruoyi/ai/assistant/KnowledgeChatAgent.java
new file mode 100644
index 0000000..bed3d18
--- /dev/null
+++ b/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;
+
+/**
+ * 鐭ヨ瘑搴撻棶绛擜gent
+ * 鍩轰簬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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java b/src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java
index 27e5a6b..0b1a9e6 100644
--- a/src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java
+++ b/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璇诲彇鎸囧畾鐩綍涓嬬殑鐭ヨ瘑搴撴枃妗�
-// //骞朵娇鐢ㄩ粯璁ょ殑鏂囨。瑙f瀽鍣ㄥ鏂囨。杩涜瑙f瀽
-// 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鐨凞ocument瀵硅薄
-//// List<Document> documents = new ArrayList<>();
-//
-// //浣跨敤鍐呭瓨鍚戦噺瀛樺偍
-// InMemoryEmbeddingStore<TextSegment> inMemoryEmbeddingStore = new InMemoryEmbeddingStore<>();
-// //浣跨敤榛樿鐨勬枃妗e垎鍓插櫒
-// EmbeddingStoreIngestor.builder()
-// .embeddingModel(embeddingModel)
-// .embeddingStore(inMemoryEmbeddingStore)
-// .build()
-// .ingest(documents);
-// //浠庡祵鍏ュ瓨鍌紙EmbeddingStore锛夐噷妫�绱㈠拰鏌ヨ鍐呭鐩稿叧鐨勪俊鎭�
-// return EmbeddingStoreContentRetriever.builder()
-// .embeddingModel(embeddingModel)
-// .embeddingStore(inMemoryEmbeddingStore)
-// .build();
-// }
-
+ /**
+ * 鐭ヨ瘑搴撳唴瀹规绱㈠櫒
+ * 浠庡悜閲忔暟鎹簱锛圥inecone锛夋绱㈠凡鍚戦噺鍖栫殑鐭ヨ瘑搴撳唴瀹�
+ * 鐭ヨ瘑搴撴枃浠堕�氳繃 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();
}
}
diff --git a/src/main/java/com/ruoyi/ai/controller/KnowledgeChatController.java b/src/main/java/com/ruoyi/ai/controller/KnowledgeChatController.java
new file mode 100644
index 0000000..cf3321b
--- /dev/null
+++ b/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;
+
+/**
+ * 鐭ヨ瘑搴撻棶绛擟ontroller
+ */
+@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("鐭ヨ瘑搴揑D涓嶈兘涓虹┖");
+ }
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/dto/KnowledgeChatRequest.java b/src/main/java/com/ruoyi/ai/dto/KnowledgeChatRequest.java
new file mode 100644
index 0000000..134c2c8
--- /dev/null
+++ b/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 = "鐭ヨ瘑搴揑D", required = true)
+ private Long knowledgeBaseId;
+
+ @Schema(description = "浼氳瘽ID锛岀敤浜庝繚鎸佷笂涓嬫枃", required = true)
+ private String memoryId;
+
+ @Schema(description = "鐢ㄦ埛鎻愰棶鍐呭", required = true)
+ private String question;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java b/src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java
new file mode 100644
index 0000000..44d5f87
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java
@@ -0,0 +1,34 @@
+package com.ruoyi.ai.service;
+
+import java.util.List;
+
+/**
+ * 鐭ヨ瘑搴揜AG鏈嶅姟
+ * 璐熻矗鏂囦欢鍚戦噺鍖栧鐞嗗拰妫�绱�
+ */
+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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/service/impl/KnowledgeRagServiceImpl.java b/src/main/java/com/ruoyi/ai/service/impl/KnowledgeRagServiceImpl.java
new file mode 100644
index 0000000..751c427
--- /dev/null
+++ b/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;
+
+/**
+ * 鐭ヨ瘑搴揜AG鏈嶅姟瀹炵幇
+ */
+@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);
+ // 娉ㄦ剰锛歅inecone 鐨勫垹闄ゆ搷浣滈渶瑕佸湪 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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java b/src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java
index 3ebb782..ac4f93d 100644
--- a/src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java
+++ b/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("璇蜂紶鍏ヨ鍒犻櫎鐨処D");
+ public AjaxResult delete(@RequestBody List<Long> ids) {
+ if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("璇蜂紶鍏ヨ鍒犻櫎鐨処D");
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("鐭ヨ瘑搴揑D涓嶈兘涓虹┖");
+ }
+ 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();
+ }
+
+ /**
+ * 鐭ヨ瘑搴撴枃浠禗TO
+ */
+ @lombok.Data
+ public static class KnowledgeBaseFileDTO {
+ private Long knowledgeBaseId;
+ private List<Long> storageBlobIds;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/dto/KnowledgeBaseVectorVO.java b/src/main/java/com/ruoyi/approve/dto/KnowledgeBaseVectorVO.java
new file mode 100644
index 0000000..9cfb721
--- /dev/null
+++ b/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;
+
+/**
+ * 鐭ヨ瘑搴撴枃浠跺悜閲忕姸鎬乂O
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "鐭ヨ瘑搴撴枃浠跺悜閲忕姸鎬乂O")
+public class KnowledgeBaseVectorVO extends KnowledgeBaseVector {
+
+ @Schema(description = "鏂囦欢棰勮URL")
+ private String previewUrl;
+
+ @Schema(description = "鏂囦欢涓嬭浇URL")
+ private String downloadUrl;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/mapper/KnowledgeBaseVectorMapper.java b/src/main/java/com/ruoyi/approve/mapper/KnowledgeBaseVectorMapper.java
new file mode 100644
index 0000000..54beba8
--- /dev/null
+++ b/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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java b/src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java
index 06dec8e..b15da7a 100644
--- a/src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java
+++ b/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;
+
}
diff --git a/src/main/java/com/ruoyi/approve/pojo/KnowledgeBaseVector.java b/src/main/java/com/ruoyi/approve/pojo/KnowledgeBaseVector.java
new file mode 100644
index 0000000..1db31fc
--- /dev/null
+++ b/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 = "鍏宠仈鐭ヨ瘑搴揑D")
+ 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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/service/KnowledgeBaseVectorService.java b/src/main/java/com/ruoyi/approve/service/KnowledgeBaseVectorService.java
new file mode 100644
index 0000000..18595cb
--- /dev/null
+++ b/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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/service/impl/KnowledgeBaseVectorServiceImpl.java b/src/main/java/com/ruoyi/approve/service/impl/KnowledgeBaseVectorServiceImpl.java
new file mode 100644
index 0000000..52bc0b5
--- /dev/null
+++ b/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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/basic/dto/StorageBlobDTO.java b/src/main/java/com/ruoyi/basic/dto/StorageBlobDTO.java
index 52eaaa5..e3412a4 100644
--- a/src/main/java/com/ruoyi/basic/dto/StorageBlobDTO.java
+++ b/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;
+
+ /**
+ * 鏀寔浠庢暟瀛桰D鍙嶅簭鍒楀寲锛堝墠绔彲鑳藉彧浼營D锛�
+ */
+ @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);
+ }
}
diff --git a/src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java b/src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java
index 8cdf321..8b069a0 100644
--- a/src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java
+++ b/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;
diff --git a/src/main/java/com/ruoyi/basic/utils/FileUtil.java b/src/main/java/com/ruoyi/basic/utils/FileUtil.java
index 6ad5d77..d0b7576 100644
--- a/src/main/java/com/ruoyi/basic/utils/FileUtil.java
+++ b/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<>();
diff --git a/src/main/java/com/ruoyi/quality/pojo/QualityInspect.java b/src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
index 1d99181..4370687 100644
--- a/src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
+++ b/src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -164,6 +164,12 @@
@Schema(description = "鍏宠仈妫�娴嬫爣鍑嗕富琛╥d")
private Long testStandardId;
+ /**
+ * 鍏ュ簱姣斾緥(鐧惧垎姣�)锛岃川妫�鍏ュ簱鏃跺叆搴撴暟閲�=鍚堟牸鏁伴噺*鍏ュ簱姣斾緥/100
+ */
+ @Excel(name = "鍏ュ簱姣斾緥(%)")
+ @Schema(description = "鍏ュ簱姣斾緥(鐧惧垎姣�)锛岄粯璁�100")
+ private BigDecimal stockInRatio;
@TableField(fill = FieldFill.INSERT)
private Long deptId;
diff --git a/src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java b/src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
index 2842878..7e9ad95 100644
--- a/src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
+++ b/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));
diff --git a/src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java b/src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
index 5f02cca..b717f9d 100644
--- a/src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
+++ b/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);
diff --git a/src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java b/src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java
index c5d1109..43c0937 100644
--- a/src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java
+++ b/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);
// 濡傛灉浼犲叆浜唎utboundBatches鍒欎娇鐢紝鍚﹀垯鑷姩鐢熸垚
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())){
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 63fe8c2..ff737e0 100644
--- a/src/main/resources/application.yml
+++ b/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
--
Gitblit v1.9.3