feat(ai): 增强AI文件提取和审批待办功能
- 添加图片文件类型的文本提取支持,返回图片上传提示信息
- 优化审批待办助手的意图识别,增加多种关键词匹配模式
- 扩展采购代理功能,新增物料金额排行等统计工具
- 完善审批待办列表查询,支持申请人和审批人范围筛选
- 增加采购订单未入库、到货异常等业务场景的查询功能
- 优化时间范围解析,支持相对时间表达式如"近N天"等
- 添加多模态模型配置,支持图片内容识别功能
- 修复采购台账在无审批人时的自动通过逻辑
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # éè´æºè½ä½å¤æä»¶åæå端èè°è¯´æ |
| | | |
| | | ## æµç¨è¯´æ |
| | | |
| | | å端已æ°å¢éè´æºè½ä½å¤æä»¶åæç¡®è®¤æµç¨ï¼ |
| | | |
| | | 1. å端ä¸ä¼ å¤ä¸ªéè´ç¸å
³æä»¶ï¼å¹¶éå¸¦ç¨æ·è¦æ±ã |
| | | 2. å端æåæä»¶å
容ï¼äº¤ç»éè´æºè½ä½åæã |
| | | 3. æºè½ä½è¿åå¾
客æ·ç¡®è®¤çç»æå JSONã |
| | | 4. å端å±ç¤ºæè¦ãé£é©ãç¼ºå¤±åæ®µåå¾
å¤çæ°æ®ã |
| | | 5. 客æ·ç¡®è®¤æè¡¥å
æ°æ®åï¼å端è°ç¨ç¡®è®¤æ¥å£ã |
| | | 6. åç«¯æ ¹æ®ç¡®è®¤åçæ°æ®æ§è¡å¯¹åºéè´ä¸å¡å¤çã |
| | | |
| | | åææ¥å£ä¸ä¼è½åºï¼åªæç¡®è®¤æ¥å£ä¼æ§è¡ä¸å¡å¤çã |
| | | |
| | | ## æ¥å£ 1ï¼éè´å¤æä»¶åæ |
| | | |
| | | ```http |
| | | POST /purchase-ai/analyze-files |
| | | Content-Type: multipart/form-data |
| | | ``` |
| | | |
| | | 请æ±åæ°ï¼ |
| | | |
| | | | åæ° | ç±»å | å¿
å¡« | 说æ | |
| | | | --- | --- | --- | --- | |
| | | | files | file[] | æ¯ | 夿件ä¸ä¼ åæ®µï¼å段åå¿
é¡»æ¯ `files` | |
| | | | message | string | å¦ | ç¨æ·è¦æ±ï¼ä¾å¦ï¼è¯·æ ¹æ®è¿äºéè´åååæç»æ´çéè´å°è´¦æ°æ® | |
| | | | memoryId | string | å¦ | ä¼è¯ IDï¼ä¸ä¼ æ¶å端ä¼èªå¨çæå
é¨ä¼è¯ | |
| | | |
| | | è¿åï¼ |
| | | |
| | | ```http |
| | | Content-Type: text/stream;charset=utf-8 |
| | | ``` |
| | | |
| | | å端éè¦æ¼æ¥å®æ´æµå¼ææ¬ååæ§è¡ `JSON.parse`ã |
| | | |
| | | è¿å JSON ç»æç¤ºä¾ï¼ |
| | | |
| | | ```json |
| | | { |
| | | "success": true, |
| | | "businessType": "purchase_ledger", |
| | | "action": "confirm_required", |
| | | "description": "å·²æ ¹æ®æä»¶æ´çåºéè´å°è´¦è稿ï¼è¯·ç¡®è®¤ã", |
| | | "confidence": 0.86, |
| | | "missingFields": [], |
| | | "warnings": [], |
| | | "payload": {}, |
| | | "preview": [] |
| | | } |
| | | ``` |
| | | |
| | | åæ®µè¯´æï¼ |
| | | |
| | | | åæ®µ | 说æ | |
| | | | --- | --- | |
| | | | success | æ¯å¦åææå | |
| | | | businessType | ä¸å¡ç±»åï¼`purchase_ledger`ã`payment_registration`ã`purchase_return_order`ã`unknown` | |
| | | | action | åºå®ä¸º `confirm_required` | |
| | | | description | ä¸æè¯´æ | |
| | | | confidence | 置信度ï¼0 å° 1 | |
| | | | missingFields | ç¼ºå¤±åæ®µï¼å端éè¦æç¤ºç¨æ·è¡¥å
| |
| | | | warnings | é£é©æç¤º | |
| | | | payload | å¾
客æ·ç¡®è®¤å¹¶æäº¤ç»ç¡®è®¤æ¥å£çæ°æ® | |
| | | | preview | ç»å®¢æ·ç¡®è®¤ç¨ç䏿æè¦ | |
| | | |
| | | ## æ¥å£ 2ï¼ç¡®è®¤å¹¶æ§è¡ä¸å¡å¤ç |
| | | |
| | | ```http |
| | | POST /purchase-ai/analyze-files/confirm |
| | | Content-Type: application/json |
| | | ``` |
| | | |
| | | 请æ±ä½ï¼ |
| | | |
| | | ```json |
| | | { |
| | | "businessType": "purchase_ledger", |
| | | "payload": { |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | å½åæ¯æç `businessType`ï¼ |
| | | |
| | | | businessType | 说æ | å端å¤ç | |
| | | | --- | --- | --- | |
| | | | purchase_ledger | éè´å°è´¦ | è°ç¨éè´å°è´¦æ°å¢/ç¼è¾ | |
| | | | payment_registration | 仿¬¾ç»è®° | è°ç¨ä»æ¬¾ç»è®°æ°å¢ | |
| | | | purchase_return_order | éè´éè´§å | è°ç¨éè´éè´§åæ°å¢ | |
| | | |
| | | 确认æ¥å£è¿åæ®é `AjaxResult`ã |
| | | |
| | | ## éè´å°è´¦ Payload çº¦å® |
| | | |
| | | éè´å°è´¦ç¡®è®¤æ¨è使ç¨ä¸¤ä¸ªéåï¼ |
| | | |
| | | ```json |
| | | { |
| | | "businessType": "purchase_ledger", |
| | | "payload": { |
| | | "purchaseLedgers": [] |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | åæ®µçº¦å®ï¼ |
| | | |
| | | - `purchaseLedgers` æ¾éè´è®¢å/éè´å°è´¦ä¸»è¡¨æ°æ®ï¼å段åå¿
é¡»ä¸ `PurchaseLedgerDto` ä¿æä¸è´ã |
| | | - 产åæç»æ¾å¨æ¯æ¡ `purchaseLedgers[i].productData` ä¸ï¼å¯¹åº `PurchaseLedgerDto` ç `private List<SalesLedgerProduct> productData;`ã |
| | | - é¡¶å± `payload.productData` ä»
ä½ä¸ºæ§æ ¼å¼å
¼å®¹ï¼ä¸å»ºè®®å端继ç»ä½¿ç¨ã |
| | | - æä»¶ä¸çâéè´åå·âå°±æ¯âéè´ååå·âï¼å端å¯ä»¥ç»ä¸æ å°æ `purchaseContractNumber`ã |
| | | - æä»¶ä¸çâéå®åå·âå°±æ¯âéå®ååå·âï¼å端å¯ä»¥ç»ä¸æ å°æ `salesContractNo`ã |
| | | - æ¥æåæ®µç»ä¸ä½¿ç¨ `yyyy-MM-dd`ï¼ä¾å¦ `2026-04-30`ï¼ä¸è¦æäº¤ `4/30/26`ã`2026/4/30`ã`2026å¹´4æ30æ¥` æå¸¦æ¶åç§çæ ¼å¼ã |
| | | - éè´å°è´¦ä¸éè¦åç«¯ä¼ å®¡æ¹äººï¼ä¸è¦æäº¤ `approveUserIds`ã`approverId`ã |
| | | - `missingFields` é¢å客æ·å±ç¤ºï¼åªæ¾ä¸æç¼ºå¤±é¡¹ï¼ä¾å¦ `ä¾åºååç§°`ã`å«ç¨åä»·`ï¼ä¸è¦å±ç¤ºè±æå段åã |
| | | - éè´å°è´¦ä¸å¡å¿
å¡«ï¼éè´ååå·ãä¾åºååç§°æä¾åºåIDã |
| | | - 产åæç»ä¸å¡å¿
å¡«ï¼äº§ååç§°ãè§æ ¼åå·ãåä½ãæ°éãå«ç¨åä»·ãå«ç¨æ»ä»·ï¼å¦æåªæå«ç¨æ»ä»·åæ°éï¼å端ä¼èªå¨è®¡ç®å«ç¨åä»·ï¼å¦æåªæå«ç¨åä»·åæ°éï¼å端ä¼èªå¨è®¡ç®å«ç¨æ»ä»·ã |
| | | - 产åæç»å¯éè¿ `purchaseContractNumber`ã`purchaseContractNo`ã`éè´ååå·`ã`éè´åå·`ã`éè´è®¢åå·` å
³è对åºéè´è®¢åï¼ä¹å¯éè¿ `salesContractNo`ã`salesContractNumber`ã`éå®ååå·`ã`éå®åå·`ã`éå®è®¢åå·` è¾
å©å¹é
ã |
| | | |
| | | `purchaseLedgers` åæ¡è®°å½å
许使ç¨ç `PurchaseLedgerDto` åæ®µï¼ |
| | | |
| | | ```text |
| | | entryDateStart, entryDateEnd, id, purchaseContractNumber, |
| | | supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo, |
| | | salesContractNoId, projectName, entryDate, executionDate, remarks, |
| | | attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type, |
| | | productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId, |
| | | productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId, |
| | | contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type, |
| | | paymentMethod, approvalStatus, templateName |
| | | ``` |
| | | |
| | | 示ä¾ï¼ |
| | | |
| | | ```json |
| | | { |
| | | "purchaseLedgers": [ |
| | | { |
| | | "purchaseContractNumber": "CG-2026-001", |
| | | "supplierName": "åé示ä¾ä¾åºå", |
| | | "salesContractNo": "XS-2026-001", |
| | | "projectName": "示ä¾é¡¹ç®", |
| | | "entryDate": "2026-04-30", |
| | | "executionDate": "2026-04-30", |
| | | "contractAmount": 120000, |
| | | "remarks": "ç±æä»¶åæçæï¼å¾
确认", |
| | | "productData": [ |
| | | { |
| | | "productCategory": "示ä¾äº§å", |
| | | "specificationModel": "åå·A", |
| | | "unit": "ä»¶", |
| | | "quantity": 10, |
| | | "taxInclusiveUnitPrice": 12000, |
| | | "taxInclusiveTotalPrice": 120000, |
| | | "type": 2 |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | ## å端å¤ç建议 |
| | | |
| | | 1. ç¨æ·éæ©å¤ä¸ªæä»¶ï¼å¡«ååæè¦æ±ã |
| | | 2. ä½¿ç¨ `multipart/form-data` è°ç¨ `/purchase-ai/analyze-files`ã |
| | | 3. æ¼æ¥æµå¼è¿åææ¬ã |
| | | 4. 坹宿´ææ¬æ§è¡ `JSON.parse`ã |
| | | 5. å±ç¤º `preview`ã`warnings`ã`missingFields` å `payload`ã |
| | | 6. 妿 `missingFields` ä¸ä¸ºç©ºï¼å¼å¯¼ç¨æ·è¡¥å
æç¼è¾ `payload`ã |
| | | 7. ç¨æ·ç¡®è®¤åï¼å° `businessType` å确认åç `payload` æäº¤å° `/purchase-ai/analyze-files/confirm`ã |
| | | |
| | | ## 注æäºé¡¹ |
| | | |
| | | - æä»¶ä¸ä¼ åæ®µåå¿
é¡»æ¯ `files`ã |
| | | - åææ¥å£åªçæå¾
ç¡®è®¤æ°æ®ï¼ä¸ä¼æ§è¡ä¸å¡è½åºã |
| | | - 确认æ¥å£æä¼æ§è¡ä¸å¡å¤çã |
| | | - 妿 `payload` 缺å°å¿
è¦ä¸å¡ IDï¼ç¡®è®¤æ¥å£å¯è½è¿åä¸å¡æ ¡éªé误ã |
| | | - å端éè¦æ `missingFields` æç¡®å±ç¤ºç»ç¨æ·ã |
| | | - AI è¿åå
容æåæ³ JSON å¤çï¼ä¸è¦ææ®éèªç¶è¯è¨å±ç¤ºã |
| | |
| | | extractTimeRange(text) |
| | | ); |
| | | } |
| | | if (containsAny(text, "æµè½¬", "è¿åº¦", "èç¹", "æ¥å¿")) { |
| | | if (containsAny(text, "æµè½¬", "è¿åº¦", "èç¹", "æ¥å¿", "å¡å¨", "å¡å°", "å½å审æ¹äºº", "å¤çè®°å½")) { |
| | | return StringUtils.hasText(approveId) |
| | | ? approveTodoTools.getTodoProgress(memoryId, approveId) |
| | | : missingApproveId("todo_progress", "æ¥è¯¢å®¡æ¹è¿åº¦éè¦æä¾æµç¨ç¼å·ã"); |
| | |
| | | ? approveTodoTools.getTodoDetail(memoryId, approveId) |
| | | : missingApproveId("todo_detail", "æ¥è¯¢å®¡æ¹è¯¦æ
éè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (containsAny(text, "åæ¶å®¡æ ¸", "æ¤éå®¡æ ¸", "åéå®¡æ ¸")) { |
| | | if (containsAny(text, "åæ¶å®¡æ ¸", "æ¤éå®¡æ ¸", "åéå®¡æ ¸", "æ¤é审æ¹", "æ¤å审æ¹") |
| | | || (containsAny(text, "æ¤é", "æ¤å") && containsAny(text, "å®¡æ¹æä½", "å®¡æ ¸æä½"))) { |
| | | return StringUtils.hasText(approveId) |
| | | ? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractTail(text, "åå ")) |
| | | ? approveTodoTools.cancelReviewTodo(memoryId, approveId, firstNonBlank(extractTail(text, "åå "), extractTail(text, "夿³¨"))) |
| | | : missingApproveId("cancel_review_action", "åæ¶å®¡æ ¸éè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (containsAny(text, "å é¤")) { |
| | | if (containsAny(text, "å é¤", "ç§»é¤")) { |
| | | return StringUtils.hasText(approveId) |
| | | ? approveTodoTools.deleteTodo(memoryId, approveId) |
| | | : missingApproveId("delete_action", "å é¤å®¡æ¹åéè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (containsAny(text, "驳å", "æç»")) { |
| | | return StringUtils.hasText(approveId) |
| | | ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractTail(text, "åå ")) |
| | | ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", firstNonBlank(extractTail(text, "åå "), extractTail(text, "夿³¨"))) |
| | | : missingApproveId("review_action", "驳å审æ¹éè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (containsAny(text, "å®¡æ ¸éè¿", "审æ¹éè¿", "éè¿å®¡æ¹", "åæå®¡æ¹", "审æ¹åæ")) { |
| | |
| | | && !containsAny(text, "æªéè¿", "éè¿ç", "审æ¹éè¿ç", "å®¡æ ¸éè¿ç")) { |
| | | return approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "夿³¨")); |
| | | } |
| | | if (containsAny(text, "ä¿®æ¹")) { |
| | | if (containsAny(text, "ä¿®æ¹", "æ´æ°", "åæ´")) { |
| | | return StringUtils.hasText(approveId) |
| | | ? approveTodoTools.updateTodo( |
| | | memoryId, |
| | |
| | | extractValue(text, "夿³¨")) |
| | | : missingApproveId("update_action", "ä¿®æ¹å®¡æ¹åéè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (containsAny(text, "å表", "å¾
å", "æ¥è¯¢å®¡æ¹")) { |
| | | if (containsAny(text, "å表", "å¾
å", "æ¥è¯¢å®¡æ¹", "åæ®", "æµç¨", "å®¡æ¹æ¹")) { |
| | | return approveTodoTools.listTodos( |
| | | memoryId, |
| | | extractStatus(text), |
| | | extractApproveType(text), |
| | | extractKeyword(text), |
| | | extractLimit(text)); |
| | | extractLimit(text), |
| | | extractScope(text)); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private boolean isStatsIntent(String text) { |
| | | if (containsAny(text, "ç»è®¡", "åæ", "å¾è¡¨", "è¶å¿", "å æ¯", "æ±æ»", "æ»é")) { |
| | | if (containsAny(text, "ç»è®¡", "åæ", "å¾è¡¨", "è¶å¿", "å æ¯", "æ±æ»", "æ»é", "åå¸", "åæå¤å°", "æå¤å°")) { |
| | | return true; |
| | | } |
| | | boolean hasQueryWord = containsAny(text, "æ¥è¯¢", "æ¥ç", "çä¸", "çç", "è·å"); |
| | |
| | | if (containsAny(text, "å¾
å®¡æ ¸", "å¾
审æ¹")) { |
| | | return "pending"; |
| | | } |
| | | if (containsAny(text, "å®¡æ ¸ä¸")) { |
| | | if (containsAny(text, "å®¡æ ¸ä¸", "å¤çä¸", "å¤çä¸ç", "åçä¸")) { |
| | | return "processing"; |
| | | } |
| | | if (containsAny(text, "å·²éè¿", "å®¡æ ¸å®æ")) { |
| | | if (containsAny(text, "å·²éè¿", "éè¿", "å®¡æ ¸å®æ", "审æ¹å®æ")) { |
| | | return "approved"; |
| | | } |
| | | if (containsAny(text, "æªéè¿", "驳å")) { |
| | | if (containsAny(text, "æªéè¿", "驳å", "已驳å", "æç»")) { |
| | | return "rejected"; |
| | | } |
| | | if (containsAny(text, "éæ°æäº¤")) { |
| | |
| | | private String extractKeyword(String text) { |
| | | String cleaned = text |
| | | .replace("æ¥è¯¢", "") |
| | | .replace("æ¥ç", "") |
| | | .replace("ååº", "") |
| | | .replace("帮æ", "") |
| | | .replace("审æ¹", "") |
| | | .replace("åæ®", "") |
| | | .replace("å¾
å", "") |
| | | .replace("å表", "") |
| | | .replace("å10æ¡", "") |
| | |
| | | return matcher.find() ? matcher.group(2).trim() : null; |
| | | } |
| | | |
| | | private String extractScope(String text) { |
| | | if (containsAny(text, "æåèµ·", "ææäº¤", "æç³è¯·", "ç³è¯·äººæ¯æ")) { |
| | | return "applicant"; |
| | | } |
| | | if (containsAny(text, "å¾
æå®¡æ¹", "å¾
æå®¡æ ¸", "æå¤ç", "æå®¡æ¹", "å½åå¾
æ", "éè¦æå¤ç")) { |
| | | return "approver"; |
| | | } |
| | | return "related"; |
| | | } |
| | | |
| | | private String firstNonBlank(String first, String second) { |
| | | return StringUtils.hasText(first) ? first : second; |
| | | } |
| | | |
| | | private String missingApproveId(String type, String description) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("success", false); |
| | |
| | | } |
| | | String text = message.trim(); |
| | | |
| | | if (containsAny(text, "æè¡", "æå", "åå ", "åäº", "åå") && containsAny(text, "ç©æ", "产å", "åææ", "éè´éé¢", "éé¢")) { |
| | | return purchaseAgentTools.rankPurchaseMaterials( |
| | | memoryId, |
| | | extractStartDate(text), |
| | | extractEndDate(text), |
| | | text, |
| | | extractLimit(text) |
| | | ); |
| | | } |
| | | if (containsAny(text, "æªå
¥åº", "å¾
å
¥åº", "没æå
¥åº", "è¿æªå
¥åº")) { |
| | | return purchaseAgentTools.listUnstockedPurchaseOrders( |
| | | memoryId, |
| | | extractStartDate(text), |
| | | extractEndDate(text), |
| | | extractKeyword(text), |
| | | extractLimit(text) |
| | | ); |
| | | } |
| | | if (containsAny(text, "å°è´§å¼å¸¸", "å°è´§æå¼å¸¸", "å¼å¸¸å°è´§", "å°è´§é®é¢", "ä¾åºåå°è´§å¼å¸¸")) { |
| | | return purchaseAgentTools.listArrivalExceptions( |
| | | memoryId, |
| | | extractStartDate(text), |
| | | extractEndDate(text), |
| | | text, |
| | | extractLimit(text) |
| | | ); |
| | | } |
| | | if (containsAny(text, "å¾
仿¬¾", "æªä»æ¬¾", "æªä»æ¸
", "å¾
æ¯ä»", "åºä»")) { |
| | | return purchaseAgentTools.listPendingPaymentOrders( |
| | | memoryId, |
| | | extractStartDate(text), |
| | | extractEndDate(text), |
| | | extractKeyword(text), |
| | | extractLimit(text) |
| | | ); |
| | | } |
| | | if (containsAny(text, "éè´§", "éæ", "ææ¶")) { |
| | | return purchaseAgentTools.listPurchaseReturns( |
| | | memoryId, |
| | | extractStartDate(text), |
| | | extractEndDate(text), |
| | | extractKeyword(text), |
| | | extractLimit(text) |
| | | ); |
| | | } |
| | | if (isStatsIntent(text)) { |
| | | return purchaseAgentTools.getPurchaseStats( |
| | | memoryId, |
| | |
| | | if (containsAny(text, "详æ
", "æç»") && extractId(text) != null) { |
| | | return purchaseAgentTools.getPurchaseLedgerDetail(memoryId, extractId(text)); |
| | | } |
| | | if (containsAny(text, "å°è´¦", "éè´å", "åå", "å表", "æ¥è¯¢")) { |
| | | if (containsAny(text, "å°è´¦", "éè´å", "éè´è®¢å", "订å", "åå", "å表", "æ¥è¯¢")) { |
| | | return purchaseAgentTools.listPurchaseLedgers( |
| | | memoryId, |
| | | extractKeyword(text), |
| | |
| | | } |
| | | |
| | | private boolean isStatsIntent(String text) { |
| | | if (containsAny(text, "ç»è®¡", "åæ", "æ¥è¡¨", "æ±æ»", "è¶å¿", "æ°æ®çæ¿")) { |
| | | if (containsAny(text, "ç»è®¡", "åæ", "æ¥è¡¨", "æ±æ»", "è¶å¿", "æ°æ®çæ¿", "æ
åµ", "æå¤å°")) { |
| | | return true; |
| | | } |
| | | boolean queryWord = containsAny(text, "æ¥è¯¢", "æ¥ç", "çä¸", "çç", "è·å"); |
| | |
| | | .replace("æ¥è¯¢", "") |
| | | .replace("æ¥ç", "") |
| | | .replace("éè´", "") |
| | | .replace("éè´å", "") |
| | | .replace("éè´è®¢å", "") |
| | | .replace("订å", "") |
| | | .replace("å°è´¦", "") |
| | | .replace("å表", "") |
| | | .replace("åªäº", "") |
| | | .replace("ååº", "") |
| | | .replace("帮æ", "") |
| | | .replace("æè¿10æ¡", "") |
| | | .replace("å10æ¡", "") |
| | | .trim(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package com.ruoyi.ai.bean; |
| | | |
| | | import java.util.Map; |
| | | |
| | | public class PurchaseAiConfirmRequest { |
| | | |
| | | private String businessType; |
| | | |
| | | private Map<String, Object> payload; |
| | | |
| | | public String getBusinessType() { |
| | | return businessType; |
| | | } |
| | | |
| | | public void setBusinessType(String businessType) { |
| | | this.businessType = businessType; |
| | | } |
| | | |
| | | public Map<String, Object> getPayload() { |
| | | return payload; |
| | | } |
| | | |
| | | public void setPayload(Map<String, Object> payload) { |
| | | this.payload = payload; |
| | | } |
| | | } |
| | |
| | | package com.ruoyi.ai.config; |
| | | |
| | | import com.ruoyi.ai.store.MongoChatMemoryStore; |
| | | import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel; |
| | | import dev.langchain4j.memory.chat.ChatMemoryProvider; |
| | | import dev.langchain4j.memory.chat.MessageWindowChatMemory; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | |
| | |
| | | .chatMemoryStore(mongoChatMemoryStore) |
| | | .build(); |
| | | } |
| | | |
| | | @Bean("purchaseVisionStreamingChatModel") |
| | | QwenStreamingChatModel purchaseVisionStreamingChatModel( |
| | | @Value("${langchain4j.community.dashscope.streaming-chat-model.api-key}") String apiKey) { |
| | | return QwenStreamingChatModel.builder() |
| | | .apiKey(apiKey) |
| | | .modelName("qwen-vl-max") |
| | | .isMultimodalModel(true) |
| | | .build(); |
| | | } |
| | | } |
| | |
| | | package com.ruoyi.ai.controller; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.ruoyi.ai.assistant.PurchaseAgent; |
| | | import com.ruoyi.ai.assistant.PurchaseIntentExecutor; |
| | | import com.ruoyi.ai.bean.ChatForm; |
| | | import com.ruoyi.ai.bean.PurchaseAiConfirmRequest; |
| | | import com.ruoyi.ai.context.AiSessionUserContext; |
| | | import com.ruoyi.ai.service.AiChatSessionService; |
| | | import com.ruoyi.ai.service.AiFileTextExtractor; |
| | | import com.ruoyi.ai.store.MongoChatMemoryStore; |
| | | import com.ruoyi.basic.mapper.SupplierManageMapper; |
| | | import com.ruoyi.basic.pojo.SupplierManage; |
| | | import com.ruoyi.common.utils.SecurityUtils; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.framework.security.LoginUser; |
| | | import com.ruoyi.framework.web.controller.BaseController; |
| | | import com.ruoyi.framework.web.domain.AjaxResult; |
| | | import com.ruoyi.purchase.dto.PurchaseLedgerDto; |
| | | import com.ruoyi.purchase.dto.PurchaseReturnOrderDto; |
| | | import com.ruoyi.purchase.pojo.PaymentRegistration; |
| | | import com.ruoyi.purchase.service.IPaymentRegistrationService; |
| | | import com.ruoyi.purchase.service.IPurchaseLedgerService; |
| | | import com.ruoyi.purchase.service.PurchaseReturnOrdersService; |
| | | import com.ruoyi.sales.pojo.SalesLedgerProduct; |
| | | import dev.langchain4j.data.image.Image; |
| | | import dev.langchain4j.data.message.AiMessage; |
| | | import dev.langchain4j.data.message.ChatMessage; |
| | | import dev.langchain4j.data.message.Content; |
| | | import dev.langchain4j.data.message.ImageContent; |
| | | import dev.langchain4j.data.message.SystemMessage; |
| | | import dev.langchain4j.data.message.TextContent; |
| | | import dev.langchain4j.data.message.UserMessage; |
| | | import dev.langchain4j.model.chat.StreamingChatLanguageModel; |
| | | import dev.langchain4j.model.chat.response.ChatResponse; |
| | | import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; |
| | | import io.swagger.v3.oas.annotations.Operation; |
| | | import io.swagger.v3.oas.annotations.tags.Tag; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.DeleteMapping; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | import reactor.core.publisher.Flux; |
| | | |
| | | import java.io.IOException; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.Base64; |
| | | import java.time.LocalDate; |
| | | import java.time.ZoneId; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Date; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.NoSuchElementException; |
| | | import java.util.UUID; |
| | | |
| | | @Tag(name = "éè´æºè½ä½") |
| | | @RestController |
| | | @RequestMapping("/purchase-ai") |
| | | public class PurchaseAiController extends BaseController { |
| | | |
| | | private static final String PURCHASE_FILE_ANALYZE_MEMORY_PREFIX = "purchase-file-analyze::"; |
| | | private static final int MAX_FILE_COUNT = 10; |
| | | private static final int MAX_SINGLE_FILE_TEXT_LENGTH = 8000; |
| | | private static final int MAX_TOTAL_FILE_TEXT_LENGTH = 30000; |
| | | |
| | | private final PurchaseAgent purchaseAgent; |
| | | private final PurchaseIntentExecutor purchaseIntentExecutor; |
| | | private final AiSessionUserContext aiSessionUserContext; |
| | | private final MongoChatMemoryStore mongoChatMemoryStore; |
| | | private final AiChatSessionService aiChatSessionService; |
| | | private final AiFileTextExtractor aiFileTextExtractor; |
| | | private final ObjectMapper objectMapper; |
| | | private final IPurchaseLedgerService purchaseLedgerService; |
| | | private final IPaymentRegistrationService paymentRegistrationService; |
| | | private final PurchaseReturnOrdersService purchaseReturnOrdersService; |
| | | private final SupplierManageMapper supplierManageMapper; |
| | | private final StreamingChatLanguageModel purchaseVisionStreamingChatModel; |
| | | |
| | | public PurchaseAiController(PurchaseAgent purchaseAgent, |
| | | PurchaseIntentExecutor purchaseIntentExecutor, |
| | | AiSessionUserContext aiSessionUserContext, |
| | | MongoChatMemoryStore mongoChatMemoryStore, |
| | | AiChatSessionService aiChatSessionService) { |
| | | AiChatSessionService aiChatSessionService, |
| | | AiFileTextExtractor aiFileTextExtractor, |
| | | ObjectMapper objectMapper, |
| | | IPurchaseLedgerService purchaseLedgerService, |
| | | IPaymentRegistrationService paymentRegistrationService, |
| | | PurchaseReturnOrdersService purchaseReturnOrdersService, |
| | | SupplierManageMapper supplierManageMapper, |
| | | @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) { |
| | | this.purchaseAgent = purchaseAgent; |
| | | this.purchaseIntentExecutor = purchaseIntentExecutor; |
| | | this.aiSessionUserContext = aiSessionUserContext; |
| | | this.mongoChatMemoryStore = mongoChatMemoryStore; |
| | | this.aiChatSessionService = aiChatSessionService; |
| | | this.aiFileTextExtractor = aiFileTextExtractor; |
| | | this.objectMapper = objectMapper; |
| | | this.purchaseLedgerService = purchaseLedgerService; |
| | | this.paymentRegistrationService = paymentRegistrationService; |
| | | this.purchaseReturnOrdersService = purchaseReturnOrdersService; |
| | | this.supplierManageMapper = supplierManageMapper; |
| | | this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel; |
| | | } |
| | | |
| | | @Operation(summary = "éè´å¯¹è¯") |
| | |
| | | .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser)); |
| | | } |
| | | |
| | | @Operation(summary = "éè´å¤æä»¶åæ") |
| | | @PostMapping(value = "/analyze-files", consumes = "multipart/form-data", produces = "text/stream;charset=utf-8") |
| | | public Flux<String> analyzeFiles(@RequestParam("files") MultipartFile[] files, |
| | | @RequestParam(value = "message", required = false) String message, |
| | | @RequestParam(value = "memoryId", required = false) String memoryId) { |
| | | if (files == null || files.length == 0) { |
| | | return Flux.just("filesä¸è½ä¸ºç©º"); |
| | | } |
| | | if (files.length > MAX_FILE_COUNT) { |
| | | return Flux.just("䏿¬¡æå¤åæ" + MAX_FILE_COUNT + "个æä»¶"); |
| | | } |
| | | |
| | | String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString(); |
| | | String finalMemoryId = rawMemoryId.startsWith(PURCHASE_FILE_ANALYZE_MEMORY_PREFIX) |
| | | ? rawMemoryId |
| | | : PURCHASE_FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId; |
| | | |
| | | LoginUser loginUser = SecurityUtils.getLoginUser(); |
| | | aiSessionUserContext.bind(finalMemoryId, loginUser); |
| | | |
| | | String finalMessage = StringUtils.hasText(message) |
| | | ? message |
| | | : "请åæè¿äºéè´æä»¶ï¼æåå¯ç¨äºä¸å¡å¤ççæ°æ®ï¼å¹¶æ´çæå¾
客æ·ç¡®è®¤çæ ¼å¼"; |
| | | |
| | | String fileContent; |
| | | try { |
| | | fileContent = buildMultiFileContent(files); |
| | | } catch (IllegalArgumentException ex) { |
| | | return Flux.just(ex.getMessage()); |
| | | } catch (IOException ex) { |
| | | return Flux.just("æä»¶è¯»å失败"); |
| | | } |
| | | |
| | | if (!StringUtils.hasText(fileContent)) { |
| | | return Flux.just("æªæåå°æææä»¶å
容"); |
| | | } |
| | | |
| | | String userPrompt = buildPurchaseFileAnalyzePrompt(finalMessage, fileContent); |
| | | aiChatSessionService.touchSession(finalMemoryId, loginUser, "éè´å¤æä»¶åæ: " + finalMessage); |
| | | |
| | | if (containsImageFile(files)) { |
| | | return chatWithPurchaseVisionModel(finalMemoryId, userPrompt, files) |
| | | .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)) |
| | | .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)); |
| | | } |
| | | |
| | | return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt)) |
| | | .onErrorResume(NoSuchElementException.class, ex -> { |
| | | mongoChatMemoryStore.deleteMessages(finalMemoryId); |
| | | return purchaseAgent.chat(finalMemoryId, userPrompt); |
| | | }) |
| | | .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)) |
| | | .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)); |
| | | } |
| | | |
| | | @Operation(summary = "éè´å¤æä»¶åæç¡®è®¤å¤ç") |
| | | @PostMapping("/analyze-files/confirm") |
| | | public AjaxResult confirmAnalyzeResult(@RequestBody PurchaseAiConfirmRequest request) { |
| | | if (request == null || !StringUtils.hasText(request.getBusinessType())) { |
| | | return AjaxResult.error("businessTypeä¸è½ä¸ºç©º"); |
| | | } |
| | | if (request.getPayload() == null || request.getPayload().isEmpty()) { |
| | | return AjaxResult.error("payloadä¸è½ä¸ºç©º"); |
| | | } |
| | | |
| | | try { |
| | | String businessType = request.getBusinessType().trim(); |
| | | return switch (businessType) { |
| | | case "purchase_ledger" -> processPurchaseLedger(request.getPayload()); |
| | | case "payment_registration" -> processPaymentRegistration(request.getPayload()); |
| | | case "purchase_return_order" -> processPurchaseReturnOrder(request.getPayload()); |
| | | default -> AjaxResult.error("æä¸æ¯æè¯¥ä¸å¡ç±»å: " + businessType); |
| | | }; |
| | | } catch (Exception ex) { |
| | | return AjaxResult.error(toCustomerMessage(ex)); |
| | | } |
| | | } |
| | | |
| | | @Operation(summary = "éè´ä¼è¯å表") |
| | | @GetMapping("/history/sessions") |
| | | public AjaxResult listSessions() { |
| | |
| | | aiSessionUserContext.remove(memoryId); |
| | | return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser())); |
| | | } |
| | | |
| | | private String buildMultiFileContent(MultipartFile[] files) throws IOException { |
| | | StringBuilder builder = new StringBuilder(); |
| | | int totalLength = 0; |
| | | for (MultipartFile file : files) { |
| | | String text = aiFileTextExtractor.extractText(file); |
| | | if (!StringUtils.hasText(text)) { |
| | | continue; |
| | | } |
| | | String limitedText = text.length() > MAX_SINGLE_FILE_TEXT_LENGTH |
| | | ? text.substring(0, MAX_SINGLE_FILE_TEXT_LENGTH) |
| | | : text; |
| | | if (totalLength + limitedText.length() > MAX_TOTAL_FILE_TEXT_LENGTH) { |
| | | int remain = MAX_TOTAL_FILE_TEXT_LENGTH - totalLength; |
| | | if (remain <= 0) { |
| | | break; |
| | | } |
| | | limitedText = limitedText.substring(0, remain); |
| | | } |
| | | builder.append("\n--- æä»¶: ") |
| | | .append(file.getOriginalFilename()) |
| | | .append(" ---\n") |
| | | .append(limitedText) |
| | | .append('\n'); |
| | | totalLength += limitedText.length(); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private boolean containsImageFile(MultipartFile[] files) { |
| | | for (MultipartFile file : files) { |
| | | if (aiFileTextExtractor.isImageFile(file)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private Flux<String> chatWithPurchaseVisionModel(String memoryId, String userPrompt, MultipartFile[] files) { |
| | | return Flux.create(sink -> { |
| | | try { |
| | | List<Content> contents = new ArrayList<>(); |
| | | contents.add(TextContent.from(userPrompt)); |
| | | for (MultipartFile file : files) { |
| | | if (!aiFileTextExtractor.isImageFile(file)) { |
| | | continue; |
| | | } |
| | | contents.add(TextContent.from("ä¸é¢è¿å¼ å¾çæä»¶åï¼" + file.getOriginalFilename())); |
| | | contents.add(ImageContent.from(Image.builder() |
| | | .base64Data(Base64.getEncoder().encodeToString(file.getBytes())) |
| | | .mimeType(resolveImageMimeType(file)) |
| | | .build())); |
| | | } |
| | | |
| | | List<ChatMessage> messages = List.of( |
| | | SystemMessage.from("ä½ æ¯éè´ä¸å¡æä»¶åæå©æãè¯·ä»ææ¬åå¾çä¸è¯å«éè´å°è´¦ãéè´äº§åæç»ã仿¬¾æéè´§ä¿¡æ¯ï¼åªè¾åºåæ³ JSONã"), |
| | | UserMessage.from(contents) |
| | | ); |
| | | purchaseVisionStreamingChatModel.chat(messages, new StreamingChatResponseHandler() { |
| | | @Override |
| | | public void onPartialResponse(String partialResponse) { |
| | | sink.next(partialResponse); |
| | | } |
| | | |
| | | @Override |
| | | public void onCompleteResponse(ChatResponse completeResponse) { |
| | | sink.complete(); |
| | | } |
| | | |
| | | @Override |
| | | public void onError(Throwable error) { |
| | | sink.error(error); |
| | | } |
| | | }); |
| | | } catch (Exception ex) { |
| | | sink.next("å¾çæä»¶è¯»å失败ï¼è¯·ç¡®è®¤å¾çæ ¼å¼ä¸º pngãjpgãjpegãwebp æ bmpï¼ä¸å¤§å°ä¸è¶
è¿10MB"); |
| | | sink.complete(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private String resolveImageMimeType(MultipartFile file) { |
| | | String contentType = file.getContentType(); |
| | | if (StringUtils.hasText(contentType) && contentType.startsWith("image/")) { |
| | | return contentType; |
| | | } |
| | | String filename = file.getOriginalFilename(); |
| | | String ext = ""; |
| | | if (StringUtils.hasText(filename) && filename.contains(".")) { |
| | | ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); |
| | | } |
| | | return switch (ext) { |
| | | case "jpg", "jpeg" -> "image/jpeg"; |
| | | case "webp" -> "image/webp"; |
| | | case "bmp" -> "image/bmp"; |
| | | default -> "image/png"; |
| | | }; |
| | | } |
| | | |
| | | private String buildPurchaseFileAnalyzePrompt(String message, String fileContent) { |
| | | return """ |
| | | ä½ æ¯éè´ä¸å¡æä»¶åæå©æãè¯·ä¸¥æ ¼æ ¹æ®ç¨æ·ä¸ä¼ çå¤ä¸ªæä»¶åç¨æ·è¦æ±æåéè´ä¸å¡æ°æ®ã |
| | | |
| | | ç¨æ·è¦æ±: |
| | | %s |
| | | |
| | | è¾åºè¦æ±: |
| | | 1. åªè¾åºåæ³ JSONï¼ä¸è¦ Markdownï¼ä¸è¦é¢å¤è§£éã |
| | | 2. JSON é¡¶å±å段åºå®ä¸º: |
| | | - success: boolean |
| | | - businessType: purchase_ledger | payment_registration | purchase_return_order | unknown |
| | | - action: confirm_required |
| | | - description: ä¸æè¯´æ |
| | | - confidence: 0å°1çå°æ° |
| | | - missingFields: ç¼ºå¤±åæ®µä¸æåç§°æ°ç»ï¼é¢å客æ·å±ç¤ºï¼ä¸è¦è¾åºè±æå段å |
| | | - warnings: é£é©æç¤ºæ°ç» |
| | | - payload: å¾
客æ·ç¡®è®¤çæ°æ®ï¼å段åå¿
须使ç¨å端 DTO åæ®µå |
| | | - preview: ç»å®¢æ·ç¡®è®¤ç¨ç䏿æè¦æ°ç» |
| | | 3. 妿å¯å¤æä¸ºéè´å°è´¦ï¼businessType ä½¿ç¨ purchase_ledgerï¼payload.purchaseLedgers 为éè´è®¢å/éè´å°è´¦æ°ç»: |
| | | - purchaseLedgers: éè´è®¢å/éè´å°è´¦æ°ç»ï¼æ¯æ¡è®°å½å段åå¿
é¡»ä¸ PurchaseLedgerDto ä¿æä¸è´ |
| | | - 产åæç»å¿
é¡»æ¾å¨æ¯æ¡éè´å°è´¦è®°å½ç productData åæ®µä¸ï¼productData ç±»å为 List<SalesLedgerProduct> |
| | | - ä¸è¦ä¼å
ä½¿ç¨ payload é¡¶å± productDataï¼é¡¶å± productData ä»
ä½ä¸ºæ§æ ¼å¼å
¼å®¹ |
| | | - æä»¶éçâéè´åå·âå°±æ¯âéè´ååå·âï¼ç»ä¸æ å°ä¸º purchaseContractNumber |
| | | - æä»¶éçâéå®åå·âå°±æ¯âéå®ååå·âï¼ç»ä¸æ å°ä¸º salesContractNo |
| | | - æææ¥æå段å¿
é¡»ä½¿ç¨ yyyy-MM-ddï¼ä¾å¦ 2026-04-30ï¼ä¸è¦è¾åº 4/30/26ã2026/4/30ã2026å¹´4æ30æ¥ æå¸¦æ¶åç§çæ ¼å¼ |
| | | - éè´å°è´¦ä¸éè¦å¨ payload ä¸ä¼ 审æ¹äººï¼ä¸è¦è¾åº approveUserIdsãapproverId |
| | | - missingFields åªå¡«åä¸å¡å¿
填使 æ³è¯å«çåæ®µï¼ä¸è¦æ PurchaseLedgerDto çææç©ºåæ®µé½å为缺失ï¼ç¼ºå¤±é¡¹å¿
é¡»å䏿ï¼ä¾å¦âä¾åºååç§°ââå«ç¨åä»·âï¼ä¸è¦å supplierIdãtaxInclusiveUnitPrice |
| | | - éè´å°è´¦ä¸»è¡¨å¿
å¡«åæ®µä»
æè¿äºå¤æ: purchaseContractNumberãsupplierName æ supplierId |
| | | - productData æ¯æ¡äº§åå¿
å¡«åæ®µ: productCategoryãspecificationModelãunitãquantityãtaxInclusiveUnitPrice æ taxInclusiveTotalPriceï¼å¦æåªæå«ç¨æ»ä»·åæ°éï¼å¿
é¡»è®¡ç® taxInclusiveUnitPriceï¼å¦æåªæå«ç¨åä»·åæ°éï¼å¿
é¡»è®¡ç® taxInclusiveTotalPrice |
| | | - 产ååæ®µæéè´å¯¼å
¥æ¥å£ PurchaseLedgerProductImportDto 对é½: éè´åå·ã产å大类ãè§æ ¼åå·ãåä½ãæ°éãç¨çãå«ç¨åä»·ãå«ç¨æ»ä»·ãå票类åãæ¯å¦è´¨æ£ |
| | | - éè´äº§å type åºå®ä¸º 2 |
| | | - purchaseLedgers æ¯æ¡è®°å½åªä½¿ç¨è¿äº PurchaseLedgerDto åæ®µå: |
| | | entryDateStart, entryDateEnd, id, purchaseContractNumber, supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo, salesContractNoId, projectName, entryDate, executionDate, remarks, attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type, productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId, productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId, contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type, paymentMethod, approvalStatus, templateName |
| | | - productData æ¯æ¡äº§ååªä½¿ç¨è¿äº SalesLedgerProduct åæ®µå: |
| | | productCategory, specificationModel, unit, quantity, taxRate, taxInclusiveUnitPrice, taxInclusiveTotalPrice, taxExclusiveTotalPrice, invoiceType, productId, productModelId, isChecked, type |
| | | 4. 妿å¯å¤æä¸ºä»æ¬¾ç»è®°ï¼businessType ä½¿ç¨ payment_registrationï¼payload.records ä¸ºä»æ¬¾ç»è®°æ°ç»ï¼å段尽éå
å« purchaseLedgerIdãsalesLedgerProductIdãcurrentPaymentAmountãpaymentMethodãpaymentDateã |
| | | 5. 妿å¯å¤æä¸ºéè´éè´§ï¼businessType ä½¿ç¨ purchase_return_orderï¼payload æ PurchaseReturnOrderDto ç»ç»ï¼æç»æ¾ purchaseReturnOrderProductsDtosã |
| | | 6. 缺å°ä¸å¡å¤çå¿
须忮µæ¶ï¼ä¸è¦ç¼é IDï¼æåæ®µæ¾å
¥ missingFieldsï¼å¹¶ä»è¿åå¯ç¡®è®¤çèç¨¿æ°æ®ã |
| | | 7. ææä¸æå
å®¹ç´æ¥ä¿çï¼ä¸è¦è½¬ä¹æ Unicodeã |
| | | |
| | | æä»¶å
容: |
| | | %s |
| | | """.formatted(message, fileContent); |
| | | } |
| | | |
| | | private AjaxResult processPurchaseLedger(Map<String, Object> payload) throws Exception { |
| | | if (payload.containsKey("purchaseLedgers")) { |
| | | return processPurchaseLedgerBatch(payload); |
| | | } |
| | | |
| | | Map<String, Object> normalizedPayload = normalizePurchaseLedgerMap(payload); |
| | | PurchaseLedgerDto dto = objectMapper.convertValue(normalizedPayload, PurchaseLedgerDto.class); |
| | | AjaxResult ledgerResult = validatePurchaseLedger(dto, 0); |
| | | if (ledgerResult != null) { |
| | | return ledgerResult; |
| | | } |
| | | AjaxResult supplierResult = fillSupplierIdByName(dto); |
| | | if (supplierResult != null) { |
| | | return supplierResult; |
| | | } |
| | | AjaxResult productResult = validatePurchaseProducts(dto.getProductData(), 0); |
| | | if (productResult != null) { |
| | | return productResult; |
| | | } |
| | | int result = purchaseLedgerService.addOrEditPurchase(dto); |
| | | return AjaxResult.success("éè´å°è´¦å·²å¤ç", result); |
| | | } |
| | | |
| | | private AjaxResult processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception { |
| | | List<Map<String, Object>> purchaseLedgers = toMapList(payload.get("purchaseLedgers")); |
| | | if (purchaseLedgers.isEmpty()) { |
| | | return AjaxResult.error("purchaseLedgersä¸è½ä¸ºç©º"); |
| | | } |
| | | |
| | | List<Map<String, Object>> topLevelProductData = toMapList(payload.get("productData")); |
| | | List<Map<String, Object>> results = new ArrayList<>(); |
| | | for (int i = 0; i < purchaseLedgers.size(); i++) { |
| | | Map<String, Object> ledgerMap = normalizePurchaseLedgerMap(purchaseLedgers.get(i)); |
| | | PurchaseLedgerDto dto = objectMapper.convertValue(ledgerMap, PurchaseLedgerDto.class); |
| | | AjaxResult ledgerResult = validatePurchaseLedger(dto, i); |
| | | if (ledgerResult != null) { |
| | | return ledgerResult; |
| | | } |
| | | AjaxResult supplierResult = fillSupplierIdByName(dto); |
| | | if (supplierResult != null) { |
| | | return supplierResult; |
| | | } |
| | | |
| | | List<SalesLedgerProduct> products = dto.getProductData(); |
| | | if (products == null || products.isEmpty()) { |
| | | products = matchProductsForLedger(ledgerMap, dto, topLevelProductData, purchaseLedgers.size() == 1); |
| | | dto.setProductData(products); |
| | | } |
| | | AjaxResult productResult = validatePurchaseProducts(products, i); |
| | | if (productResult != null) { |
| | | return productResult; |
| | | } |
| | | int result = purchaseLedgerService.addOrEditPurchase(dto); |
| | | |
| | | Map<String, Object> item = new LinkedHashMap<>(); |
| | | item.put("index", i); |
| | | item.put("purchaseContractNumber", dto.getPurchaseContractNumber()); |
| | | item.put("supplierId", dto.getSupplierId()); |
| | | item.put("supplierName", dto.getSupplierName()); |
| | | item.put("productCount", products.size()); |
| | | item.put("result", result); |
| | | results.add(item); |
| | | } |
| | | return AjaxResult.success("éè´å°è´¦å·²æ¹éå¤ç", results); |
| | | } |
| | | |
| | | private List<SalesLedgerProduct> matchProductsForLedger(Map<String, Object> ledgerMap, |
| | | PurchaseLedgerDto dto, |
| | | List<Map<String, Object>> productData, |
| | | boolean onlyOneLedger) { |
| | | List<SalesLedgerProduct> products = new ArrayList<>(); |
| | | for (Map<String, Object> productMap : productData) { |
| | | if (onlyOneLedger || productBelongsToLedger(productMap, ledgerMap, dto)) { |
| | | products.add(objectMapper.convertValue(normalizeSalesLedgerProductMap(productMap), SalesLedgerProduct.class)); |
| | | } |
| | | } |
| | | return products; |
| | | } |
| | | |
| | | private boolean productBelongsToLedger(Map<String, Object> productMap, Map<String, Object> ledgerMap, PurchaseLedgerDto dto) { |
| | | Long productPurchaseLedgerId = longValue(productMap, "purchaseLedgerId", "purchaseId", "éè´è®¢åid", "éè´å°è´¦id"); |
| | | if (productPurchaseLedgerId != null && dto.getId() != null && productPurchaseLedgerId.equals(dto.getId())) { |
| | | return true; |
| | | } |
| | | |
| | | Long productSalesLedgerId = longValue(productMap, "salesLedgerId"); |
| | | if (productSalesLedgerId != null && dto.getId() != null && productSalesLedgerId.equals(dto.getId())) { |
| | | return true; |
| | | } |
| | | |
| | | String productContractNo = stringValue(productMap, "purchaseContractNumber", "purchaseContractNo", "éè´ååå·", "éè´åå·", "éè´è®¢åå·"); |
| | | if (StringUtils.hasText(productContractNo) |
| | | && StringUtils.hasText(dto.getPurchaseContractNumber()) |
| | | && productContractNo.trim().equals(dto.getPurchaseContractNumber().trim())) { |
| | | return true; |
| | | } |
| | | |
| | | String ledgerContractNo = stringValue(ledgerMap, "purchaseContractNumber", "purchaseContractNo", "éè´ååå·", "éè´åå·", "éè´è®¢åå·"); |
| | | if (StringUtils.hasText(productContractNo) |
| | | && StringUtils.hasText(ledgerContractNo) |
| | | && productContractNo.trim().equals(ledgerContractNo.trim())) { |
| | | return true; |
| | | } |
| | | |
| | | String productSalesContractNo = stringValue(productMap, "salesContractNo", "salesContractNumber", "éå®ååå·", "éå®åå·", "éå®è®¢åå·"); |
| | | if (StringUtils.hasText(productSalesContractNo) |
| | | && StringUtils.hasText(dto.getSalesContractNo()) |
| | | && productSalesContractNo.trim().equals(dto.getSalesContractNo().trim())) { |
| | | return true; |
| | | } |
| | | |
| | | String ledgerSalesContractNo = stringValue(ledgerMap, "salesContractNo", "salesContractNumber", "éå®ååå·", "éå®åå·", "éå®è®¢åå·"); |
| | | if (StringUtils.hasText(productSalesContractNo) |
| | | && StringUtils.hasText(ledgerSalesContractNo) |
| | | && productSalesContractNo.trim().equals(ledgerSalesContractNo.trim())) { |
| | | return true; |
| | | } |
| | | |
| | | String productSupplierName = stringValue(productMap, "supplierName", "ä¾åºååç§°"); |
| | | return StringUtils.hasText(productSupplierName) |
| | | && StringUtils.hasText(dto.getSupplierName()) |
| | | && productSupplierName.trim().equals(dto.getSupplierName().trim()); |
| | | } |
| | | |
| | | private Map<String, Object> normalizePurchaseLedgerMap(Map<String, Object> source) { |
| | | Map<String, Object> target = new LinkedHashMap<>(); |
| | | copyPurchaseLedgerDtoFields(source, target); |
| | | putDtoFieldIfPresent(source, target, "entryDateStart", "å½å
¥å¼å§æ¥æ", "å½å
¥æ¥æå¼å§"); |
| | | putDtoFieldIfPresent(source, target, "entryDateEnd", "å½å
¥ç»ææ¥æ", "å½å
¥æ¥æç»æ"); |
| | | putDtoFieldIfPresent(source, target, "id", "éè´å°è´¦id", "éè´è®¢åid", "主é®"); |
| | | putDtoFieldIfPresent(source, target, "purchaseContractNumber", "purchaseContractNo", "éè´ååå·", "éè´åå·", "éè´è®¢åå·"); |
| | | putDtoFieldIfPresent(source, target, "supplierId", "ä¾åºåid", "ä¾åºåID", "ä¾åºååç§°id", "ä¾åºååç§°ID"); |
| | | putDtoFieldIfPresent(source, target, "supplierName", "ä¾åºå", "ä¾åºååç§°"); |
| | | putDtoFieldIfPresent(source, target, "isWhite", "æ¯å¦ç½åå"); |
| | | putDtoFieldIfPresent(source, target, "recorderId", "å½å
¥äººid", "å½å
¥äººID", "å½å
¥äººå§åid", "å½å
¥äººå§åID"); |
| | | putDtoFieldIfPresent(source, target, "recorderName", "å½å
¥äºº", "å½å
¥äººå§å"); |
| | | putDtoFieldIfPresent(source, target, "salesContractNo", "salesContractNumber", "éå®ååå·", "éå®åå·", "éå®è®¢åå·"); |
| | | putDtoFieldIfPresent(source, target, "salesContractNoId", "éå®ååå·id", "éå®ååå·ID", "éå®åå·id", "éå®åå·ID"); |
| | | putDtoFieldIfPresent(source, target, "projectName", "项ç®", "项ç®åç§°"); |
| | | putDtoFieldIfPresent(source, target, "entryDate", "å½å
¥æ¥æ"); |
| | | putDtoFieldIfPresent(source, target, "executionDate", "ç¾è®¢æ¥æ", "ååç¾è®¢æ¥æ"); |
| | | putDtoFieldIfPresent(source, target, "remarks", "夿³¨", "说æ"); |
| | | putDtoFieldIfPresent(source, target, "attachmentMaterials", "éä»¶ææ", "éä»¶ææè·¯å¾æåç§°"); |
| | | putDtoFieldIfPresent(source, target, "createdAt", "å建æ¶é´", "è®°å½å建æ¶é´"); |
| | | putDtoFieldIfPresent(source, target, "updatedAt", "æ´æ°æ¶é´", "è®°å½æåæ´æ°æ¶é´"); |
| | | putDtoFieldIfPresent(source, target, "salesLedgerId", "éå®å°è´¦id", "éå®å°è´¦ID", "å
³èéå®å°è´¦ä¸»è¡¨ä¸»é®"); |
| | | putDtoFieldIfPresent(source, target, "hasChildren", "æ¯å¦æå级", "æ¯å¦ææç»"); |
| | | putDtoFieldIfPresent(source, target, "Type", "å°è´¦ç±»å", "ä¸å¡ç±»å"); |
| | | putDtoFieldIfPresent(source, target, "productData", "products", "产åæç»", "éè´äº§åæç»"); |
| | | putDtoFieldIfPresent(source, target, "tempFileIds", "ä¸´æ¶æä»¶id", "ä¸´æ¶æä»¶ID", "ä¸´æ¶æä»¶ids"); |
| | | putDtoFieldIfPresent(source, target, "SalesLedgerFiles", "éä»¶å表", "éå®å°è´¦éä»¶"); |
| | | putDtoFieldIfPresent(source, target, "phoneNumber", "ä¸å¡åææºå·", "ææºå·"); |
| | | putDtoFieldIfPresent(source, target, "businessPersonId", "ä¸å¡åid", "ä¸å¡åID"); |
| | | putDtoFieldIfPresent(source, target, "productId", "产åid", "产åID"); |
| | | putDtoFieldIfPresent(source, target, "productModelId", "产åè§æ ¼id", "产åè§æ ¼ID"); |
| | | putDtoFieldIfPresent(source, target, "invoiceNumber", "å票å·", "å票å·ç "); |
| | | putDtoFieldIfPresent(source, target, "invoiceAmount", "å票éé¢", "å票éé¢ï¼å
ï¼"); |
| | | putDtoFieldIfPresent(source, target, "ticketRegistrationId", "æ¥ç¥¨ç»è®°id", "æ¥ç¥¨ç»è®°ID"); |
| | | putDtoFieldIfPresent(source, target, "contractAmount", "ååéé¢", "ååéé¢ï¼äº§åå«ç¨æ»ä»·ï¼"); |
| | | putDtoFieldIfPresent(source, target, "receiptPaymentAmount", "æ¥ç¥¨éé¢", "å·²æ¥ç¥¨éé¢", "å·²æ¥ç¥¨éé¢(å
)"); |
| | | putDtoFieldIfPresent(source, target, "unReceiptPaymentAmount", "æªæ¥ç¥¨éé¢", "æªæ¥ç¥¨éé¢(å
)"); |
| | | putDtoFieldIfPresent(source, target, "type", "æä»¶ç±»å"); |
| | | putDtoFieldIfPresent(source, target, "paymentMethod", "仿¬¾æ¹å¼"); |
| | | putDtoFieldIfPresent(source, target, "approvalStatus", "审æ¹ç¶æ"); |
| | | putDtoFieldIfPresent(source, target, "templateName", "模æ¿åç§°"); |
| | | target.remove("approveUserIds"); |
| | | target.remove("approverId"); |
| | | normalizeNestedProductData(target); |
| | | attachImportStyleProductData(source, target); |
| | | if (target.get("type") == null) { |
| | | target.put("type", 2); |
| | | } |
| | | target.putIfAbsent("entryDate", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)); |
| | | normalizePurchaseLedgerDateFields(target); |
| | | return target; |
| | | } |
| | | |
| | | private void attachImportStyleProductData(Map<String, Object> source, Map<String, Object> target) { |
| | | if (target.get("productData") != null) { |
| | | return; |
| | | } |
| | | Map<String, Object> productMap = normalizeSalesLedgerProductMap(source); |
| | | if (hasImportStyleProductData(productMap)) { |
| | | target.put("productData", List.of(productMap)); |
| | | } |
| | | } |
| | | |
| | | private boolean hasImportStyleProductData(Map<String, Object> productMap) { |
| | | return hasMapText(productMap, "productCategory") |
| | | || hasMapText(productMap, "specificationModel") |
| | | || productMap.get("quantity") != null |
| | | || productMap.get("taxInclusiveUnitPrice") != null |
| | | || productMap.get("taxInclusiveTotalPrice") != null; |
| | | } |
| | | |
| | | private boolean hasMapText(Map<String, Object> map, String key) { |
| | | Object value = map.get(key); |
| | | return value != null && StringUtils.hasText(String.valueOf(value)); |
| | | } |
| | | |
| | | private void normalizeNestedProductData(Map<String, Object> target) { |
| | | Object productDataValue = target.get("productData"); |
| | | if (productDataValue == null) { |
| | | return; |
| | | } |
| | | List<Map<String, Object>> productMaps = toMapList(productDataValue); |
| | | List<Map<String, Object>> normalizedProducts = new ArrayList<>(); |
| | | for (Map<String, Object> productMap : productMaps) { |
| | | normalizedProducts.add(normalizeSalesLedgerProductMap(productMap)); |
| | | } |
| | | target.put("productData", normalizedProducts); |
| | | } |
| | | |
| | | private Map<String, Object> normalizeSalesLedgerProductMap(Map<String, Object> source) { |
| | | Map<String, Object> target = new LinkedHashMap<>(); |
| | | copySalesLedgerProductFields(source, target); |
| | | putDtoFieldIfPresent(source, target, "productCategory", "产å大类", "产ååç§°", "产å", "åå", "ç©æåç§°"); |
| | | putDtoFieldIfPresent(source, target, "specificationModel", "è§æ ¼åå·", "åå·", "è§æ ¼", "产åè§æ ¼"); |
| | | putDtoFieldIfPresent(source, target, "unit", "åä½"); |
| | | putDtoFieldIfPresent(source, target, "quantity", "æ°é", "éè´æ°é"); |
| | | putDtoFieldIfPresent(source, target, "taxRate", "ç¨ç"); |
| | | putDtoFieldIfPresent(source, target, "taxInclusiveUnitPrice", "å«ç¨åä»·", "åä»·", "éè´åä»·", "å«ç¨ä»·æ ¼"); |
| | | putDtoFieldIfPresent(source, target, "taxInclusiveTotalPrice", "å«ç¨æ»ä»·", "æ»ä»·", "éè´éé¢", "éé¢", "ååéé¢"); |
| | | putDtoFieldIfPresent(source, target, "taxExclusiveTotalPrice", "ä¸å«ç¨æ»ä»·"); |
| | | putDtoFieldIfPresent(source, target, "invoiceType", "å票类å", "å票类å«"); |
| | | putDtoFieldIfPresent(source, target, "productId", "产åid", "产åID"); |
| | | putDtoFieldIfPresent(source, target, "productModelId", "产åè§æ ¼id", "产åè§æ ¼ID", "åå·id", "åå·ID"); |
| | | putDtoFieldIfPresent(source, target, "isChecked", "æ¯å¦è´¨æ£", "æ¯å¦è´¨æ£éª", "è´¨æ£"); |
| | | putDtoFieldIfPresent(source, target, "type", "å°è´¦ç±»å"); |
| | | normalizeProductAmounts(target); |
| | | target.putIfAbsent("type", 2); |
| | | return target; |
| | | } |
| | | |
| | | private void copySalesLedgerProductFields(Map<String, Object> source, Map<String, Object> target) { |
| | | String[] productFields = { |
| | | "id", "salesLedgerId", "warnNum", "productCategory", "specificationModel", "unit", |
| | | "speculativeTradingName", "quantity", "minStock", "taxRate", "taxInclusiveUnitPrice", |
| | | "taxInclusiveTotalPrice", "taxExclusiveTotalPrice", "invoiceType", "type", "ticketsNum", |
| | | "ticketsAmount", "futureTickets", "futureTicketsAmount", "invoiceNum", "noInvoiceNum", |
| | | "invoiceAmount", "noInvoiceAmount", "productId", "productModelId", "register", "registerDate", |
| | | "approveStatus", "pendingInvoiceTotal", "invoiceTotal", "pendingTicketsTotal", "ticketsTotal", |
| | | "isChecked", "isProduction" |
| | | }; |
| | | for (String field : productFields) { |
| | | if (source.containsKey(field)) { |
| | | target.put(field, source.get(field)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void normalizeProductAmounts(Map<String, Object> target) { |
| | | BigDecimal quantity = decimalValue(target.get("quantity")); |
| | | BigDecimal unitPrice = decimalValue(target.get("taxInclusiveUnitPrice")); |
| | | BigDecimal totalPrice = decimalValue(target.get("taxInclusiveTotalPrice")); |
| | | if (unitPrice == null && totalPrice != null && quantity != null && quantity.compareTo(BigDecimal.ZERO) != 0) { |
| | | target.put("taxInclusiveUnitPrice", totalPrice.divide(quantity, 6, RoundingMode.HALF_UP)); |
| | | } |
| | | if (totalPrice == null && unitPrice != null && quantity != null) { |
| | | target.put("taxInclusiveTotalPrice", unitPrice.multiply(quantity)); |
| | | } |
| | | BigDecimal taxRate = decimalValue(target.get("taxRate")); |
| | | totalPrice = decimalValue(target.get("taxInclusiveTotalPrice")); |
| | | if (target.get("taxExclusiveTotalPrice") == null && totalPrice != null && taxRate != null) { |
| | | BigDecimal divisor = BigDecimal.ONE.add(taxRate.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP)); |
| | | target.put("taxExclusiveTotalPrice", totalPrice.divide(divisor, 2, RoundingMode.HALF_UP)); |
| | | } |
| | | } |
| | | |
| | | private AjaxResult validatePurchaseProducts(List<SalesLedgerProduct> products, int ledgerIndex) { |
| | | if (products == null || products.isEmpty()) { |
| | | return null; |
| | | } |
| | | for (int i = 0; i < products.size(); i++) { |
| | | SalesLedgerProduct product = products.get(i); |
| | | String prefix = "第" + (ledgerIndex + 1) + "个éè´å°è´¦ç第" + (i + 1) + "æ¡äº§å"; |
| | | if (!StringUtils.hasText(product.getProductCategory())) { |
| | | return AjaxResult.error(prefix + "缺å°äº§ååç§°ï¼è¯·è¡¥å
åå确认"); |
| | | } |
| | | if (!StringUtils.hasText(product.getSpecificationModel())) { |
| | | return AjaxResult.error(prefix + "缺å°è§æ ¼åå·ï¼è¯·è¡¥å
åå确认"); |
| | | } |
| | | if (!StringUtils.hasText(product.getUnit())) { |
| | | return AjaxResult.error(prefix + "缺å°åä½ï¼è¯·è¡¥å
åå确认"); |
| | | } |
| | | if (product.getQuantity() == null) { |
| | | return AjaxResult.error(prefix + "ç¼ºå°æ°é"); |
| | | } |
| | | if (product.getTaxInclusiveUnitPrice() == null) { |
| | | return AjaxResult.error(prefix + "缺å°å«ç¨åä»·ï¼è¯·è¡¥å
åå确认"); |
| | | } |
| | | if (product.getTaxInclusiveTotalPrice() == null) { |
| | | return AjaxResult.error(prefix + "缺å°å«ç¨æ»ä»·ï¼è¯·è¡¥å
åå确认"); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private AjaxResult validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) { |
| | | String prefix = "第" + (ledgerIndex + 1) + "个éè´å°è´¦"; |
| | | if (!StringUtils.hasText(dto.getPurchaseContractNumber())) { |
| | | return AjaxResult.error(prefix + "缺å°éè´ååå·ï¼è¯·è¡¥å
åå确认"); |
| | | } |
| | | if (dto.getSupplierId() == null && !StringUtils.hasText(dto.getSupplierName())) { |
| | | return AjaxResult.error(prefix + "缺å°ä¾åºååç§°ï¼è¯·è¡¥å
åå确认"); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private void normalizePurchaseLedgerDateFields(Map<String, Object> target) { |
| | | normalizeDateField(target, "entryDate"); |
| | | normalizeDateField(target, "executionDate"); |
| | | normalizeDateField(target, "createdAt"); |
| | | normalizeDateField(target, "updatedAt"); |
| | | } |
| | | |
| | | private void normalizeDateField(Map<String, Object> target, String fieldName) { |
| | | Object value = target.get(fieldName); |
| | | if (value == null) { |
| | | return; |
| | | } |
| | | String normalizedDate = normalizeDateValue(value); |
| | | if (StringUtils.hasText(normalizedDate)) { |
| | | target.put(fieldName, normalizedDate); |
| | | } |
| | | } |
| | | |
| | | private String normalizeDateValue(Object value) { |
| | | if (value instanceof Date date) { |
| | | return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE); |
| | | } |
| | | if (value instanceof Number number) { |
| | | return LocalDate.of(1899, 12, 30) |
| | | .plusDays(number.longValue()) |
| | | .format(DateTimeFormatter.ISO_LOCAL_DATE); |
| | | } |
| | | |
| | | String text = String.valueOf(value).trim(); |
| | | if (!StringUtils.hasText(text)) { |
| | | return null; |
| | | } |
| | | if (text.length() >= 10 && text.charAt(4) == '-' && text.charAt(7) == '-') { |
| | | return text.substring(0, 10); |
| | | } |
| | | |
| | | String normalizedText = text.replace("å¹´", "-") |
| | | .replace("æ", "-") |
| | | .replace("æ¥", "") |
| | | .replace(".", "-") |
| | | .replace("/", "-") |
| | | .trim(); |
| | | DateTimeFormatter[] formatters = { |
| | | DateTimeFormatter.ofPattern("yyyy-M-d"), |
| | | DateTimeFormatter.ofPattern("M-d-yyyy"), |
| | | DateTimeFormatter.ofPattern("M-d-yy") |
| | | }; |
| | | for (DateTimeFormatter formatter : formatters) { |
| | | try { |
| | | return LocalDate.parse(normalizedText, formatter).format(DateTimeFormatter.ISO_LOCAL_DATE); |
| | | } catch (DateTimeParseException ignored) { |
| | | // Try the next supported input pattern. |
| | | } |
| | | } |
| | | return text; |
| | | } |
| | | |
| | | private void copyPurchaseLedgerDtoFields(Map<String, Object> source, Map<String, Object> target) { |
| | | String[] dtoFields = { |
| | | "entryDateStart", "entryDateEnd", "id", "purchaseContractNumber", |
| | | "supplierId", "supplierName", "isWhite", "recorderId", "recorderName", "salesContractNo", |
| | | "salesContractNoId", "projectName", "entryDate", "executionDate", "remarks", "attachmentMaterials", |
| | | "createdAt", "updatedAt", "salesLedgerId", "hasChildren", "Type", "productData", "tempFileIds", |
| | | "SalesLedgerFiles", "phoneNumber", "businessPersonId", "productId", "productModelId", "invoiceNumber", |
| | | "invoiceAmount", "ticketRegistrationId", "contractAmount", "receiptPaymentAmount", |
| | | "unReceiptPaymentAmount", "type", "paymentMethod", "approvalStatus", "templateName" |
| | | }; |
| | | for (String field : dtoFields) { |
| | | if (source.containsKey(field)) { |
| | | target.put(field, source.get(field)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void putDtoFieldIfPresent(Map<String, Object> source, Map<String, Object> target, String dtoField, String... aliases) { |
| | | if (target.containsKey(dtoField) && target.get(dtoField) != null) { |
| | | return; |
| | | } |
| | | for (String alias : aliases) { |
| | | Object value = source.get(alias); |
| | | if (value != null && StringUtils.hasText(String.valueOf(value))) { |
| | | target.put(dtoField, value); |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | |
| | | private List<Map<String, Object>> toMapList(Object value) { |
| | | if (value == null) { |
| | | return List.of(); |
| | | } |
| | | return objectMapper.convertValue(value, new TypeReference<List<Map<String, Object>>>() { |
| | | }); |
| | | } |
| | | |
| | | private String stringValue(Map<String, Object> map, String... keys) { |
| | | for (String key : keys) { |
| | | Object value = map.get(key); |
| | | if (value != null && StringUtils.hasText(String.valueOf(value))) { |
| | | return String.valueOf(value); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Long longValue(Map<String, Object> map, String... keys) { |
| | | String value = stringValue(map, keys); |
| | | if (!StringUtils.hasText(value)) { |
| | | return null; |
| | | } |
| | | try { |
| | | return Long.parseLong(value.trim()); |
| | | } catch (NumberFormatException ignored) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private BigDecimal decimalValue(Object value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | if (value instanceof BigDecimal decimal) { |
| | | return decimal; |
| | | } |
| | | if (value instanceof Number number) { |
| | | return new BigDecimal(String.valueOf(number)); |
| | | } |
| | | String text = String.valueOf(value) |
| | | .replace(",", "") |
| | | .replace("ï¼", "") |
| | | .replace("å
", "") |
| | | .replace("ï¿¥", "") |
| | | .trim(); |
| | | if (!StringUtils.hasText(text)) { |
| | | return null; |
| | | } |
| | | try { |
| | | return new BigDecimal(text); |
| | | } catch (NumberFormatException ignored) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private String toCustomerMessage(Exception ex) { |
| | | String message = ex.getMessage(); |
| | | if (!StringUtils.hasText(message)) { |
| | | return "å¤ç失败ï¼è¯·æ£æ¥ç¡®è®¤æ°æ®åéè¯"; |
| | | } |
| | | if (message.contains("tax_inclusive_unit_price")) { |
| | | return "å¤ç失败ï¼äº§åæç»ç¼ºå°å«ç¨åä»·ï¼è¯·è¡¥å
åå确认"; |
| | | } |
| | | if (message.contains("tax_inclusive_total_price")) { |
| | | return "å¤ç失败ï¼äº§åæç»ç¼ºå°å«ç¨æ»ä»·ï¼è¯·è¡¥å
åå确认"; |
| | | } |
| | | if (message.contains("entryDate")) { |
| | | return "å¤ç失败ï¼å½å
¥æ¥ææ ¼å¼ä¸æ£ç¡®ï¼è¯·ä½¿ç¨ yyyy-MM-ddï¼ä¾å¦ 2026-04-30"; |
| | | } |
| | | if (message.contains("supplier")) { |
| | | return "å¤ç失败ï¼ä¾åºåä¿¡æ¯ä¸å®æ´ï¼è¯·ç¡®è®¤ä¾åºååç§°æä¾åºåID"; |
| | | } |
| | | if (message.contains("SQL") || message.contains("java.") || message.contains("Exception")) { |
| | | return "å¤ç失败ï¼ç¡®è®¤æ°æ®ä¸å®æ´ææ ¼å¼ä¸æ£ç¡®ï¼è¯·æ£æ¥å¿
å¡«åæ®µåéè¯"; |
| | | } |
| | | return "å¤ç失败ï¼" + message; |
| | | } |
| | | |
| | | private AjaxResult fillSupplierIdByName(PurchaseLedgerDto dto) { |
| | | if (dto.getSupplierId() != null) { |
| | | return null; |
| | | } |
| | | if (!StringUtils.hasText(dto.getSupplierName())) { |
| | | return AjaxResult.error("ä¾åºåIDä¸è½ä¸ºç©ºï¼æªè¯å«å°ä¾åºååç§°ï¼æ æ³èªå¨å¹é
ä¾åºåID"); |
| | | } |
| | | |
| | | SupplierManage supplier = supplierManageMapper.selectOne(new LambdaQueryWrapper<SupplierManage>() |
| | | .eq(SupplierManage::getSupplierName, dto.getSupplierName().trim()) |
| | | .last("limit 1")); |
| | | if (supplier == null) { |
| | | return AjaxResult.error("æªæ¾å°ä¾åºåï¼" + dto.getSupplierName() + "ï¼è¯·å
ç»´æ¤ä¾åºåææå¨éæ©ä¾åºåID"); |
| | | } |
| | | dto.setSupplierId(supplier.getId()); |
| | | return null; |
| | | } |
| | | |
| | | private AjaxResult processPaymentRegistration(Map<String, Object> payload) { |
| | | Object recordsValue = payload.get("records"); |
| | | List<PaymentRegistration> records; |
| | | if (recordsValue == null) { |
| | | records = Collections.singletonList(objectMapper.convertValue(payload, PaymentRegistration.class)); |
| | | } else { |
| | | records = objectMapper.convertValue(recordsValue, new TypeReference<List<PaymentRegistration>>() { |
| | | }); |
| | | } |
| | | int result = paymentRegistrationService.insertPaymentRegistration(records); |
| | | return AjaxResult.success("仿¬¾ç»è®°å·²å¤ç", result); |
| | | } |
| | | |
| | | private AjaxResult processPurchaseReturnOrder(Map<String, Object> payload) { |
| | | PurchaseReturnOrderDto dto = objectMapper.convertValue(payload, PurchaseReturnOrderDto.class); |
| | | Boolean result = purchaseReturnOrdersService.add(dto); |
| | | return AjaxResult.success("éè´éè´§åå·²å¤ç", result); |
| | | } |
| | | } |
| | |
| | | if ("xls".equals(ext)) { |
| | | return extractXls(bytes); |
| | | } |
| | | if (isImage(ext)) { |
| | | return "å¾çæä»¶ï¼" + filename + "ï¼å·²ä¸ä¼ ï¼è¯·ç»åå¾çå
容è¯å«éè´åæ®ãè¡¨æ ¼å产åæç»ã"; |
| | | } |
| | | throw new IllegalArgumentException("æä¸æ¯æè¯¥æä»¶ç±»å: " + ext); |
| | | } |
| | | |
| | | public boolean isImageFile(MultipartFile file) { |
| | | if (file == null) { |
| | | return false; |
| | | } |
| | | return isImage(getExtension(file.getOriginalFilename())); |
| | | } |
| | | |
| | | private String extractDocx(byte[] bytes) throws IOException { |
| | |
| | | "txt", "md", "markdown", "json", "xml", "yaml", "yml", "csv", "log", "properties", |
| | | "java", "js", "ts", "vue", "html", "css", "sql", "py", "go", "sh", "bat"); |
| | | } |
| | | |
| | | private boolean isImage(String ext) { |
| | | return StringUtils.inStringIgnoreCase(ext, "png", "jpg", "jpeg", "webp", "bmp"); |
| | | } |
| | | } |
| | |
| | | this.aiSessionUserContext = aiSessionUserContext; |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢å®¡æ¹å¾
åå表", value = "æ¥è¯¢å½åç»å½äººç¸å
³ç审æ¹å¾
åï¼ä¼å
è¿åèªå·±å¾
å¤çç审æ¹ï¼æ¯ææç¶æãç±»åãå
³é®åè¿æ»¤ã") |
| | | @Tool(name = "æ¥è¯¢å®¡æ¹å¾
åå表", value = "æ¥è¯¢å½åç»å½äººç¸å
³ç审æ¹å¾
åï¼ä¼å
è¿åèªå·±å¾
å¤çç审æ¹ï¼æ¯ææç¶æãç±»åãå
³é®ååèå´è¿æ»¤ã") |
| | | public String listTodos(@ToolMemoryId String memoryId, |
| | | @P(value = "审æ¹ç¶æï¼å¯éå¼ï¼allãpendingãprocessingãapprovedãrejectedãresubmitted", required = false) String status, |
| | | @P(value = "审æ¹ç±»åç¼å·ï¼å¯ä¸ä¼ ", required = false) Integer approveType, |
| | | @P(value = "å
³é®åï¼å¯å¹é
æµç¨ç¼å·ãæ é¢ãç³è¯·äººãå½å审æ¹äºº", required = false) String keyword, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§20", required = false) Integer limit) { |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§20", required = false) Integer limit, |
| | | @P(value = "æ¥è¯¢èå´ï¼å¯éå¼ï¼relatedãapplicantãapproverï¼related 表示å½åç¨æ·ç¸å
³ï¼applicant 表示æåèµ·çï¼approver 表示å¾
æå¤çç", required = false) String scope) { |
| | | |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | Long userId = loginUser.getUserId(); |
| | | Integer statusCode = parseStatus(status); |
| | | String normalizedScope = normalizeScope(scope); |
| | | |
| | | LambdaQueryWrapper<ApproveProcess> wrapper = new LambdaQueryWrapper<>(); |
| | | wrapper.eq(ApproveProcess::getApproveDelete, 0) |
| | | .ne(ApproveProcess::getApproveStatus, 2); |
| | | wrapper.eq(ApproveProcess::getApproveDelete, 0); |
| | | if (statusCode == null) { |
| | | wrapper.ne(ApproveProcess::getApproveStatus, 2); |
| | | } |
| | | |
| | | if (approveType != null) { |
| | | wrapper.eq(ApproveProcess::getApproveType, approveType); |
| | | } |
| | | // if (StringUtils.hasText(keyword)) { |
| | | // wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword) |
| | | // .or().like(ApproveProcess::getApproveReason, keyword) |
| | | // .or().like(ApproveProcess::getApproveUserName, keyword) |
| | | // .or().like(ApproveProcess::getApproveUserCurrentName, keyword)); |
| | | // } |
| | | if (statusCode != null && (statusCode == 0 || statusCode == 1)) { |
| | | if (StringUtils.hasText(keyword)) { |
| | | wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword) |
| | | .or().like(ApproveProcess::getApproveReason, keyword) |
| | | .or().like(ApproveProcess::getApproveUserName, keyword) |
| | | .or().like(ApproveProcess::getApproveUserCurrentName, keyword)); |
| | | } |
| | | if ("applicant".equals(normalizedScope)) { |
| | | wrapper.eq(ApproveProcess::getApproveUser, userId); |
| | | if (statusCode != null) { |
| | | wrapper.eq(ApproveProcess::getApproveStatus, statusCode); |
| | | } |
| | | } else if ("approver".equals(normalizedScope)) { |
| | | wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId); |
| | | if (statusCode != null) { |
| | | wrapper.eq(ApproveProcess::getApproveStatus, statusCode); |
| | | } |
| | | } else if (statusCode != null && (statusCode == 0 || statusCode == 1)) { |
| | | wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId); |
| | | wrapper.eq(ApproveProcess::getApproveStatus, statusCode); |
| | | } else { |
| | | wrapper.and(w -> w.eq(ApproveProcess::getApproveUser, userId) |
| | | .or().eq(ApproveProcess::getApproveUserCurrentId, userId) |
| | | .or().apply("FIND_IN_SET({0}, approve_user_ids)", userId)); |
| | | if (statusCode != null) { |
| | | wrapper.eq(ApproveProcess::getApproveStatus, statusCode); |
| | | } |
| | | } |
| | | |
| | | wrapper.orderByDesc(ApproveProcess::getCreateTime) |
| | |
| | | "count", items.size(), |
| | | "statusFilter", StringUtils.hasText(status) ? status : "all", |
| | | "approveType", approveType == null ? "" : approveType, |
| | | "keyword", keyword == null ? "" : keyword |
| | | "keyword", keyword == null ? "" : keyword, |
| | | "scope", normalizedScope |
| | | ), |
| | | Map.of("columns", todoColumns(), "items", items), |
| | | Map.of()); |
| | |
| | | }; |
| | | } |
| | | |
| | | private String normalizeScope(String scope) { |
| | | if (!StringUtils.hasText(scope)) { |
| | | return "related"; |
| | | } |
| | | return switch (scope.trim().toLowerCase()) { |
| | | case "applicant", "mine", "created", "initiated" -> "applicant"; |
| | | case "approver", "handler", "todo", "pending" -> "approver"; |
| | | default -> "related"; |
| | | }; |
| | | } |
| | | |
| | | private String approveStatusName(Integer status) { |
| | | if (status == null) { |
| | | return "æªç¥"; |
| | |
| | | import com.ruoyi.purchase.pojo.PaymentRegistration; |
| | | import com.ruoyi.purchase.pojo.PurchaseLedger; |
| | | import com.ruoyi.purchase.pojo.PurchaseReturnOrders; |
| | | import com.ruoyi.procurementrecord.mapper.InboundManagementMapper; |
| | | import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper; |
| | | import com.ruoyi.procurementrecord.pojo.InboundManagement; |
| | | import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage; |
| | | import com.ruoyi.sales.mapper.SalesLedgerProductMapper; |
| | | import com.ruoyi.sales.pojo.SalesLedgerProduct; |
| | | import dev.langchain4j.agent.tool.P; |
| | | import dev.langchain4j.agent.tool.Tool; |
| | | import dev.langchain4j.agent.tool.ToolMemoryId; |
| | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Comparator; |
| | | import java.util.stream.Collectors; |
| | | |
| | | @Component |
| | |
| | | private final PaymentRegistrationMapper paymentRegistrationMapper; |
| | | private final InvoicePurchaseMapper invoicePurchaseMapper; |
| | | private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper; |
| | | private final SalesLedgerProductMapper salesLedgerProductMapper; |
| | | private final ProcurementRecordMapper procurementRecordMapper; |
| | | private final InboundManagementMapper inboundManagementMapper; |
| | | private final AiSessionUserContext aiSessionUserContext; |
| | | |
| | | public PurchaseAgentTools(PurchaseLedgerMapper purchaseLedgerMapper, |
| | | PaymentRegistrationMapper paymentRegistrationMapper, |
| | | InvoicePurchaseMapper invoicePurchaseMapper, |
| | | PurchaseReturnOrdersMapper purchaseReturnOrdersMapper, |
| | | SalesLedgerProductMapper salesLedgerProductMapper, |
| | | ProcurementRecordMapper procurementRecordMapper, |
| | | InboundManagementMapper inboundManagementMapper, |
| | | AiSessionUserContext aiSessionUserContext) { |
| | | this.purchaseLedgerMapper = purchaseLedgerMapper; |
| | | this.paymentRegistrationMapper = paymentRegistrationMapper; |
| | | this.invoicePurchaseMapper = invoicePurchaseMapper; |
| | | this.purchaseReturnOrdersMapper = purchaseReturnOrdersMapper; |
| | | this.salesLedgerProductMapper = salesLedgerProductMapper; |
| | | this.procurementRecordMapper = procurementRecordMapper; |
| | | this.inboundManagementMapper = inboundManagementMapper; |
| | | this.aiSessionUserContext = aiSessionUserContext; |
| | | } |
| | | |
| | |
| | | wrapper.ge(PurchaseLedger::getEntryDate, toDate(start)); |
| | | } |
| | | if (end != null) { |
| | | wrapper.le(PurchaseLedger::getEntryDate, toDate(end)); |
| | | wrapper.lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(end)); |
| | | } |
| | | wrapper.orderByDesc(PurchaseLedger::getEntryDate, PurchaseLedger::getId).last("limit " + finalLimit); |
| | | |
| | |
| | | return jsonResponse(true, "purchase_stats", "å·²è¿åéè´ç»è®¡æ°æ®", summary, Map.of(), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "éè´ç©æé颿è¡", value = "ææ¶é´èå´ç»è®¡éè´ç©æé颿è¡ï¼å¯åçæ¬æéè´é颿åé åçç©æã") |
| | | public String rankPurchaseMaterials(@ToolMemoryId String memoryId, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "æ¶é´èå´æè¿°ï¼ä¾å¦æ¬æãè¿7天ãè¿30天", required = false) String timeRange, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, timeRange); |
| | | List<Long> ledgerIds = queryLedgers(loginUser, range).stream() |
| | | .map(PurchaseLedger::getId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList()); |
| | | if (ledgerIds.isEmpty()) { |
| | | return jsonResponse(true, "purchase_material_rank", "å½åæ¶é´èå´å
没æéè´ç©ææ°æ®ã", |
| | | rangeSummary(range, 0), Map.of("items", List.of()), Map.of()); |
| | | } |
| | | |
| | | List<SalesLedgerProduct> products = defaultList(salesLedgerProductMapper.selectList(new LambdaQueryWrapper<SalesLedgerProduct>() |
| | | .eq(SalesLedgerProduct::getType, 2) |
| | | .in(SalesLedgerProduct::getSalesLedgerId, ledgerIds))); |
| | | |
| | | Map<String, MaterialRankItem> grouped = new LinkedHashMap<>(); |
| | | for (SalesLedgerProduct product : products) { |
| | | String name = safe(product.getProductCategory()); |
| | | String model = safe(product.getSpecificationModel()); |
| | | String key = name + "|" + model; |
| | | MaterialRankItem item = grouped.computeIfAbsent(key, ignored -> new MaterialRankItem(name, model, safe(product.getUnit()))); |
| | | item.quantity = item.quantity.add(defaultDecimal(product.getQuantity())); |
| | | item.amount = item.amount.add(defaultDecimal(product.getTaxInclusiveTotalPrice())); |
| | | } |
| | | |
| | | List<Map<String, Object>> items = grouped.values().stream() |
| | | .sorted(Comparator.comparing((MaterialRankItem item) -> item.amount).reversed()) |
| | | .limit(normalizeLimit(limit)) |
| | | .map(MaterialRankItem::toMap) |
| | | .collect(Collectors.toList()); |
| | | |
| | | return jsonResponse(true, "purchase_material_rank", "å·²è¿åéè´ç©æé颿è¡ã", |
| | | rangeSummary(range, items.size()), Map.of("items", items), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢æªå
¥åºéè´è®¢å", value = "æ¥è¯¢éè´è®¢åä¸ä»æå¾
å
¥åºæ°éçç©ææç»ã") |
| | | public String listUnstockedPurchaseOrders(@ToolMemoryId String memoryId, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "å
³é®åï¼å¯å¹é
éè´ååå·/ä¾åºå/ç©æ", required = false) String keyword, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, null); |
| | | List<PurchaseLedger> ledgers = queryLedgers(loginUser, range).stream() |
| | | .filter(ledger -> matchLedgerKeyword(ledger, keyword)) |
| | | .collect(Collectors.toList()); |
| | | Map<Long, PurchaseLedger> ledgerMap = ledgers.stream() |
| | | .filter(ledger -> ledger.getId() != null) |
| | | .collect(Collectors.toMap(PurchaseLedger::getId, ledger -> ledger, (a, b) -> a, LinkedHashMap::new)); |
| | | if (ledgerMap.isEmpty()) { |
| | | return jsonResponse(true, "purchase_unstocked_list", "æªæ¥è¯¢å°ç¬¦åæ¡ä»¶çéè´è®¢åã", |
| | | rangeSummary(range, 0), Map.of("items", List.of()), Map.of()); |
| | | } |
| | | |
| | | List<SalesLedgerProduct> products = defaultList(salesLedgerProductMapper.selectList(new LambdaQueryWrapper<SalesLedgerProduct>() |
| | | .eq(SalesLedgerProduct::getType, 2) |
| | | .in(SalesLedgerProduct::getSalesLedgerId, ledgerMap.keySet()))); |
| | | |
| | | List<Map<String, Object>> items = products.stream() |
| | | .filter(product -> matchProductKeyword(product, keyword)) |
| | | .map(product -> toUnstockedItem(product, ledgerMap.get(product.getSalesLedgerId()))) |
| | | .filter(Objects::nonNull) |
| | | .limit(normalizeLimit(limit)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | return jsonResponse(true, "purchase_unstocked_list", "å·²è¿åæªå
¥åºéè´è®¢åã", |
| | | rangeSummary(range, items.size()), Map.of("items", items), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢éè´å°è´§å¼å¸¸", value = "æ¥è¯¢å°è´§ç¶æå¼å¸¸æå¤æ³¨å
å«å¼å¸¸ä¿¡æ¯çå°è´§è®°å½ã") |
| | | public String listArrivalExceptions(@ToolMemoryId String memoryId, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "æ¶é´èå´æè¿°ï¼ä¾å¦è¿7å¤©ãæ¬æ", required = false) String timeRange, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, timeRange); |
| | | LambdaQueryWrapper<InboundManagement> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), InboundManagement::getTenantId); |
| | | wrapper.ge(InboundManagement::getArrivalTime, toDate(range.start())) |
| | | .lt(InboundManagement::getArrivalTime, toExclusiveEndDate(range.end())) |
| | | .and(w -> w.notLike(InboundManagement::getStatus, "æ£å¸¸") |
| | | .notLike(InboundManagement::getStatus, "宿") |
| | | .notLike(InboundManagement::getStatus, "å·²å°è´§") |
| | | .or().like(InboundManagement::getStatus, "å¼å¸¸") |
| | | .or().like(InboundManagement::getRemark, "å¼å¸¸") |
| | | .or().like(InboundManagement::getRemark, "é®é¢") |
| | | .or().like(InboundManagement::getRemark, "å»¶è¿") |
| | | .or().like(InboundManagement::getRemark, "ç缺")); |
| | | wrapper.orderByDesc(InboundManagement::getArrivalTime).last("limit " + normalizeLimit(limit)); |
| | | |
| | | List<Map<String, Object>> items = defaultList(inboundManagementMapper.selectList(wrapper)).stream() |
| | | .map(this::toArrivalItem) |
| | | .collect(Collectors.toList()); |
| | | return jsonResponse(true, "purchase_arrival_exception_list", "å·²è¿åéè´å°è´§å¼å¸¸è®°å½ã", |
| | | rangeSummary(range, items.size()), Map.of("items", items), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢å¾
仿¬¾éè´å", value = "æ¥è¯¢ååéé¢å¤§äºå·²ä»æ¬¾éé¢çéè´åã") |
| | | public String listPendingPaymentOrders(@ToolMemoryId String memoryId, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "å
³é®åï¼å¯å¹é
éè´ååå·/ä¾åºå/项ç®å", required = false) String keyword, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, null); |
| | | List<Map<String, Object>> items = queryLedgers(loginUser, range).stream() |
| | | .filter(ledger -> matchLedgerKeyword(ledger, keyword)) |
| | | .map(ledger -> toPendingPaymentItem(loginUser, ledger)) |
| | | .filter(Objects::nonNull) |
| | | .sorted(Comparator.comparing(item -> (BigDecimal) item.get("pendingAmount"), Comparator.reverseOrder())) |
| | | .limit(normalizeLimit(limit)) |
| | | .collect(Collectors.toList()); |
| | | return jsonResponse(true, "purchase_pending_payment_list", "å·²è¿åå¾
仿¬¾éè´åã", |
| | | rangeSummary(range, items.size()), Map.of("items", items), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢éè´éè´§æ
åµ", value = "ææ¶é´èå´æ¥è¯¢éè´éè´§åå表åéè´§éé¢ã") |
| | | public String listPurchaseReturns(@ToolMemoryId String memoryId, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "å
³é®åï¼å¯å¹é
éè´§åå·/夿³¨", required = false) String keyword, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, null); |
| | | LambdaQueryWrapper<PurchaseReturnOrders> wrapper = new LambdaQueryWrapper<>(); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), PurchaseReturnOrders::getDeptId); |
| | | wrapper.ge(PurchaseReturnOrders::getPreparedAt, range.start()) |
| | | .le(PurchaseReturnOrders::getPreparedAt, range.end()); |
| | | if (StringUtils.hasText(keyword)) { |
| | | wrapper.and(w -> w.like(PurchaseReturnOrders::getNo, keyword) |
| | | .or().like(PurchaseReturnOrders::getRemark, keyword) |
| | | .or().like(PurchaseReturnOrders::getReturnUserName, keyword)); |
| | | } |
| | | wrapper.orderByDesc(PurchaseReturnOrders::getPreparedAt).last("limit " + normalizeLimit(limit)); |
| | | |
| | | List<PurchaseReturnOrders> returns = defaultList(purchaseReturnOrdersMapper.selectList(wrapper)); |
| | | BigDecimal totalAmount = returns.stream() |
| | | .map(PurchaseReturnOrders::getTotalAmount) |
| | | .filter(Objects::nonNull) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | Map<String, Object> summary = rangeSummary(range, returns.size()); |
| | | summary.put("returnAmount", totalAmount); |
| | | |
| | | return jsonResponse(true, "purchase_return_list", "å·²è¿åéè´éè´§æ
åµã", |
| | | summary, |
| | | Map.of("items", returns.stream().map(this::toReturnItem).collect(Collectors.toList())), |
| | | Map.of()); |
| | | } |
| | | |
| | | private List<PurchaseLedger> queryLedgers(LoginUser loginUser, DateRange range) { |
| | | LambdaQueryWrapper<PurchaseLedger> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), PurchaseLedger::getTenantId); |
| | | wrapper.ge(PurchaseLedger::getEntryDate, toDate(range.start())) |
| | | .le(PurchaseLedger::getEntryDate, toDate(range.end())); |
| | | .lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(range.end())); |
| | | return defaultList(purchaseLedgerMapper.selectList(wrapper)); |
| | | } |
| | | |
| | | private Map<String, Object> rangeSummary(DateRange range, int count) { |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("timeRange", range.label()); |
| | | summary.put("startDate", range.start().toString()); |
| | | summary.put("endDate", range.end().toString()); |
| | | summary.put("count", count); |
| | | return summary; |
| | | } |
| | | |
| | | private boolean matchLedgerKeyword(PurchaseLedger ledger, String keyword) { |
| | | if (!StringUtils.hasText(keyword)) { |
| | | return true; |
| | | } |
| | | String text = keyword.trim(); |
| | | return safe(ledger.getPurchaseContractNumber()).contains(text) |
| | | || safe(ledger.getSupplierName()).contains(text) |
| | | || safe(ledger.getProjectName()).contains(text); |
| | | } |
| | | |
| | | private boolean matchProductKeyword(SalesLedgerProduct product, String keyword) { |
| | | if (!StringUtils.hasText(keyword)) { |
| | | return true; |
| | | } |
| | | String text = keyword.trim(); |
| | | return safe(product.getProductCategory()).contains(text) |
| | | || safe(product.getSpecificationModel()).contains(text); |
| | | } |
| | | |
| | | private Map<String, Object> toUnstockedItem(SalesLedgerProduct product, PurchaseLedger ledger) { |
| | | if (product == null || ledger == null || product.getId() == null) { |
| | | return null; |
| | | } |
| | | BigDecimal orderedQuantity = defaultDecimal(product.getQuantity()); |
| | | BigDecimal inboundQuantity = sumInboundQuantity(product.getId()); |
| | | BigDecimal pendingQuantity = orderedQuantity.subtract(inboundQuantity); |
| | | if (pendingQuantity.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return null; |
| | | } |
| | | Map<String, Object> item = new LinkedHashMap<>(); |
| | | item.put("purchaseLedgerId", ledger.getId()); |
| | | item.put("purchaseContractNumber", safe(ledger.getPurchaseContractNumber())); |
| | | item.put("supplierName", safe(ledger.getSupplierName())); |
| | | item.put("productCategory", safe(product.getProductCategory())); |
| | | item.put("specificationModel", safe(product.getSpecificationModel())); |
| | | item.put("unit", safe(product.getUnit())); |
| | | item.put("orderedQuantity", orderedQuantity); |
| | | item.put("inboundQuantity", inboundQuantity); |
| | | item.put("pendingInboundQuantity", pendingQuantity); |
| | | item.put("entryDate", formatDate(ledger.getEntryDate())); |
| | | return item; |
| | | } |
| | | |
| | | private BigDecimal sumInboundQuantity(Long salesLedgerProductId) { |
| | | List<ProcurementRecordStorage> records = defaultList(procurementRecordMapper.selectList(new LambdaQueryWrapper<ProcurementRecordStorage>() |
| | | .eq(ProcurementRecordStorage::getType, 1) |
| | | .eq(ProcurementRecordStorage::getSalesLedgerProductId, salesLedgerProductId))); |
| | | return records.stream() |
| | | .map(ProcurementRecordStorage::getInboundNum) |
| | | .filter(Objects::nonNull) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | } |
| | | |
| | | private Map<String, Object> toArrivalItem(InboundManagement item) { |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("id", item.getId()); |
| | | map.put("orderNo", safe(item.getOrderNo())); |
| | | map.put("arrivalNo", safe(item.getArrivalNo())); |
| | | map.put("supplierName", safe(item.getSupplierName())); |
| | | map.put("status", safe(item.getStatus())); |
| | | map.put("arrivalTime", formatDate(item.getArrivalTime())); |
| | | map.put("arrivalQuantity", safe(item.getArrivalQuantity())); |
| | | map.put("remark", safe(item.getRemark())); |
| | | return map; |
| | | } |
| | | |
| | | private Map<String, Object> toPendingPaymentItem(LoginUser loginUser, PurchaseLedger ledger) { |
| | | BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount()); |
| | | BigDecimal paidAmount = sumPaymentAmount(loginUser, ledger.getId()); |
| | | BigDecimal pendingAmount = contractAmount.subtract(paidAmount); |
| | | if (pendingAmount.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return null; |
| | | } |
| | | Map<String, Object> item = toLedgerItem(ledger); |
| | | item.put("paidAmount", paidAmount); |
| | | item.put("pendingAmount", pendingAmount); |
| | | return item; |
| | | } |
| | | |
| | | private BigDecimal sumPaymentAmount(LoginUser loginUser, Long purchaseLedgerId) { |
| | | LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId); |
| | | wrapper.eq(PaymentRegistration::getPurchaseLedgerId, purchaseLedgerId); |
| | | return defaultList(paymentRegistrationMapper.selectList(wrapper)).stream() |
| | | .map(PaymentRegistration::getCurrentPaymentAmount) |
| | | .filter(Objects::nonNull) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | } |
| | | |
| | | private Map<String, Object> toReturnItem(PurchaseReturnOrders item) { |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("id", item.getId()); |
| | | map.put("no", safe(item.getNo())); |
| | | map.put("returnType", item.getReturnType()); |
| | | map.put("purchaseLedgerId", item.getPurchaseLedgerId()); |
| | | map.put("preparedAt", item.getPreparedAt() == null ? "" : item.getPreparedAt().toString()); |
| | | map.put("returnUserName", safe(item.getReturnUserName())); |
| | | map.put("totalAmount", item.getTotalAmount()); |
| | | map.put("remark", safe(item.getRemark())); |
| | | return map; |
| | | } |
| | | |
| | | private BigDecimal defaultDecimal(BigDecimal value) { |
| | | return value == null ? BigDecimal.ZERO : value; |
| | | } |
| | | |
| | | private List<PaymentRegistration> queryPayments(LoginUser loginUser, DateRange range) { |
| | | LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId); |
| | | wrapper.ge(PaymentRegistration::getPaymentDate, toDate(range.start())) |
| | | .le(PaymentRegistration::getPaymentDate, toDate(range.end())); |
| | | .lt(PaymentRegistration::getPaymentDate, toExclusiveEndDate(range.end())); |
| | | return defaultList(paymentRegistrationMapper.selectList(wrapper)); |
| | | } |
| | | |
| | |
| | | if (text.contains("è¿å个æ") || text.contains("æè¿å个æ") || text.contains("å个æ")) { |
| | | return new DateRange(today.minusDays(14), today, "è¿å个æ"); |
| | | } |
| | | java.util.regex.Matcher relativeMatcher = java.util.regex.Pattern.compile("(è¿|æè¿)(\\d+)(天|å¨|个æ|æ|å¹´)").matcher(text); |
| | | if (relativeMatcher.find()) { |
| | | int amount = Integer.parseInt(relativeMatcher.group(2)); |
| | | String unit = relativeMatcher.group(3); |
| | | LocalDate relativeStart = switch (unit) { |
| | | case "天" -> today.minusDays(Math.max(amount - 1L, 0)); |
| | | case "å¨" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1); |
| | | case "个æ", "æ" -> today.minusMonths(Math.max(amount, 1)).plusDays(1); |
| | | case "å¹´" -> today.minusYears(Math.max(amount, 1)).plusDays(1); |
| | | default -> today.minusDays(29); |
| | | }; |
| | | return new DateRange(relativeStart, today, "è¿" + amount + unit); |
| | | } |
| | | return new DateRange(today.minusDays(29), today, "è¿30天"); |
| | | } |
| | | |
| | |
| | | |
| | | private Date toDate(LocalDate localDate) { |
| | | return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); |
| | | } |
| | | |
| | | private Date toExclusiveEndDate(LocalDate localDate) { |
| | | return Date.from(localDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant()); |
| | | } |
| | | |
| | | private String formatDate(Date date) { |
| | |
| | | |
| | | private record DateRange(LocalDate start, LocalDate end, String label) { |
| | | } |
| | | |
| | | private static class MaterialRankItem { |
| | | private final String productCategory; |
| | | private final String specificationModel; |
| | | private final String unit; |
| | | private BigDecimal quantity = BigDecimal.ZERO; |
| | | private BigDecimal amount = BigDecimal.ZERO; |
| | | |
| | | private MaterialRankItem(String productCategory, String specificationModel, String unit) { |
| | | this.productCategory = productCategory; |
| | | this.specificationModel = specificationModel; |
| | | this.unit = unit; |
| | | } |
| | | |
| | | private Map<String, Object> toMap() { |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("productCategory", productCategory); |
| | | map.put("specificationModel", specificationModel); |
| | | map.put("unit", unit); |
| | | map.put("quantity", quantity); |
| | | map.put("amount", amount); |
| | | return map; |
| | | } |
| | | } |
| | | } |
| | |
| | | package com.ruoyi.approve.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | |
| | | List<ApproveProcessConfigNodeVo> list = approveProcessConfigNodeService.listNode( approveProcessVO.getApproveType()); |
| | | List<Long> nodeIds = list.stream() |
| | | .map(ApproveProcessConfigNodeVo::getApproverId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList()); |
| | | if (CollectionUtils.isEmpty(nodeIds)) { |
| | | autoPassPurchaseApproveIfNoApprover(approveProcessVO); |
| | | return; |
| | | } |
| | | List<SysUser> sysUsers = sysUserMapper.selectUserByIds(nodeIds); |
| | | if (CollectionUtils.isEmpty(sysUsers)) throw new RuntimeException("å®¡æ ¸ç¨æ·ä¸åå¨"); |
| | | if (sysDept == null) throw new RuntimeException("é¨é¨ä¸åå¨"); |
| | |
| | | } |
| | | } |
| | | |
| | | private void autoPassPurchaseApproveIfNoApprover(ApproveProcessVO approveProcessVO) { |
| | | if (!Objects.equals(approveProcessVO.getApproveType(), 5) |
| | | || !StringUtils.hasText(approveProcessVO.getApproveReason())) { |
| | | throw new RuntimeException("å®¡æ ¸ç¨æ·ä¸åå¨"); |
| | | } |
| | | purchaseLedgerMapper.update(null, new LambdaUpdateWrapper<PurchaseLedger>() |
| | | .eq(PurchaseLedger::getPurchaseContractNumber, approveProcessVO.getApproveReason()) |
| | | .set(PurchaseLedger::getApprovalStatus, 3)); |
| | | } |
| | | |
| | | @Override |
| | | public List<SysDept> selectDeptListByDeptIds(Long[] deptIds) { |
| | | List<SysDept> sysDeptList = new ArrayList<SysDept>(); |
| | |
| | | } |
| | | purchaseLedgerMapper.updateById(purchaseLedger); |
| | | } |
| | | // 6.éè´å®¡æ ¸æ°å¢ |
| | | // 6.éè´å®¡æ ¸æ°å¢ï¼å®¡æ¹ç®¡çæªé
ç½®éè´å®¡æ¹äººæ¶ï¼å®¡æ¹æå¡ä¼èªå¨ç½®ä¸ºå®¡æ¹éè¿ã |
| | | addApproveByPurchase(loginUser, purchaseLedger); |
| | | |
| | | // 4. å¤çåè¡¨æ°æ® |
| | |
| | | if (products == null || products.isEmpty()) { |
| | | throw new BaseException("产åä¿¡æ¯ä¸åå¨"); |
| | | } |
| | | Integer ledgerType = type == null ? 2 : type; |
| | | |
| | | // æåæ¶éææéè¦æ¥è¯¢çID |
| | | Set<Long> productIds = products.stream() |
| | |
| | | // æ§è¡æ´æ°æä½ |
| | | if (!updateList.isEmpty()) { |
| | | for (SalesLedgerProduct product : updateList) { |
| | | product.setType(type); |
| | | product.setType(ledgerType); |
| | | salesLedgerProductMapper.updateById(product); |
| | | } |
| | | } |
| | | // æ§è¡æå
¥æä½ |
| | | if (!insertList.isEmpty()) { |
| | | for (SalesLedgerProduct salesLedgerProduct : insertList) { |
| | | salesLedgerProduct.setType(type); |
| | | salesLedgerProduct.setType(ledgerType); |
| | | Date entryDate = purchaseLedger.getEntryDate(); |
| | | |
| | | LocalDateTime localDateTime = entryDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); |
| | |
| | | if(salesLedger1 != null){ |
| | | salesLedger.setSalesLedgerId(salesLedger1.getId()); |
| | | } |
| | | // éè´å®¡æ ¸ |
| | | // éè¿æµç§°è·åç¨æ·ID |
| | | String[] split = salesLedger.getApproveUserIds().split("ï¼"); |
| | | List<Long> ids = new ArrayList<>(); |
| | | for (int i = 0; i < split.length; i++) { |
| | | SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getNickName, split[i]) |
| | | .last("LIMIT 1")); |
| | | if (sysUser != null) { |
| | | ids.add(sysUser.getUserId()); |
| | | if (StringUtils.hasText(salesLedger.getApproveUserIds())) { |
| | | // éè´å®¡æ ¸ï¼åå²å¯¼å
¥æ¨¡æ¿ä¼ 审æ¹äººå§åæ¶ï¼ç»§ç»å
¼å®¹è½¬æ¢ä¸ºç¨æ·IDã |
| | | String[] split = salesLedger.getApproveUserIds().split("ï¼"); |
| | | List<Long> ids = new ArrayList<>(); |
| | | for (int i = 0; i < split.length; i++) { |
| | | SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getNickName, split[i]) |
| | | .last("LIMIT 1")); |
| | | if (sysUser != null) { |
| | | ids.add(sysUser.getUserId()); |
| | | } |
| | | } |
| | | // å°éå转为å符串ï¼éå¼ |
| | | String collect = ids.stream().map(Object::toString).collect(Collectors.joining(",")); |
| | | salesLedger.setApproveUserIds(collect); |
| | | } |
| | | // å°éå转为å符串ï¼éå¼ |
| | | String collect = ids.stream().map(Object::toString).collect(Collectors.joining(",")); |
| | | salesLedger.setApproveUserIds(collect); |
| | | purchaseLedgerMapper.insert(salesLedger); |
| | | |
| | | for (PurchaseLedgerProductImportDto salesLedgerProductImportDto : salesLedgerProductImportDtos) { |
| | |
| | | } |
| | | |
| | | public void addApproveByPurchase(LoginUser loginUser,PurchaseLedger purchaseLedger) throws Exception { |
| | | if (loginUser == null) { |
| | | return; |
| | | } |
| | | ApproveProcessVO approveProcessVO = new ApproveProcessVO(); |
| | | approveProcessVO.setApproveType(5); |
| | | approveProcessVO.setApproveDeptId(loginUser.getCurrentDeptId()); |
| | |
| | | ä½ æ¯ä¸ä¸ªå®¡æ¹å¾
å婿ï¼è´è´£å®¡æ¹å¾
åçæ¥è¯¢ãå®¡æ ¸ãåæ¶å®¡æ ¸ãä¿®æ¹ãå é¤åç»è®¡åæã |
| | | ä½ æ¯ä¸ä¸ªå®¡æ¹å¾
å婿ï¼è´è´£åååå
¬å®¡æ¹å¾
åçæ¥è¯¢ãå®¡æ ¸ãåæ¶å®¡æ ¸ãä¿®æ¹ãå é¤åç»è®¡åæã |
| | | |
| | | å·¥ä½è¦æ±ï¼ |
| | | 1. ç¨æ·é®å¾
åå表ã审æ¹è¿åº¦ã审æ¹è¯¦æ
ãç»è®¡æ°æ®æ¶ï¼ä¼å
è°ç¨å·¥å
·ï¼ä¸è¦èé æ°æ®ã |
| | |
| | | 3. å®¡æ ¸å¨ä½éï¼`approve` 表示éè¿ï¼`reject` 表示驳åã |
| | | 4. ä¿®æ¹å®¡æ¹åæ¶ï¼å¦æç¨æ·æ²¡ææç¡®è¦ä¿®æ¹åªäºå段ï¼è¦å
追é®ç¼ºå¤±å段ï¼ä¸è¦çã |
| | | 5. å é¤ãå®¡æ ¸ãåæ¶å®¡æ ¸è¿ç±»å¨ä½å±äºç¶æåæ´ï¼æ§è¡åè¦æç¡®åé¦ç»æã |
| | | 6. é¤âæ¥è¯¢å®¡æ¹å¾
å详æ
âå¤ï¼å
¶ä»å·¥å
·é»è®¤è¿å JSONã |
| | | 7. 对äºè¿äº JSON å·¥å
·ï¼ä½ å¿
é¡»ç´æ¥è¾åºåå§ JSON å符串æ¬èº«ï¼ä¸è¦æ¹åï¼ä¸è¦é¢å¤è§£éï¼ä¸è¦å
裹 Markdown 代ç åï¼ä¸è¦å¨ JSON ååå 任使åã |
| | | 8. åªæâæ¥è¯¢å®¡æ¹å¾
å详æ
âè¿ä¸ªå·¥å
·å
许è¾åºèªç¶è¯è¨ææ¬ã |
| | | 9. 妿工å
·è¿åçæ¯ç»è®¡ JSONï¼ä¹åæ ·ç´æ¥è¾åºåå§ JSONï¼å
¶ä¸ `description`ã`summary`ã`charts` å·²ç»ä¾å端使ç¨ã |
| | | 10. åç使ç¨ä¸æï¼ä½å¨ JSON åºæ¯ä¸ï¼æç»è¾åºå¿
é¡»æ¯åæ³ JSON æ¬ä½ã |
| | | 6. ç¨æ·è¯´âåæ®ââæµç¨ââå®¡æ¹æ¹ââå¾
åâï¼é½æå®¡æ¹å¾
åçè§£ï¼ç¨æ·è¯´âå¡å¨åªä¸ªèç¹ââå½å审æ¹äººââæµè½¬è®°å½âï¼è°ç¨âæ¥è¯¢å®¡æ¹æµè½¬è®°å½âã |
| | | 7. ç¨æ·è¯´âæåèµ·çââææäº¤çââæç³è¯·çâï¼æ¥è¯¢èå´ä½¿ç¨ `applicant`ï¼ç¨æ·è¯´âå¾
æå®¡æ¹ââå½åå¾
æå¤çââéè¦æå¤çâï¼æ¥è¯¢èå´ä½¿ç¨ `approver`ï¼æ²¡ææç¡®èå´æ¶ä½¿ç¨ `related`ã |
| | | 8. ç¨æ·è¯´âå¤çä¸ââåçä¸âï¼ç¶æä½¿ç¨ `processing`ï¼è¯´âå¾
审æ¹ââå¾
å®¡æ ¸âï¼ç¶æä½¿ç¨ `pending`ï¼è¯´âéè¿ââå·²éè¿âï¼ç¶æä½¿ç¨ `approved`ï¼è¯´â驳åââæç»ââæªéè¿âï¼ç¶æä½¿ç¨ `rejected`ã |
| | | 9. ç¨æ·è¦æ±âè¿7天ââæ¬æââè¿30天ââåç±»ååå¸ââéè¿/驳å/å¤çä¸åæå¤å°âçç»è®¡å£å¾æ¶ï¼è°ç¨ç»è®¡å·¥å
·ã |
| | | 10. ç¨æ·è¯´â夿³¨åæââ夿³¨è¯·æ±è¡¥å
说æâæ¶ï¼æå¤æ³¨å
å®¹ä¼ ç»å®¡æ ¸å·¥å
·ç remarkï¼é©³åæ¶å¦ææ²¡æâåå â使â夿³¨âï¼ä¹ä½¿ç¨å¤æ³¨ã |
| | | 11. é¤âæ¥è¯¢å®¡æ¹å¾
å详æ
âå¤ï¼å
¶ä»å·¥å
·é»è®¤è¿å JSONã |
| | | 12. 对äºè¿äº JSON å·¥å
·ï¼ä½ å¿
é¡»ç´æ¥è¾åºåå§ JSON å符串æ¬èº«ï¼ä¸è¦æ¹åï¼ä¸è¦é¢å¤è§£éï¼ä¸è¦å
裹 Markdown 代ç åï¼ä¸è¦å¨ JSON ååå 任使åã |
| | | 13. åªæâæ¥è¯¢å®¡æ¹å¾
å详æ
âè¿ä¸ªå·¥å
·å
许è¾åºèªç¶è¯è¨ææ¬ã |
| | | 14. 妿工å
·è¿åçæ¯ç»è®¡ JSONï¼ä¹åæ ·ç´æ¥è¾åºåå§ JSONï¼å
¶ä¸ `description`ã`summary`ã`charts` å·²ç»ä¾å端使ç¨ã |
| | | 15. åç使ç¨ä¸æï¼ä½å¨ JSON åºæ¯ä¸ï¼æç»è¾åºå¿
é¡»æ¯åæ³ JSON æ¬ä½ã |
| | |
| | | å·¥ä½è§åï¼ |
| | | 1. ä¼å
è°ç¨å·¥å
·å½æ°è·åéè´å°è´¦ã仿¬¾ãå票ãéè´§çç»æåæ°æ®ã |
| | | 2. éå°âç»è®¡/åæ/æ¥è¡¨/ä»å¹´/æ¬æ/è¿XX天âçéæ±ï¼ä¼å
ç»åºç»è®¡ç»æåå
³é®ç»è®ºã |
| | | 3. æ æ³ç´æ¥å¾åºç»è®ºæ¶ï¼æç¡®è¯´æç¼ºå°åªäºå段æç鿡件ã |
| | | 4. ç»æç¨ç®æ´ä¸æåçï¼å
ç»ç»è®ºï¼åç»å
³é®æ°æ®ç¹ã |
| | | 5. ä¸è¦ç¼é éè´æ°æ®ï¼ææç»è®ºå¿
é¡»åºäºå·¥å
·è¿åã |
| | | 3. ç¨æ·é®âæ¬æéè´é颿åé åçç©æââéè´é颿è¡ââç©ææè¡âæ¶ï¼è°ç¨âéè´ç©æé颿è¡âã |
| | | 4. ç¨æ·é®âåªäºéè´è®¢åè¿æªå
¥åºââæªå
¥åºéè´åââå¾
å
¥åºè®¢åâæ¶ï¼è°ç¨âæ¥è¯¢æªå
¥åºéè´è®¢åâã |
| | | 5. ç¨æ·é®âæè¿7天ä¾åºåå°è´§å¼å¸¸ââå°è´§é®é¢ââå°è´§å¼å¸¸âæ¶ï¼è°ç¨âæ¥è¯¢éè´å°è´§å¼å¸¸âã |
| | | 6. ç¨æ·é®âå¾
仿¬¾éè´åââæªä»æ¬¾éè´åââæªä»æ¸
éè´è®¢åâæ¶ï¼è°ç¨âæ¥è¯¢å¾
仿¬¾éè´åâã |
| | | 7. ç¨æ·é®âæ¬æéè´éè´§æ
åµââéè´éè´§å表ââéæ/ææ¶æ
åµâæ¶ï¼è°ç¨âæ¥è¯¢éè´éè´§æ
åµâã |
| | | 8. ç»æç¨ç®æ´ä¸æåçï¼å
ç»ç»è®ºï¼åç»å
³é®æ°æ®ç¹ã |
| | | 9. ä¸è¦ç¼é éè´æ°æ®ï¼ææç»è®ºå¿
é¡»åºäºå·¥å
·è¿åã |
| | | 10. æ æ³ç´æ¥å¾åºç»è®ºæ¶ï¼æç¡®è¯´æç¼ºå°åªäºå段æç鿡件ã |