.playwright-mcp/ÎïÁϵ¼ÈëÄ£°å.xlsBinary files differ
docs/project-business/data/business.json
@@ -21,8 +21,8 @@ "frontend": "mom-pro2-beforeï¼Vue3 管çåå°ï¼åä»åºåæ¾ï¼åªè¯»åèï¼" }, "stats": { "businessModules": 12, "coreObjects": 46, "businessModules": 13, "coreObjects": 48, "documentFlows": 6, "approvalListeners": 13, "aiCapabilities": 8, docs/project-business/data/flows.json
@@ -29,13 +29,15 @@ {"stage": "èå¨", "action": "审æ¹éè¿æå·¥åºæ è®°: åå²âèªå¨ç©ææ¶èæ£çº¿è¾¹åº(å 许è´åºå)ï¼è´¨æ£â产åºå¾ æ£è¡ç IPQCï¼å ³é®éæ£âç´æ¥äº§åºå ¥åº", "roles": ["ç³»ç»èªå¨"]}, {"stage": "å®å·¥", "action": "ä» æ«éå·¥åºååå·¥åå·²ç产æ°éï¼è¾¾æ èªå¨å®æï¼MPS ç´¯è®¡å¤æå®æå¹¶åäºä»¶ç»éå®è®¢å", "roles": ["ç³»ç»èªå¨"]}]}, {"id": "quality", "name": "è´¨éæµç¨", "mermaid": "mermaid/05-quality-flow.mmd", "color": "#e06c6c", "summary": "å鿣éªé¸å£(IQC/IPQC/OQC/RQCï¼äºæ 0â4) + å¤å®åæ(åæ ¼/ç¹é/éè´§/æ¥åº) + NCR è¯å®¡å¤ç½®å ³éï¼AI æä¾å¤å®å»ºè®®ï¼NCR å½åä» å°è´¦ä¸è§¦åè¿å·¥åã", "summary": "å鿣éªé¸å£(IQC/IPQC/OQC/RQCï¼äºæ 0â4) + å¤å®åæ(åæ ¼/ç¹é/éè´§/æ¥åº) + NCR è¯å®¡å¤ç½®å ³éï¼AI æä¾å¤å®å»ºè®®ï¼NCR å½åä» å°è´¦ä¸è§¦åè¿å·¥åï¼æ£éªåå¯ç±æºè½è´¨æ£æ¥åå¹³å°ä¸é®åºä»¶ï¼generate-from-qcï¼æ°æ®ä» MES åï¼äºéæ ¡éªå渲æè½åºï¼ã", "steps": [ {"stage": "æ¥æ", "action": "å°è´§éç¥/å¤åå ¥åºâIQCï¼ç»è®ºå忍è¿å¾ å ¥åºæè§¦åéè´§", "roles": ["IQC è´¨æ£å"]}, {"stage": "è¿ç¨", "action": "æ¥å·¥è´¨æ£å·¥åºäº§åºè¡âIPQCï¼åæ ¼æè¡å ¥åºãä¸åæ ¼çæ NCR", "roles": ["IPQC è´¨æ£å"]}, {"stage": "åºè´§", "action": "éå®åºåºæäº¤âOQCï¼å®æååºåºåè¿å ¥å¾ æ£è´§", "roles": ["OQC è´¨æ£å"]}, {"stage": "éè´§", "action": "éæ/éå®éè´§/å®åéè´§âRQCï¼ç»è®ºåè°åæ®ä¸å®å", "roles": ["RQC è´¨æ£å"]}, {"stage": "ä¸åæ ¼", "action": "NCR: å¾ è¯å®¡âè¯å®¡âå¤ç½®(éè´§/æ¥åº/è¿å·¥/è®©æ¥æ¥æ¶/é级)âå ³é", "roles": ["è´¨éå·¥ç¨å¸"]}]}, {"stage": "ä¸åæ ¼", "action": "NCR: å¾ è¯å®¡âè¯å®¡âå¤ç½®(éè´§/æ¥åº/è¿å·¥/è®©æ¥æ¥æ¶/é级)âå ³é", "roles": ["è´¨éå·¥ç¨å¸"]}, {"stage": "åºä»¶", "action": "两æ¡åºä»¶å ¥å£ï¼â éç¨ generate/preview(è°ç¨æ¹èªå¸¦ä¸ä¸æ) 渲æ HTML å¹¶å»ç»æ°æ®å¿«ç §ï¼ç¼å· QR+æ¥æ+4使µæ°´æå¤©ç¬ç«ï¼preview åªæ¸²æä¸è½åºä¸æ¶èç¼å·ï¼regenerate ç¨å»ç»å¿«ç §éæ¸²ä¸æ°æ®ä¸ç¼å·ä¸åå¹¶ä½åºå·²å½æ¡£ PDFï¼å¯¼åº PDF ç±åç«¯è° Chromium æå°å®ä¾å·²åç HTML å彿¡£éä»¶ä¸å¿ï¼â¡generate-from-qc ç±è´¨æ£åç´æ¥åºä»¶(æ°æ®ç» MesQcReportApi ä» MES åï¼åç«¯ä¸æ¼ä¸ä¸æ)ï¼åºä»¶åäºéæ ¡éªæä¸å¡ä¼å çº§åªæ¥ç¬¬ä¸å¤ï¼ç±»åéæ³/åæ®ä¸åå¨/æªå®ææ£éª/æªå¡«å¤å®/模æ¿ç±»åä¸ç¬¦/æ å¯ç¨æ£éªé¡¹ï¼æ¥å头å¦å¸¦åæ®åå¤å® qcResult+qcResultText ä¸å¼æå¤å®å¹¶åï¼ååºå undecidableCount å¾ å¤å®é¡¹æ°ä¸ warnings(åç»æ é¢è·³è¿/ææ å·²å /æªå½å®æµå¼/æ ·åè¶ 3)ï¼åä¸å¼ åæ®å¯åå¤åºå¤ä»½", "roles": ["è´¨æ£å"]}, {"stage": "建模æ¿", "action": "AI å¯¼å ¥ï¼ä¸ä¼ æ¢ææ£éªæ¥åæä»¶(æ«æä»¶/ç §çãçµåç PDFãWord/Excel) â è¯å«ææ¨¡æ¿è稿(ä¸è½åº) â 人工å¨è®¾è®¡å¨ç¡®è®¤è°æ´åä¿åä¸ºçæ¬ï¼.doc/.xls æ§æ ¼å¼ä¸æ¯æ", "roles": ["è´¨æ£å", "è´¨éå·¥ç¨å¸"]}]}, {"id": "warehouse", "name": "ä»å¨æµç¨", "mermaid": "mermaid/06-warehouse-flow.mmd", "color": "#7e6bd6", "summary": "15 ç±»åæ®é©±å¨åºåå°è´¦(å å ç»ãåé)åå¨ï¼å ¨é¨ç»äºå¡æµæ°´ççï¼æ¯æè°æ¨å¨éãçç¹è°æ´ãæ¹æ¬¡/SN 追溯ãåºåå ç¨ä¸å级å»ç»ã线边èæåºè´åºåï¼å¯¹å¤ WmsStockApi ä¾ ERP æ¶åã", "steps": [ @@ -57,6 +59,7 @@ {"stage": "å½å ¥ææ", "action": "å票 OCR(å¾ç夿¨¡æ/PDFææ¬)ãæ¥ä»·å OCR â ç»æåé¢å¡«", "roles": ["éè´å", "éå®"]}, {"stage": "è¾ å©å¤å®", "action": "è´¨æ£å+ææ +ç¼ºé· â å»ºè®®åæ ¼/ç¹é/éè´§/æ¥åº+çç±", "roles": ["è´¨æ£å"]}, {"stage": "å³ç颿µ", "action": "缺æ/ç产æ¶é¿/ç产é£é©/交ä»åç±»é¢æµä¾è®¡åæäº§åè", "roles": ["计åå"]}, {"stage": "模æ¿è¯å«", "action": "ä¸ä¼ æ¢ææ£éªæ¥åæä»¶(æ«æä»¶/ç §çãçµåç PDFãWord/Excel) â åééæ½å(æææ¬å±èµ°ææ¬ãæ«æçé页转å¾) â 大模åæå端ä¸ä¼ çç»ä»¶æ¸ åäº§åºæå¹³ç»ä»¶è稿 â 人工å¨è®¾è®¡å¨ç¡®è®¤åä¿å为模æ¿çæ¬", "roles": ["è´¨æ£å", "è´¨éå·¥ç¨å¸"]}, {"stage": "ç¥è¯æ£ç´¢", "action": "ææ¡£âåçâMilvus åéâæ£ç´¢æ¥å£(é¢ç)", "roles": ["â"]}]} ] } docs/project-business/data/modules.json
@@ -56,6 +56,14 @@ "roles": ["è´¨æ£å(IQC/IPQC/OQC)", "è´¨éå·¥ç¨å¸"], "upstream": ["ä»å¨(å°è´§)", "ç产(æ¥å·¥)", "éå®(åºåº)", "å®å(éè´§)"], "downstream": ["ä»å¨(åæ ¼å ¥åº/æ¥åºåºåº)", "ç产(NCRè¿å·¥)"], "coreTables": ["mes_qc_iqc", "mes_qc_ipqc", "mes_qc_oqc", "mes_qc_rqc", "mes_qc_template", "mes_qc_indicator", "mes_qc_defect", "mes_qc_ncr"]}, {"key": "qcreport", "name": "æºè½è´¨æ£æ¥å", "color": "#b06ce0", "tag": "QC-REPORT", "flow": "quality", "positioning": "å¯è§å设计æ¥åæ¨¡æ¿ â ä¸é®åºä»¶å¹¶å»ç»å岿¥å", "problem": "åè¡ä¸è´¨æ£æ¥åæ ¼å¼åå¼ï¼æç硬ç¼ç æ¹ä¸æ¬¡å䏿¬¡çï¼ä¸å岿¥åå¿ é¡»å¯å¤ç°ãä¸éä¸å¡æ°æ®åå", "features": ["GrapesJS å¯è§å设计å¨ï¼è´¨æ£ç»ä»¶èµ°æ³¨åæºå¶ï¼ä¸ç¡¬ç¼ç è¿ç¼è¾å¨ï¼", "æ¨¡æ¿ + å¤çæ¬ï¼è稿0/å·²åå¸1/å·²åç¨2ï¼å·²åå¸çæ¬å 容ä¸å¯è¦ç", "ç»ä»¶æ°æ®ç»å®ä¸ç¬ç«å¤å®è§å弿ï¼ç¦ç¨ eval / ScriptEngineï¼", "ä¸å端éå对é½ç Java 渲æå¼æï¼åºç« HTML å¥ç¦» on* äºä»¶å±æ§ä¸ javascript:/vbscript:/data: åè®®", "åºä»¶æ¥å£ï¼preview åªæ¸²æä¸è½åºä¸æ¶èç¼å· / generate 渲æ+è½åº+å»ç» data_snapshot / regenerate ç¨å»ç»å¿«ç §ä¸åä¸çæ¬é渲", "æ¥åç¼å· QR+yyyyMMdd+4使µæ°´ï¼æå¤©ç¬ç«ï¼å¯æ¾å¼æå®ï¼æå·ä¼æ¥åºå ·ä½ç¼å·ï¼", "ç¼ºå£æ¸ ååªå¨ preview/generate/regenerate ååºéè¿åï¼ä¸è½åº", "PDF(Playwright) 彿¡£ä¸ MES åç§è´¨æ£åå½ä¸å对æ¥å¾ åç»è½®æ¬¡"], "objects": ["æ¥å模æ¿", "æ¥åå®ä¾"], "roles": ["æ¥å设计å", "è´¨æ£åï¼åºä»¶ï¼"], "upstream": ["è´¨éï¼è´¨æ£åä¸ä¸æï¼æ¬è½®ç±è°ç¨æ¹ä¼ å ¥ï¼"], "downstream": ["éä»¶ä¸å¿ï¼PDF 彿¡£Â·åç»è½®æ¬¡ï¼", "BI"], "coreTables": ["qc_report_template", "qc_report_template_version", "qc_report_instance", "qc_report_data_source", "qc_report_rule", "qc_report_render_record"]}, {"key": "dv", "name": "设å¤ä¸å·¥è£ æç", "color": "#59a8b8", "tag": "MES-DV/TM/CAL", "flow": null, "positioning": "设å¤èµäº§å¥åº·ä¸çäº§èµæºä¿é", "problem": "设å¤ç¹æ£ä¿å »ç»´ä¿®å¯æ§è¡ãå·¥è£ ç¶æå¯æ§ãçç»æçæä¾æ®", docs/project-business/data/objects.json
@@ -35,16 +35,18 @@ {"name": "çç¹ä»»å¡", "module": "ä»å¨", "table": "mes_wm_stock_taking_task", "doc": "éæ/卿çç¹ï¼è¡ç»æ:æ£å¸¸/çç(ADJUST_IN)/çäº(ADJUST_OUT)ï¼å¯å»ç»åºåãç²ç", "upstream": ["åºåå°è´¦"], "downstream": ["åºåå°è´¦(è°æ´)"], "keywords": "çç¹ çç çäº stock taking"}, {"name": "åºåå°è´¦", "module": "ä»å¨", "table": "mes_wm_material_stock", "doc": "ç©æÃä»åºÃåºåºÃåºä½Ãæ¹æ¬¡Ãä¾åºåå å ç»ï¼æ°é/å»ç»/å ç¨/å¨éåé", "upstream": ["å ¨é¨å ¥åºå"], "downstream": ["å ¨é¨åºåºå", "äºå¡æµæ°´"], "keywords": "åºå å°è´¦ stock å¯ç¨é"}, {"name": "æ¹æ¬¡", "module": "ä»å¨", "table": "mes_wm_batch", "doc": "å ³èç©æ/å·¥å/ä¾åºå/客æ·/è´¨éç¶æï¼SN ç 嫿¹æ¬¡ uuidï¼æ¡ç æ¯æ18ç§ä¸å¡å¯¹è±¡", "upstream": ["å ¥åºåæ®"], "downstream": ["åºåå°è´¦"], "keywords": "æ¹æ¬¡ batch SN æ¡ç 追溯"}, {"name": "IQCæ¥ææ£éª", "module": "è´¨é", "table": "mes_qc_iqc", "doc": "å°è´§éç¥/å¤åå ¥åºè§¦åï¼è稿0â宿4ï¼ç»è®ºå忥æºåæ®", "upstream": ["å°è´§éç¥", "å¤åå ¥åº"], "downstream": ["éè´å ¥åº", "ä¾åºåéè´§", "NCR"], "keywords": "IQC æ¥ææ£éª"}, {"name": "IPQCè¿ç¨æ£éª", "module": "è´¨é", "table": "mes_qc_ipqc", "doc": "æ¥å·¥è´¨æ£å·¥åºäº§åºè¡è§¦åï¼å®æåè°æ¨è¿æ¥å·¥ä¸äº§åºå ¥åº", "upstream": ["æ¥å·¥"], "downstream": ["产åºå ¥åº", "NCR"], "keywords": "IPQC è¿ç¨æ£éª å·¡æ£"}, {"name": "OQCåºè´§æ£éª", "module": "è´¨é", "table": "mes_qc_oqc", "doc": "éå®åºåºæäº¤è§¦åï¼å®ææ¨è¿åºåºåå¾ æ£è´§", "upstream": ["éå®åºåºå"], "downstream": ["éå®åºåºå(æ¨è¿)"], "keywords": "OQC åºè´§æ£éª åºåæ£éª"}, {"name": "RQCéè´§æ£éª", "module": "è´¨é", "table": "mes_qc_rqc", "doc": "ç产éæ/éå®éè´§/å®åé货触åï¼ç»è®ºåè°å®å", "upstream": ["éæ", "éè´§"], "downstream": ["éè´§å ¥åº", "å®ååè°"], "keywords": "RQC éè´§æ£éª"}, {"name": "IQCæ¥ææ£éª", "module": "è´¨é", "table": "mes_qc_iqc", "doc": "å°è´§éç¥/å¤åå ¥åºè§¦åï¼è稿0â宿4ï¼ç»è®ºå忥æºåæ®ï¼è¡æä½ãåºæ¥åãä» å¨å·²å®ææ¶å¯è§", "upstream": ["å°è´§éç¥", "å¤åå ¥åº"], "downstream": ["éè´å ¥åº", "ä¾åºåéè´§", "NCR", "æ¥åå®ä¾(åºæ¥å)"], "keywords": "IQC æ¥ææ£éª åºæ¥å"}, {"name": "IPQCè¿ç¨æ£éª", "module": "è´¨é", "table": "mes_qc_ipqc", "doc": "æ¥å·¥è´¨æ£å·¥åºäº§åºè¡è§¦åï¼å®æåè°æ¨è¿æ¥å·¥ä¸äº§åºå ¥åºï¼è¡æä½ãåºæ¥åãä» å¨å·²å®ææ¶å¯è§", "upstream": ["æ¥å·¥"], "downstream": ["产åºå ¥åº", "NCR", "æ¥åå®ä¾(åºæ¥å)"], "keywords": "IPQC è¿ç¨æ£éª å·¡æ£ åºæ¥å"}, {"name": "OQCåºè´§æ£éª", "module": "è´¨é", "table": "mes_qc_oqc", "doc": "éå®åºåºæäº¤è§¦åï¼å®ææ¨è¿åºåºåå¾ æ£è´§ï¼è¡æä½ãåºæ¥åãä» å¨å·²å®ææ¶å¯è§", "upstream": ["éå®åºåºå"], "downstream": ["éå®åºåºå(æ¨è¿)", "æ¥åå®ä¾(åºæ¥å)"], "keywords": "OQC åºè´§æ£éª åºåæ£éª åºæ¥å"}, {"name": "RQCéè´§æ£éª", "module": "è´¨é", "table": "mes_qc_rqc", "doc": "ç产éæ/éå®éè´§/å®åé货触åï¼ç»è®ºåè°å®åï¼è¡æä½ãåºæ¥åãä» å¨å·²å®ææ¶å¯è§ãæªè¾å·²ç»ä¸ä¸ºãéè´§æ£éªãï¼åèåç¶é¡¹ä¸ mes_qc_type åå ¸åãéææ£éªãï¼ä¸å端æä¸¾/表注é/èåæéå项ä¸ä¸è´ï¼RQC ä¸è é½è¦çï¼æ åãéè´§æ£éªãï¼", "upstream": ["éæ", "éè´§"], "downstream": ["éè´§å ¥åº", "å®ååè°", "æ¥åå®ä¾(åºæ¥å)"], "keywords": "RQC éè´§æ£éª éææ£éª åºæ¥å"}, {"name": "NCRä¸åæ ¼å", "module": "è´¨é", "table": "mes_qc_ncr", "doc": "å¾ è¯å®¡0âå·²è¯å®¡6âå·²å¤ç½®7âå·²å ³é8ï¼å¤ç½®:éè´§/æ¥åº/è¿å·¥/è®©æ¥æ¥æ¶/é级", "upstream": ["IQC/IPQC/OQC/RQC"], "downstream": ["éè´§/æ¥åº/è¿å·¥(å°è´¦)"], "keywords": "NCR ä¸åæ ¼ å¤ç½® ç¹é æ¥åº"}, {"name": "设å¤", "module": "设å¤", "table": "mes_dv_machinery", "doc": "å°è´¦ç¶æ:åæº/ç产ä¸/ä¿å »ä¸ï¼ç¹æ£Â·ä¿å »Â·ç»´ä¿®å·¥åï¼AI é£é©é¢æµæ¶è´¹å ¶æ°æ®", "upstream": [], "downstream": ["ç¹æ£", "ç»´ä¿®", "ç产é£é©é¢æµ"], "keywords": "è®¾å¤ ç¹æ£ ä¿å » ç»´ä¿® machinery"}, {"name": "å®åå·¥å", "module": "å®å", "table": "after_sale_ticket", "doc": "è稿0âå¤çä¸10âå¾ å ¥åº30âå¾ éæ¬¾40âå®ç»50âå ³é60/åæ¶99ï¼å¤å®:ä¸è¬/ç»´ä¿®/éè´§", "upstream": ["客æ·åé¦(人工å½å ¥)"], "downstream": ["维修记å½", "éè´§ç³è¯·"], "keywords": "å®å å·¥å ticket ç»´ä¿®"}, {"name": "éè´§ç³è¯·", "module": "å®å", "table": "after_sale_return", "doc": "è稿0âå·²å®¡æ ¸20(èªå¨å»ºMESéè´§å)â已质æ£40âå·²å ¥åº50â已鿬¾60âå ³é70ï¼ç±»å:鿬¾éè´§/æ¢è´§/ä» éæ¬¾", "upstream": ["å®åå·¥å"], "downstream": ["éå®éè´§å", "鿬¾"], "keywords": "å®åéè´§ return"}, {"name": "åå·¥", "module": "人å", "table": "hrm_employee", "doc": "å¨è1/è¯ç¨2/离è3ï¼å ¥èèªå¨å»ºç³»ç»è´¦å·", "upstream": [], "downstream": ["äº¤æ¥æ å°", "èå¤", "èªé ¬"], "keywords": "åå·¥ å ¥è employee"}, {"name": "离è交æ¥", "module": "人å", "table": "hrm_user_handover", "doc": "fromâto æ å°ï¼å·²å®æ20 å expandUserIds æ©å±äº¤æ¥äººå¯è§æ°æ®ï¼å½ååä¸å¡æ¨¡åå°æªå®é æ¥å ¥è°ç¨ï¼", "upstream": ["离èç³è¯·"], "downstream": ["忍¡åæçæ°æ®æ¥è¯¢"], "keywords": "äº¤æ¥ ç¦»è handover expandUserIds"}, {"name": "æç计å", "module": "设å¤", "table": "mes_cal_plan", "doc": "çç»Ãçæ¬¡Ãæ¥ææçï¼ä¾ç产ä¸èå¤å¼ç¨", "upstream": [], "downstream": ["ç产任å¡", "èå¤"], "keywords": "æç çç» çæ¬¡ æ¥å"} {"name": "æç计å", "module": "设å¤", "table": "mes_cal_plan", "doc": "çç»Ãçæ¬¡Ãæ¥ææçï¼ä¾ç产ä¸èå¤å¼ç¨", "upstream": [], "downstream": ["ç产任å¡", "èå¤"], "keywords": "æç çç» çæ¬¡ æ¥å"}, {"name": "æ¥å模æ¿", "module": "è´¨æ£æ¥å", "table": "qc_report_template", "doc": "æºè½è´¨æ£æ¥å模æ¿ï¼å¯ç¨0/åç¨1ï¼ç»å¸å 容åå¨ qc_report_template_versionï¼è稿0/å·²åå¸1/å·²åç¨2ï¼å·²åå¸ä¸å¯è¦çï¼ï¼currentVersion æåå½åå·²åå¸çæ¬ï¼æºæä»¶ç» AI å¯¼å ¥è¯å«æè稿åç±äººå·¥ç¡®è®¤ææä¸ºçæ¬ï¼å䏿¨¡æ¿éå¤å¯¼å ¥æãæ¿æ¢ãå¤çââå å æ§éä»¶ï¼è¿å¸¦ blob è¡ä¸ç£çæä»¶ï¼åç»æ°çï¼ä¸çå¤å¿æä»¶ï¼å 餿¨¡æ¿ä¼è¿å¸¦æ¸ éä»¶", "upstream": ["人工设计(GrapesJS 设计å¨)", "AI å¯¼å ¥è稿(qc-report/ai-import/draft)"], "downstream": ["模æ¿çæ¬", "æ¥åå®ä¾"], "keywords": "æ¥åæ¨¡æ¿ è´¨æ£æ¥å template è®¾è®¡å¨ çæ¬ AIå¯¼å ¥ è¯å« è稿 æºæä»¶æ¿æ¢"}, {"name": "æ¥åå®ä¾", "module": "è´¨æ£æ¥å", "table": "qc_report_instance", "doc": "åºä»¶äº§ç©ï¼ç¼å· QR+yyyyMMdd+4使µæ°´æå¤©ç¬ç«ä¸ä¸å¡å¯ä¸ï¼å¯æ¾å¼æå®è¦çï¼æ³¨æ uk_report_no ä¸åºå软å é¤ï¼å æçå·ä¸åæ¶ï¼ï¼ä¸¤æ¡åºä»¶å ¥å£ï¼éç¨ generateï¼è°ç¨æ¹èªå¸¦ contextï¼ä¸ generate-from-qcï¼æ°æ®ä» MES è´¨æ£ååï¼æéç qc-report:instance:generate å·²å ¥ system_menu id=1075416ï¼äºéæ ¡éªæä¸å¡ä¼å çº§åªæ¥ç¬¬ä¸å¤ï¼105_000 ç±»åéæ³ / 105_001 åæ®ä¸åå¨ / 105_002 æªå®æ / 105_003 æªå¤å® / 105_005 模æ¿ç±»åä¸ç¬¦ / 105_006 æ å¯ç¨æ£éªé¡¹ï¼åå¼ è´¨æ£åå表çè¡æä½ãåºæ¥åãä» å¨ status=已宿æ¶å¯è§ï¼å¼¹çªåªå reportType ä¸è´ã模æ¿å¯ç¨ãä¸ currentVersion æåççæ¬å·²åå¸ç模æ¿ï¼å端 /qc-report/template/selectable å¤å®ââåç¨çæ¬ä¸æ¸ 空 currentVersionï¼å端æ¿åé¡µç»æèªå·±æ¨å¯¼ä¼æ¼ï¼ï¼ç»æå°±å°å±ç¤ºå¹¶è·³æ¬é¡µå表ï¼ï¼å»ç» data_snapshotï¼å¤å®åæ°æ®ï¼å«å端ç®å¥½ç PASS/FAIL ä¸åæ ¼çï¼ä¸ render_htmlï¼regenerate åªé渲æçãæ°æ®ä¸ç¼å·ä¸åï¼business_id/business_type å MES 表åï¼mes_qc_iqc çï¼ä¸åæ® IDï¼å¯åæ¥ãä¸åå¯åºå¤ä»½ï¼æ¥å头å¦å¸¦åæ®åå¤å® qcResult(æ°)/qcResultText(䏿ï¼åæ ¼/ç¹é/ä¸åæ ¼éè´§/ä¸åæ ¼æ¥åº)ï¼ä¸å¼æä¸æå¤å®å¹¶åäºä¸è¦çï¼ååºå¸¦ undecidableCount=å¿«ç §ä¸ result 为空串ç项æ°ä¸ warningsï¼åç»æ é¢è·³è¿/ææ å·²å /æªå½å®æµå¼/æ ·åè¶ 3ï¼ï¼ä¸¤è é½ä¸è½åºãäºåçä¸å°", "upstream": ["æ¥å模æ¿çæ¬", "è´¨æ£åä¸ä¸æ(éç¨ generate ç±è°ç¨æ¹ä¼ å ¥)", "MES è´¨æ£å IQC/IPQC/OQC/RQC(generate-from-qc ç» MesQcReportApi åæ°)"], "downstream": ["å端å®ä¾é¡µ /qc/report-instance(å表·iframeé¢è§Â·å¿«ç §é¡µç¾Â·å¯¼åºPDF)", "PDF彿¡£(system_storage_attachment recordType=qc_report_instance)"], "keywords": "æ¥åå®ä¾ åºä»¶ QRç¼å· å¿«ç § å¤ç° regenerate PDF彿¡£ æ¥åå®ä¾é¡µ 导åºPDF generate-from-qc è´¨æ£ååºæ¥å åºæ¥åå¼¹çª qcResult undecidableCount warnings å¾ å¤å® 忥 ä¸å¡åæ®çé"} ] } docs/project-business/index.html
@@ -201,8 +201,8 @@ "frontend": "mom-pro2-beforeï¼Vue3 管çåå°ï¼åä»åºåæ¾ï¼åªè¯»åèï¼" }, "stats": { "businessModules": 12, "coreObjects": 46, "businessModules": 13, "coreObjects": 48, "documentFlows": 6, "approvalListeners": 13, "aiCapabilities": 8, @@ -386,6 +386,14 @@ "roles": ["è´¨æ£å(IQC/IPQC/OQC)", "è´¨éå·¥ç¨å¸"], "upstream": ["ä»å¨(å°è´§)", "ç产(æ¥å·¥)", "éå®(åºåº)", "å®å(éè´§)"], "downstream": ["ä»å¨(åæ ¼å ¥åº/æ¥åºåºåº)", "ç产(NCRè¿å·¥)"], "coreTables": ["mes_qc_iqc", "mes_qc_ipqc", "mes_qc_oqc", "mes_qc_rqc", "mes_qc_template", "mes_qc_indicator", "mes_qc_defect", "mes_qc_ncr"]}, {"key": "qcreport", "name": "æºè½è´¨æ£æ¥å", "color": "#b06ce0", "tag": "QC-REPORT", "flow": "quality", "positioning": "å¯è§å设计æ¥åæ¨¡æ¿ â ä¸é®åºä»¶å¹¶å»ç»å岿¥å", "problem": "åè¡ä¸è´¨æ£æ¥åæ ¼å¼åå¼ï¼æç硬ç¼ç æ¹ä¸æ¬¡å䏿¬¡çï¼ä¸å岿¥åå¿ é¡»å¯å¤ç°ãä¸éä¸å¡æ°æ®åå", "features": ["GrapesJS å¯è§å设计å¨ï¼è´¨æ£ç»ä»¶èµ°æ³¨åæºå¶ï¼ä¸ç¡¬ç¼ç è¿ç¼è¾å¨ï¼", "æ¨¡æ¿ + å¤çæ¬ï¼è稿0/å·²åå¸1/å·²åç¨2ï¼å·²åå¸çæ¬å 容ä¸å¯è¦ç", "ç»ä»¶æ°æ®ç»å®ä¸ç¬ç«å¤å®è§å弿ï¼ç¦ç¨ eval / ScriptEngineï¼", "ä¸å端éå对é½ç Java 渲æå¼æï¼åºç« HTML å¥ç¦» on* äºä»¶å±æ§ä¸ javascript:/vbscript:/data: åè®®", "åºä»¶æ¥å£ï¼preview åªæ¸²æä¸è½åºä¸æ¶èç¼å· / generate 渲æ+è½åº+å»ç» data_snapshot / regenerate ç¨å»ç»å¿«ç §ä¸åä¸çæ¬é渲", "æ¥åç¼å· QR+yyyyMMdd+4使µæ°´ï¼æå¤©ç¬ç«ï¼å¯æ¾å¼æå®ï¼æå·ä¼æ¥åºå ·ä½ç¼å·ï¼", "ç¼ºå£æ¸ ååªå¨ preview/generate/regenerate ååºéè¿åï¼ä¸è½åº", "PDF(Playwright) 彿¡£ä¸ MES åç§è´¨æ£åå½ä¸å对æ¥å¾ åç»è½®æ¬¡"], "objects": ["æ¥å模æ¿", "æ¥åå®ä¾"], "roles": ["æ¥å设计å", "è´¨æ£åï¼åºä»¶ï¼"], "upstream": ["è´¨éï¼è´¨æ£åä¸ä¸æï¼æ¬è½®ç±è°ç¨æ¹ä¼ å ¥ï¼"], "downstream": ["éä»¶ä¸å¿ï¼PDF 彿¡£Â·åç»è½®æ¬¡ï¼", "BI"], "coreTables": ["qc_report_template", "qc_report_template_version", "qc_report_instance", "qc_report_data_source", "qc_report_rule", "qc_report_render_record"]}, {"key": "dv", "name": "设å¤ä¸å·¥è£ æç", "color": "#59a8b8", "tag": "MES-DV/TM/CAL", "flow": null, "positioning": "设å¤èµäº§å¥åº·ä¸çäº§èµæºä¿é", "problem": "设å¤ç¹æ£ä¿å »ç»´ä¿®å¯æ§è¡ãå·¥è£ ç¶æå¯æ§ãçç»æçæä¾æ®", @@ -470,17 +478,19 @@ {"name": "çç¹ä»»å¡", "module": "ä»å¨", "table": "mes_wm_stock_taking_task", "doc": "éæ/卿çç¹ï¼è¡ç»æ:æ£å¸¸/çç(ADJUST_IN)/çäº(ADJUST_OUT)ï¼å¯å»ç»åºåãç²ç", "upstream": ["åºåå°è´¦"], "downstream": ["åºåå°è´¦(è°æ´)"], "keywords": "çç¹ çç çäº stock taking"}, {"name": "åºåå°è´¦", "module": "ä»å¨", "table": "mes_wm_material_stock", "doc": "ç©æÃä»åºÃåºåºÃåºä½Ãæ¹æ¬¡Ãä¾åºåå å ç»ï¼æ°é/å»ç»/å ç¨/å¨éåé", "upstream": ["å ¨é¨å ¥åºå"], "downstream": ["å ¨é¨åºåºå", "äºå¡æµæ°´"], "keywords": "åºå å°è´¦ stock å¯ç¨é"}, {"name": "æ¹æ¬¡", "module": "ä»å¨", "table": "mes_wm_batch", "doc": "å ³èç©æ/å·¥å/ä¾åºå/客æ·/è´¨éç¶æï¼SN ç 嫿¹æ¬¡ uuidï¼æ¡ç æ¯æ18ç§ä¸å¡å¯¹è±¡", "upstream": ["å ¥åºåæ®"], "downstream": ["åºåå°è´¦"], "keywords": "æ¹æ¬¡ batch SN æ¡ç 追溯"}, {"name": "IQCæ¥ææ£éª", "module": "è´¨é", "table": "mes_qc_iqc", "doc": "å°è´§éç¥/å¤åå ¥åºè§¦åï¼è稿0â宿4ï¼ç»è®ºå忥æºåæ®", "upstream": ["å°è´§éç¥", "å¤åå ¥åº"], "downstream": ["éè´å ¥åº", "ä¾åºåéè´§", "NCR"], "keywords": "IQC æ¥ææ£éª"}, {"name": "IPQCè¿ç¨æ£éª", "module": "è´¨é", "table": "mes_qc_ipqc", "doc": "æ¥å·¥è´¨æ£å·¥åºäº§åºè¡è§¦åï¼å®æåè°æ¨è¿æ¥å·¥ä¸äº§åºå ¥åº", "upstream": ["æ¥å·¥"], "downstream": ["产åºå ¥åº", "NCR"], "keywords": "IPQC è¿ç¨æ£éª å·¡æ£"}, {"name": "OQCåºè´§æ£éª", "module": "è´¨é", "table": "mes_qc_oqc", "doc": "éå®åºåºæäº¤è§¦åï¼å®ææ¨è¿åºåºåå¾ æ£è´§", "upstream": ["éå®åºåºå"], "downstream": ["éå®åºåºå(æ¨è¿)"], "keywords": "OQC åºè´§æ£éª åºåæ£éª"}, {"name": "RQCéè´§æ£éª", "module": "è´¨é", "table": "mes_qc_rqc", "doc": "ç产éæ/éå®éè´§/å®åé货触åï¼ç»è®ºåè°å®å", "upstream": ["éæ", "éè´§"], "downstream": ["éè´§å ¥åº", "å®ååè°"], "keywords": "RQC éè´§æ£éª"}, {"name": "IQCæ¥ææ£éª", "module": "è´¨é", "table": "mes_qc_iqc", "doc": "å°è´§éç¥/å¤åå ¥åºè§¦åï¼è稿0â宿4ï¼ç»è®ºå忥æºåæ®ï¼è¡æä½ãåºæ¥åãä» å¨å·²å®ææ¶å¯è§", "upstream": ["å°è´§éç¥", "å¤åå ¥åº"], "downstream": ["éè´å ¥åº", "ä¾åºåéè´§", "NCR", "æ¥åå®ä¾(åºæ¥å)"], "keywords": "IQC æ¥ææ£éª åºæ¥å"}, {"name": "IPQCè¿ç¨æ£éª", "module": "è´¨é", "table": "mes_qc_ipqc", "doc": "æ¥å·¥è´¨æ£å·¥åºäº§åºè¡è§¦åï¼å®æåè°æ¨è¿æ¥å·¥ä¸äº§åºå ¥åºï¼è¡æä½ãåºæ¥åãä» å¨å·²å®ææ¶å¯è§", "upstream": ["æ¥å·¥"], "downstream": ["产åºå ¥åº", "NCR", "æ¥åå®ä¾(åºæ¥å)"], "keywords": "IPQC è¿ç¨æ£éª å·¡æ£ åºæ¥å"}, {"name": "OQCåºè´§æ£éª", "module": "è´¨é", "table": "mes_qc_oqc", "doc": "éå®åºåºæäº¤è§¦åï¼å®ææ¨è¿åºåºåå¾ æ£è´§ï¼è¡æä½ãåºæ¥åãä» å¨å·²å®ææ¶å¯è§", "upstream": ["éå®åºåºå"], "downstream": ["éå®åºåºå(æ¨è¿)", "æ¥åå®ä¾(åºæ¥å)"], "keywords": "OQC åºè´§æ£éª åºåæ£éª åºæ¥å"}, {"name": "RQCéè´§æ£éª", "module": "è´¨é", "table": "mes_qc_rqc", "doc": "ç产éæ/éå®éè´§/å®åé货触åï¼ç»è®ºåè°å®åï¼è¡æä½ãåºæ¥åãä» å¨å·²å®ææ¶å¯è§ãæªè¾å·²ç»ä¸ä¸ºãéè´§æ£éªãï¼åèåç¶é¡¹ä¸ mes_qc_type åå ¸åãéææ£éªãï¼ä¸å端æä¸¾/表注é/èåæéå项ä¸ä¸è´ï¼RQC ä¸è é½è¦çï¼æ åãéè´§æ£éªãï¼", "upstream": ["éæ", "éè´§"], "downstream": ["éè´§å ¥åº", "å®ååè°", "æ¥åå®ä¾(åºæ¥å)"], "keywords": "RQC éè´§æ£éª éææ£éª åºæ¥å"}, {"name": "NCRä¸åæ ¼å", "module": "è´¨é", "table": "mes_qc_ncr", "doc": "å¾ è¯å®¡0âå·²è¯å®¡6âå·²å¤ç½®7âå·²å ³é8ï¼å¤ç½®:éè´§/æ¥åº/è¿å·¥/è®©æ¥æ¥æ¶/é级", "upstream": ["IQC/IPQC/OQC/RQC"], "downstream": ["éè´§/æ¥åº/è¿å·¥(å°è´¦)"], "keywords": "NCR ä¸åæ ¼ å¤ç½® ç¹é æ¥åº"}, {"name": "设å¤", "module": "设å¤", "table": "mes_dv_machinery", "doc": "å°è´¦ç¶æ:åæº/ç产ä¸/ä¿å »ä¸ï¼ç¹æ£Â·ä¿å »Â·ç»´ä¿®å·¥åï¼AI é£é©é¢æµæ¶è´¹å ¶æ°æ®", "upstream": [], "downstream": ["ç¹æ£", "ç»´ä¿®", "ç产é£é©é¢æµ"], "keywords": "è®¾å¤ ç¹æ£ ä¿å » ç»´ä¿® machinery"}, {"name": "å®åå·¥å", "module": "å®å", "table": "after_sale_ticket", "doc": "è稿0âå¤çä¸10âå¾ å ¥åº30âå¾ éæ¬¾40âå®ç»50âå ³é60/åæ¶99ï¼å¤å®:ä¸è¬/ç»´ä¿®/éè´§", "upstream": ["客æ·åé¦(人工å½å ¥)"], "downstream": ["维修记å½", "éè´§ç³è¯·"], "keywords": "å®å å·¥å ticket ç»´ä¿®"}, {"name": "éè´§ç³è¯·", "module": "å®å", "table": "after_sale_return", "doc": "è稿0âå·²å®¡æ ¸20(èªå¨å»ºMESéè´§å)â已质æ£40âå·²å ¥åº50â已鿬¾60âå ³é70ï¼ç±»å:鿬¾éè´§/æ¢è´§/ä» éæ¬¾", "upstream": ["å®åå·¥å"], "downstream": ["éå®éè´§å", "鿬¾"], "keywords": "å®åéè´§ return"}, {"name": "åå·¥", "module": "人å", "table": "hrm_employee", "doc": "å¨è1/è¯ç¨2/离è3ï¼å ¥èèªå¨å»ºç³»ç»è´¦å·", "upstream": [], "downstream": ["äº¤æ¥æ å°", "èå¤", "èªé ¬"], "keywords": "åå·¥ å ¥è employee"}, {"name": "离è交æ¥", "module": "人å", "table": "hrm_user_handover", "doc": "fromâto æ å°ï¼å·²å®æ20 å expandUserIds æ©å±äº¤æ¥äººå¯è§æ°æ®ï¼å½ååä¸å¡æ¨¡åå°æªå®é æ¥å ¥è°ç¨ï¼", "upstream": ["离èç³è¯·"], "downstream": ["忍¡åæçæ°æ®æ¥è¯¢"], "keywords": "äº¤æ¥ ç¦»è handover expandUserIds"}, {"name": "æç计å", "module": "设å¤", "table": "mes_cal_plan", "doc": "çç»Ãçæ¬¡Ãæ¥ææçï¼ä¾ç产ä¸èå¤å¼ç¨", "upstream": [], "downstream": ["ç产任å¡", "èå¤"], "keywords": "æç çç» çæ¬¡ æ¥å"} {"name": "æç计å", "module": "设å¤", "table": "mes_cal_plan", "doc": "çç»Ãçæ¬¡Ãæ¥ææçï¼ä¾ç产ä¸èå¤å¼ç¨", "upstream": [], "downstream": ["ç产任å¡", "èå¤"], "keywords": "æç çç» çæ¬¡ æ¥å"}, {"name": "æ¥å模æ¿", "module": "è´¨æ£æ¥å", "table": "qc_report_template", "doc": "æºè½è´¨æ£æ¥å模æ¿ï¼å¯ç¨0/åç¨1ï¼ç»å¸å 容åå¨ qc_report_template_versionï¼è稿0/å·²åå¸1/å·²åç¨2ï¼å·²åå¸ä¸å¯è¦çï¼ï¼currentVersion æåå½åå·²åå¸çæ¬ï¼æºæä»¶ç» AI å¯¼å ¥è¯å«æè稿åç±äººå·¥ç¡®è®¤ææä¸ºçæ¬ï¼å䏿¨¡æ¿éå¤å¯¼å ¥æãæ¿æ¢ãå¤çââå å æ§éä»¶ï¼è¿å¸¦ blob è¡ä¸ç£çæä»¶ï¼åç»æ°çï¼ä¸çå¤å¿æä»¶ï¼å 餿¨¡æ¿ä¼è¿å¸¦æ¸ éä»¶", "upstream": ["人工设计(GrapesJS 设计å¨)", "AI å¯¼å ¥è稿(qc-report/ai-import/draft)"], "downstream": ["模æ¿çæ¬", "æ¥åå®ä¾"], "keywords": "æ¥åæ¨¡æ¿ è´¨æ£æ¥å template è®¾è®¡å¨ çæ¬ AIå¯¼å ¥ è¯å« è稿 æºæä»¶æ¿æ¢"}, {"name": "æ¥åå®ä¾", "module": "è´¨æ£æ¥å", "table": "qc_report_instance", "doc": "åºä»¶äº§ç©ï¼ç¼å· QR+yyyyMMdd+4使µæ°´æå¤©ç¬ç«ä¸ä¸å¡å¯ä¸ï¼å¯æ¾å¼æå®è¦çï¼æ³¨æ uk_report_no ä¸åºå软å é¤ï¼å æçå·ä¸åæ¶ï¼ï¼ä¸¤æ¡åºä»¶å ¥å£ï¼éç¨ generateï¼è°ç¨æ¹èªå¸¦ contextï¼ä¸ generate-from-qcï¼æ°æ®ä» MES è´¨æ£ååï¼æéç qc-report:instance:generate å·²å ¥ system_menu id=1075416ï¼äºéæ ¡éªæä¸å¡ä¼å çº§åªæ¥ç¬¬ä¸å¤ï¼105_000 ç±»åéæ³ / 105_001 åæ®ä¸åå¨ / 105_002 æªå®æ / 105_003 æªå¤å® / 105_005 模æ¿ç±»åä¸ç¬¦ / 105_006 æ å¯ç¨æ£éªé¡¹ï¼åå¼ è´¨æ£åå表çè¡æä½ãåºæ¥åãä» å¨ status=已宿æ¶å¯è§ï¼å¼¹çªåªå reportType ä¸è´ã模æ¿å¯ç¨ãä¸ currentVersion æåççæ¬å·²åå¸ç模æ¿ï¼å端 /qc-report/template/selectable å¤å®ââåç¨çæ¬ä¸æ¸ 空 currentVersionï¼å端æ¿åé¡µç»æèªå·±æ¨å¯¼ä¼æ¼ï¼ï¼ç»æå°±å°å±ç¤ºå¹¶è·³æ¬é¡µå表ï¼ï¼å»ç» data_snapshotï¼å¤å®åæ°æ®ï¼å«å端ç®å¥½ç PASS/FAIL ä¸åæ ¼çï¼ä¸ render_htmlï¼regenerate åªé渲æçãæ°æ®ä¸ç¼å·ä¸åï¼business_id/business_type å MES 表åï¼mes_qc_iqc çï¼ä¸åæ® IDï¼å¯åæ¥ãä¸åå¯åºå¤ä»½ï¼æ¥å头å¦å¸¦åæ®åå¤å® qcResult(æ°)/qcResultText(䏿ï¼åæ ¼/ç¹é/ä¸åæ ¼éè´§/ä¸åæ ¼æ¥åº)ï¼ä¸å¼æä¸æå¤å®å¹¶åäºä¸è¦çï¼ååºå¸¦ undecidableCount=å¿«ç §ä¸ result 为空串ç项æ°ä¸ warningsï¼åç»æ é¢è·³è¿/ææ å·²å /æªå½å®æµå¼/æ ·åè¶ 3ï¼ï¼ä¸¤è é½ä¸è½åºãäºåçä¸å°", "upstream": ["æ¥å模æ¿çæ¬", "è´¨æ£åä¸ä¸æ(éç¨ generate ç±è°ç¨æ¹ä¼ å ¥)", "MES è´¨æ£å IQC/IPQC/OQC/RQC(generate-from-qc ç» MesQcReportApi åæ°)"], "downstream": ["å端å®ä¾é¡µ /qc/report-instance(å表·iframeé¢è§Â·å¿«ç §é¡µç¾Â·å¯¼åºPDF)", "PDF彿¡£(system_storage_attachment recordType=qc_report_instance)"], "keywords": "æ¥åå®ä¾ åºä»¶ QRç¼å· å¿«ç § å¤ç° regenerate PDF彿¡£ æ¥åå®ä¾é¡µ 导åºPDF generate-from-qc è´¨æ£ååºæ¥å åºæ¥åå¼¹çª qcResult undecidableCount warnings å¾ å¤å® 忥 ä¸å¡åæ®çé"} ] }</script> <script type="application/json" id="data-flows">{ @@ -514,13 +524,15 @@ {"stage": "èå¨", "action": "审æ¹éè¿æå·¥åºæ è®°: åå²âèªå¨ç©ææ¶èæ£çº¿è¾¹åº(å 许è´åºå)ï¼è´¨æ£â产åºå¾ æ£è¡ç IPQCï¼å ³é®éæ£âç´æ¥äº§åºå ¥åº", "roles": ["ç³»ç»èªå¨"]}, {"stage": "å®å·¥", "action": "ä» æ«éå·¥åºååå·¥åå·²ç产æ°éï¼è¾¾æ èªå¨å®æï¼MPS ç´¯è®¡å¤æå®æå¹¶åäºä»¶ç»éå®è®¢å", "roles": ["ç³»ç»èªå¨"]}]}, {"id": "quality", "name": "è´¨éæµç¨", "mermaid": "mermaid/05-quality-flow.mmd", "color": "#e06c6c", "summary": "å鿣éªé¸å£(IQC/IPQC/OQC/RQCï¼äºæ 0â4) + å¤å®åæ(åæ ¼/ç¹é/éè´§/æ¥åº) + NCR è¯å®¡å¤ç½®å ³éï¼AI æä¾å¤å®å»ºè®®ï¼NCR å½åä» å°è´¦ä¸è§¦åè¿å·¥åã", "summary": "å鿣éªé¸å£(IQC/IPQC/OQC/RQCï¼äºæ 0â4) + å¤å®åæ(åæ ¼/ç¹é/éè´§/æ¥åº) + NCR è¯å®¡å¤ç½®å ³éï¼AI æä¾å¤å®å»ºè®®ï¼NCR å½åä» å°è´¦ä¸è§¦åè¿å·¥åï¼æ£éªåå¯ç±æºè½è´¨æ£æ¥åå¹³å°ä¸é®åºä»¶ï¼generate-from-qcï¼æ°æ®ä» MES åï¼äºéæ ¡éªå渲æè½åºï¼ã", "steps": [ {"stage": "æ¥æ", "action": "å°è´§éç¥/å¤åå ¥åºâIQCï¼ç»è®ºå忍è¿å¾ å ¥åºæè§¦åéè´§", "roles": ["IQC è´¨æ£å"]}, {"stage": "è¿ç¨", "action": "æ¥å·¥è´¨æ£å·¥åºäº§åºè¡âIPQCï¼åæ ¼æè¡å ¥åºãä¸åæ ¼çæ NCR", "roles": ["IPQC è´¨æ£å"]}, {"stage": "åºè´§", "action": "éå®åºåºæäº¤âOQCï¼å®æååºåºåè¿å ¥å¾ æ£è´§", "roles": ["OQC è´¨æ£å"]}, {"stage": "éè´§", "action": "éæ/éå®éè´§/å®åéè´§âRQCï¼ç»è®ºåè°åæ®ä¸å®å", "roles": ["RQC è´¨æ£å"]}, {"stage": "ä¸åæ ¼", "action": "NCR: å¾ è¯å®¡âè¯å®¡âå¤ç½®(éè´§/æ¥åº/è¿å·¥/è®©æ¥æ¥æ¶/é级)âå ³é", "roles": ["è´¨éå·¥ç¨å¸"]}]}, {"stage": "ä¸åæ ¼", "action": "NCR: å¾ è¯å®¡âè¯å®¡âå¤ç½®(éè´§/æ¥åº/è¿å·¥/è®©æ¥æ¥æ¶/é级)âå ³é", "roles": ["è´¨éå·¥ç¨å¸"]}, {"stage": "åºä»¶", "action": "两æ¡åºä»¶å ¥å£ï¼â éç¨ generate/preview(è°ç¨æ¹èªå¸¦ä¸ä¸æ) 渲æ HTML å¹¶å»ç»æ°æ®å¿«ç §ï¼ç¼å· QR+æ¥æ+4使µæ°´æå¤©ç¬ç«ï¼preview åªæ¸²æä¸è½åºä¸æ¶èç¼å·ï¼regenerate ç¨å»ç»å¿«ç §éæ¸²ä¸æ°æ®ä¸ç¼å·ä¸åå¹¶ä½åºå·²å½æ¡£ PDFï¼å¯¼åº PDF ç±åç«¯è° Chromium æå°å®ä¾å·²åç HTML å彿¡£éä»¶ä¸å¿ï¼â¡generate-from-qc ç±è´¨æ£åç´æ¥åºä»¶(æ°æ®ç» MesQcReportApi ä» MES åï¼åç«¯ä¸æ¼ä¸ä¸æ)ï¼åºä»¶åäºéæ ¡éªæä¸å¡ä¼å çº§åªæ¥ç¬¬ä¸å¤ï¼ç±»åéæ³/åæ®ä¸åå¨/æªå®ææ£éª/æªå¡«å¤å®/模æ¿ç±»åä¸ç¬¦/æ å¯ç¨æ£éªé¡¹ï¼æ¥å头å¦å¸¦åæ®åå¤å® qcResult+qcResultText ä¸å¼æå¤å®å¹¶åï¼ååºå undecidableCount å¾ å¤å®é¡¹æ°ä¸ warnings(åç»æ é¢è·³è¿/ææ å·²å /æªå½å®æµå¼/æ ·åè¶ 3)ï¼åä¸å¼ åæ®å¯åå¤åºå¤ä»½", "roles": ["è´¨æ£å"]}, {"stage": "建模æ¿", "action": "AI å¯¼å ¥ï¼ä¸ä¼ æ¢ææ£éªæ¥åæä»¶(æ«æä»¶/ç §çãçµåç PDFãWord/Excel) â è¯å«ææ¨¡æ¿è稿(ä¸è½åº) â 人工å¨è®¾è®¡å¨ç¡®è®¤è°æ´åä¿åä¸ºçæ¬ï¼.doc/.xls æ§æ ¼å¼ä¸æ¯æ", "roles": ["è´¨æ£å", "è´¨éå·¥ç¨å¸"]}]}, {"id": "warehouse", "name": "ä»å¨æµç¨", "mermaid": "mermaid/06-warehouse-flow.mmd", "color": "#7e6bd6", "summary": "15 ç±»åæ®é©±å¨åºåå°è´¦(å å ç»ãåé)åå¨ï¼å ¨é¨ç»äºå¡æµæ°´ççï¼æ¯æè°æ¨å¨éãçç¹è°æ´ãæ¹æ¬¡/SN 追溯ãåºåå ç¨ä¸å级å»ç»ã线边èæåºè´åºåï¼å¯¹å¤ WmsStockApi ä¾ ERP æ¶åã", "steps": [ @@ -542,6 +554,7 @@ {"stage": "å½å ¥ææ", "action": "å票 OCR(å¾ç夿¨¡æ/PDFææ¬)ãæ¥ä»·å OCR â ç»æåé¢å¡«", "roles": ["éè´å", "éå®"]}, {"stage": "è¾ å©å¤å®", "action": "è´¨æ£å+ææ +ç¼ºé· â å»ºè®®åæ ¼/ç¹é/éè´§/æ¥åº+çç±", "roles": ["è´¨æ£å"]}, {"stage": "å³ç颿µ", "action": "缺æ/ç产æ¶é¿/ç产é£é©/交ä»åç±»é¢æµä¾è®¡åæäº§åè", "roles": ["计åå"]}, {"stage": "模æ¿è¯å«", "action": "ä¸ä¼ æ¢ææ£éªæ¥åæä»¶(æ«æä»¶/ç §çãçµåç PDFãWord/Excel) â åééæ½å(æææ¬å±èµ°ææ¬ãæ«æçé页转å¾) â 大模åæå端ä¸ä¼ çç»ä»¶æ¸ åäº§åºæå¹³ç»ä»¶è稿 â 人工å¨è®¾è®¡å¨ç¡®è®¤åä¿å为模æ¿çæ¬", "roles": ["è´¨æ£å", "è´¨éå·¥ç¨å¸"]}, {"stage": "ç¥è¯æ£ç´¢", "action": "ææ¡£âåçâMilvus åéâæ£ç´¢æ¥å£(é¢ç)", "roles": ["â"]}]} ] }</script> @@ -627,6 +640,13 @@ RQC éè´§æ£éª NCR ä¸åæ ¼å è´¨æ£æ¹æ¡ä¸ææ ç¼ºé· æºè½è´¨æ£æ¥å QC-REPORT æ¥å模æ¿ä¸å¤çæ¬ å¯è§åè®¾è®¡å¨ ç»ä»¶ç»å®ä¸å¤å®è§å HTML 渲æå¼æ æ¥ååºä»¶ä¸ç¼å· æ°æ®å¿«ç §å»ç» 设å¤ä¸æç 设å¤å°è´¦ ç¹æ£ä¸ä¿å » @@ -749,6 +769,69 @@ N2 -->|å¤ç½®æ¹å¼:éè´§1æ¥åº2è¿å·¥3è®©æ¥æ¥æ¶4é级使ç¨5| N3[å·²å¤ç½®7] N3 -->|å ³é| N4[å·²å ³é8] Q8[æ¯ææ°æ®:è´¨æ£æ¹æ¡Â·æ£éªææ ·缺é·åºÂ·ææ ç»æ] --- A1 Q8H["æ£éªææ çå±çº§å¯å¨è´¨æ£ææ 页维æ¤Â·æ¡ç®ç±»å æå±åç» å ¬å¼ æåºå·Â·åªå 许两å±Â·å åç»çº§èå å项"] --- Q8 A1 -->|åºä»¶ generate-from-qc| GATE{åºä»¶å éæ ¡éªÂ·æä¸å¡ä¼å çº§åªæ¥ç¬¬ä¸å¤} A2 -->|åºä»¶ generate-from-qc| GATE A3 -->|åºä»¶ generate-from-qc| GATE A4 -->|åºä»¶ generate-from-qc| GATE GATE -->|000 ç±»åéæ³| GE0[105_000 qcType ä¸å¨ 1~4] GATE -->|001 åæ®ä¸åå¨| GE1[105_001] GATE -->|002 æªå®ææ£éª| GE2[105_002·带å½åç¶æä¸æå] GATE -->|003 æªå¡«å¤å®| GE3[105_003·ååºå个å¯éå¤å®] GATE -->|005 模æ¿ç±»åä¸ç¬¦| GE5[105_005·两边类å䏿åé½ç»Â·å端æ reportType é¢è¿æ»¤] GATE -->|006 æ å¯ç¨æ£éªé¡¹| GE6[105_006] GATE -->|éè¿Â·åªå 许åç±»åå·²å叿¨¡æ¿| RP[æºè½è´¨æ£æ¥åå¹³å° qcreport] RP --> RPT[æ¥åæ¨¡æ¿ qc_report_template·å¯ç¨0åç¨1] RPT --> RPTV[模æ¿çæ¬ qc_report_template_version·è稿0å·²åå¸1å·²åç¨2·å½åçæ¬ä¸å¯åç¨ 101_010] RPTVSAVE[设计å¨ä¿å create æ update] -->|åå ¥ä¾§é¸é¨Â·è§å101_006 ä¸ç»å¸å®å ¨101_009 ä¸åæ ¼ä¸å¾ä¸è½åº| RPTV RPTVSAVE -.->|åªå¡æ°åå ¥Â·åéèç»å¸ä¸åæº¯æ ¸æ¥| CVNOTE[ç»å¸æ ¡éªè¦çä¸å¤æ³¨å ¥é¢Â·tagName 屿§å æ´æ®µCSS] RPTV -->|æå·²åå¸çæ¬æ¸²æ| RPI[æ¥åå®ä¾ qc_report_instance·ç¼å· QR+yyyyMMdd+4使µæ°´Â·æå¤©ç¬ç«] RPI --> SNAP[å»ç» data_snapshot·å¤å®åæ°æ®å« PASS FAIL ä¸åæ ¼ç] RPI --> RHTML[render_html 宿´ HTML ææ¡£Â·åºç«å·²å¥ç¦»äºä»¶å±æ§ä¸å±é©åè®®] RPI --> RREGEN[éæ°çæ regenerate·å»ç»å¿«ç §+å䏿¨¡æ¿çæ¬éæ¸²Â·æ°æ®ä¸ç¼å·ä¸å·并ä½åºå·²å½æ¡£PDF] RPI --> RPDF[导åºPDF å端è°Chromiumæå°å·²åHTMLÂ·å½æ¡£ system_storage_attachment·recordType=qc_report_instance] RPI --> RUI[æ¥åå®ä¾é¡µ /qc/report-instance·èåæè´¨é管ç parent_id=5500] RUI -->|æéç | RPERM["qc-report:instance:query æ¥è¯¢Â·:export 导åºPDF·:delete å é¤Â·:create éç¨åºä»¶Â·:generate è´¨æ£ååºä»¶(menu 1075416)"] RUI -->|忥 businessType=mes_qc_iqc ç表å + businessId| RBACK[åä¸å¼ è´¨æ£åå¯åºå¤ä»½æ¥å·åå²ççéè¦Â·ä¸åé夿¦æª] RUI --> RPREVIEW[详æ å¼¹çªÂ·iframe sandboxç©ºå±æ§æ¸²æ render_htmlÂ·æ°æ®å¿«ç §é¡µç¾å±ç¤ºå»ç» data_snapshot] A1 -.->|åç«¯å ¥å£Â·è¡æä½ãåºæ¥åãä» status=已宿å¯è§| UIREP[åºæ¥åå¼¹çªÂ·IQC IPQC OQC RQC åé¡µå ±ç¨] A2 -.->|åä¸| UIREP A3 -.->|åä¸| UIREP A4 -.->|åä¸| UIREP UIREP -->|åªå reportType 䏿¬åä¸è´Â·å¯ç¨Â·å½åçæ¬å·²åå¸ä¸ç»å¸é空·é¿å éä¸å¿ 被æçç»å| UIPICK[ç¨æ·é模æ¿] UIPICK -->|POST generate-from-qc| GATE UIREP -->|ç»æå°±å°å±ç¤º reportNo itemCount| UIRES["undecidableCount>0 èè²ä¿¡æ¯æ¡ï¼å¤ä¸ºè¿ç¨åæ°æ å¤å®è§å·é»è²ä¼å 乿¯å误æ¥ï¼Â·warnings ä¸ errors éæ¡å±ç¤ºÂ·ä¸è½åºäºåçä¸å°"] UIRES -->|æ¥çæ¥åå表| RUI RUI -->|è·¯ç± query å¸¦å ¥ businessType+businessId å¹¶é¢å¡«æç´¢| RBACK GATE -->|MesQcReportApi.getQcReportData| MESMAP[MES åå±åæ°å½ä¸Â·å头 / å¤å®ä¾æ®è¡ max min threshold / æ ·å indicator_result / 宿µå¼ result_detail] MESMAP --> RCTX[æ¥åä¸ä¸æ report+inspectionItems·ç±å端æ å°å¨äº§åºÂ·å端ä¸åèªå·±æ¼] RCTX --> RCITEM[æ£éªé¡¹=æ ·åÃææ 交åå±å¼Â·å ¨ææ è¿ä¸è¿æ»¤Â·å¤æ ·åæ¶æ ·åå·æè¿ remark] RCTX --> RCQC[æºå¸¦åæ®åå¤å® qcResult æ°Â·qcResultText ä¸æÂ·ä¸å¼æå¤å®å¹¶åäºä¸è¦ç] MESMAP -->|åç»é¡¹ item_type=2 ä¸å®çå项æ parent_id è¿å两å±Â·å½å±ä¸ç type ä¹ä¸çè¡åº| RCTXG RCTXG["åç»è¿å·ç»å¤´è¡èç»åä¸ formula_text å ¬å¼Â·ç»å 第ä¸è¡è span=è¯¥ç»æ»è¡æ°Â·åç»å ¶ä½è¡ span=1 ä¸ hidden 让ä½Â·åªåå¹¶æ£éªé¡¹ç®ä¸å"] --- RCTX MESMAP -.->|åç»é¡¹æ¬å没æå±äºå®çå项 / ç»å¤´æ¬å没æè¡ç±å端补ä¸è¡| RCWARN MESMAP -.->|ææ å·²å / æªå½å®æµå¼ï¼ç»å¤´éç¹è¡æå®ä¹æ å¼Â·ä¸æ¥ï¼ / æ ·åè¶ 3 ä¸ªÂ·è¿ warnings| RCWARN RCWARN[warnings 软æç¤ºÂ·ä¸è½åºÂ·äºåçä¸å°éèªè¡çç] --- RCTX RCITEM --- RCTX RCQC --- RCTX RCTX --> RPI RCTX --> RUNDEC["undecidableCount 没æå¤å®ç»è®ºç项æ°=å¿«ç §é result 为空串çé¡¹Â·å«æ å¤å®è§åä¸å¾ å¤å®ä¸¤ç§Â·ä¸è®¡å ¥åæ ¼ç忝䏿¥åç»è®ºÂ·çææ¶å¼¹æç¤ºä¸åå¡«"] RPT -->|è¿å ¥è®¾è®¡å¨å¹¶å¸¦ templateId·æé qc-report:template:ai-import| AIIMP[AIå¯¼å ¥ qc-report/ai-import/draft] AISRC["æºæä»¶ æ«æä»¶ç §ç·çµåçPDF·Word(.docx) Excel(.xlsx)·txt csv"] -->|ä¸ä¼ èµ° system Storage API å blobId·ç»ä¸º qc_report_template éä»¶| AIIMP AISRC -.->|å䏿¨¡æ¿éå¤å¯¼å ¥ææ¿æ¢å¤ç·å å æ§éä»¶åç»æ°ç·è¿å blob è¡ä¸ç£çæä»¶ä¸èµ·æ¸ | AIIMP AISRC -.->|docx ç Word 页ç页è读å并以 页ç é¡µè æ è®°å¹¶å ¥è¯å«å 容·页ç卿£æå页è卿£æåÂ·åæ¶è¿ warningsÂ·æ«æä»¶ç §çä¸åå½±å| AIIMP AIIMP -->|è¯å«Â·åªäº§è稿ä¸è½åº| AIRD[模æ¿è稿 æå¹³ç»ä»¶+屿§æ¸ å] AIHINT[ç§¯æ¨æ¸ 忝项另 hint éå说æÂ·æ¥èªå端注å表 aiHint·ç»ä»¶éåçå¯ä¸çç¸æ¥æº] -.->|éå以 hint 为å·æä¸é¨ç»ä»¶å°±ä¸éç¨éç¨ç»ä»¶åæ°| AIIMP AIIMP -->|è¡¨æ ¼æ«è¡çè½æ¬¾ç¾ç½²è¡å±è½æ¬¾ä¸å±æ°æ®è¡Â·ä¸å件页èåå¹¶ä¸äººåæ¥æç空æå¡«Â·ä¸å¾èªé ç»å®é®| AIRD AIIMP -.->|å件忝é»è®¤å¤æè¡¨å¤´åä¸¤å±ææ ·ååæ®µä¸åæ¶Â·åè¿ç»ä»¶ç columns headerSpans fields ä¸ä¸ªææ¬å±æ§è¦çÂ·ä¸æ¢ç»ä»¶| AIRD AIKEY[æ¥åå¯ç»å®é®ç½åå·ç±å端 ReportFields åå°çæÂ·ä¸æå] -.->|åªç¦ç¼é ä¸ç»å¯éé忍¡ååªè½ç·æ å ¨éæä¸¾| AIIMP AIRD -->|人工å¨è®¾è®¡å¨ç¡®è®¤Â·è£ é å¨ç¼è¯è¿ç»å¸åä¿å| RPTV AIRD -.->|ç»å¸ä¸ç ReportHeader ReportFooter æ¯æ£æéçæ¬å¤´è½æ¬¾Â·ä¸ PDF æ¯é¡µéå¤ççå¼é¡µç页è䏿¯ä¸åäº| RPTV AIIMP -.->|表头æåèªå·±æ¢æ¯åå忝䏿 ¼Â·å件鿝䏿 ¼çºµååå¹¶è·¨ä½ä¸ä¸ä¸¤å±ãä¸å±é£æ ¼å¨æåææ¬éåçâåä¸| AIRD AIIMP -.->|该段æ é¢ä¸ååä¸åä¸å·®æè®¤æçºµååå¹¶æ ¼ãåº rowspan=2 䏿 ¼ä¸ä¸å±è·³è¿è¯¥å·ä¸ä¸è´å°±è¢«å½æå¦ä¸å±åç»æ é¢ãç»è®ºäºåä¸ä¸åå°ä¸é| AIRD RPTVSAVE -.->|设计å¨ç»å¸ä¸è·ç»å®Â·ç»åæ ¼ç hidden å ä½ç¬¦ä¼è¢«æµè§å¨å½æç hidden 让æ´è¡å·¦ç§»ãæ«åæ¤æç«æç»æ¡| CVNOTE RPTVSAVE -.->|èªå®åä¸å¸¦å®½åº¦å£°æÂ·å宽åªè½ç±æå°å å®¹å®½åº¦åæ¨Â·è弿¯å ä½ç¬¦è¿ç±»ä¸å«ç©ºæ ¼çé¿ä¸²Â·æ´è¡¨ä¼è¢«æåºçº¸å¼ ãæ«åæ¤æç«æ¡| CVWIDTH CVWIDTH["æ èªå®åçåå æ ¼å¸¦ overflow-wrap:anywhere å 许é¿ä¸²æè¡Â·é»è®¤åææå·¥å®½åº¦å£°æä¸å Â·å®æµå äºåèææ£éªé¡¹ç®å项æ¤å° 30 ä½åç´ ãè¡é«ç¿»å å"] --- RPTVSAVE </pre> <pre style="display:none" class="mmd-src" data-mmd="06-warehouse-flow"> flowchart LR @@ -824,6 +907,8 @@ AI[AI å¹³å° éä¹åé®+Milvus] -.->|å票OCR| ERP AI -.->|æ¥ä»·OCR| CRM AI -.->|è´¨æ£å¤å®ä¸çäº§é¢æµ| MES MES -.->|è´¨æ£åä¸ä¸æÂ·æ¬è½®ç±è°ç¨æ¹ä¼ å ¥ å¾ MesQcReportApi æé| QCR[æºè½è´¨æ£æ¥å qcreport 模æ¿çæ¬å®ä¾å¿«ç §] QCR -.->|PDF 彿¡£Â·åç»è½®æ¬¡| STG MES --> BI[BI 管çé©¾é©¶è± SQLé 置大å±] ERP --> BI CRM --> BI @@ -872,6 +957,39 @@ IV -->|忬¾å¼ºå¶å å¼ç¥¨| RK RC -->|å ³èCRMå®¢æ·æ ¡éª| CU PA -->|å ³èæ¥ç¥¨ä¸ä¾åºå| PI subgraph S4 [æºè½è´¨æ£æ¥å qcreport] RPT[æ¥å模æ¿] --> RPTV[模æ¿çæ¬] RPTVSAVE[设计å¨ä¿å create æ update] -->|åå ¥ä¾§é¸é¨ è§å101_006 ç»å¸å®å ¨101_009 ä¸åæ ¼ä¸è½åº| RPTV RPTVSAVE -.->|åªå¡æ°åå ¥ åéèç»å¸ä¸åæº¯æ ¸æ¥| CVNOTE[ç»å¸æ ¡éªè¦ç tagName 屿§å æ´æ®µCSS ä¸å¤æ³¨å ¥é¢] RPTV --> RPI[æ¥åå®ä¾] RPI --> SNAP[æ°æ®å¿«ç §å»ç»] RPI --> RHTML[HTML 渲æäº§ç©] RPI --> RPDF[PDF 产ç©] AISRC[æºæä»¶ æ«æä»¶ç §ç·çµåçPDF·Word Excel·txt csv] -->|ä¸ä¼ èµ° system Storage API å blobId å¹¶ç»ä¸ºæ¨¡æ¿éä»¶| AIRD[AI è¯å«è稿] AIRD -->|人工å¨è®¾è®¡å¨ç¡®è®¤åä¿å| RPTV end QCDOC[è´¨æ£å IQC IPQC OQC RQC] -->|generate-from-qc·带 qcType qcId templateId| QCGATE{äºéæ ¡éª ç±»å åå¨ å·²å®æ å·²å¤å® 模æ¿ç±»å ææ£éªé¡¹} QCGATE -->|ä»»ä¸ä¸è¿Â·æä¸å¡ä¼å çº§åªæ¥ç¬¬ä¸å¤| QCERR[105_000~105_006] QCGATE -->|éè¿| MESQCAPI[MesQcReportApi åå±åæ°Â·å头 å¤å®ä¾æ®è¡ æ ·å 宿µå¼] MESQCAPI -->|çº¯å½æ°æ å°å¨å½ä¸ å«åå¤å® qcResult qcResultText| RPI MESQCAPI -.->|åç»æ é¢è·³è¿ ææ å·²å æªå½å®æµå¼ æ ·åè¶ 3| QCWARN[warnings 软æç¤ºÂ·ä¸è½åº] QCWARN -.-> RPI RPI -.->|undecidableCount=å¿«ç § result 为空串çé¡¹Â·çææ¶å¼¹æç¤º| QCFE RPI -.->|businessType=mes_qc_iqc ç表å + businessId åæ¥Â·ä¸åå¯åºå¤ä»½| QCFE RPT -.->|report_type æ°åå符串须çäº qcType·åç«¯æ®æ¤è¿æ»¤æ¨¡æ¿å表| QCGATE RPTV -->|模æ¿åç¨åä¸ååºæ°ä»¶Â·åå²å®ä¾ä¸åå½±å·å½åçæ¬ä¸å¯åç¨ 101_010| RPI RPT -->|è¿å ¥è®¾è®¡å¨å¸¦ templateId è° AI å¯¼å ¥| AIRD RPI -->|å表 详æ é¢è§ æå°| QCFE["å端æ¥åé¡µé¢ /qc/report-instance å·²è½å°Â·iframe 渲æ render_html + å¿«ç §é¡µç¾ + 导åºPDF"] RPDF -->|彿¡£ éä»¶ä¸å¿ recordType=qc_report_instance| PDFARCH[system_storage_attachment] AISRC -.->|å½å± éä»¶ä¸å¿ recordType=qc_report_template| PDFARCH AISRC -.->|å䏿¨¡æ¿éå¤å¯¼å ¥ææ¿æ¢å¤ç·å å æ§é件·è¿å blob è¡ä¸ç£çæä»¶ä¸èµ·æ¸ | PDFARCH QCFE -.->|æéç qc-report:instance:query/export/delete/create å·²å ¥ system_menu| SYS1[system_menu] QCGATE -.->|æéç qc-report:instance:generate å·²å ¥ system_menu id=1075416| SYS1[system_menu] QCDOC -.->|åç«¯å ¥å£ è¡æä½ãåºæ¥åãä» status=已宿å¯è§Â·åé¡µå ±ç¨åä¸å¼¹çª| UIREP[åºæ¥åå¼¹çªÂ·åªååç±»å å¯ç¨ 䏿çæçæ¬ç模æ¿] UIREP -->|鿍¡æ¿å POST generate-from-qc| QCGATE UIREP -.->|ç»æå°±å°å±ç¤º reportNo itemCount ä¸ undecidableCount åè¦| QCFE QCFE -.->|è¿å ¥æ¶å¸¦ businessType+businessId è·¯ç± query·é¢å¡«æç´¢è¡¨åçãä¸å¡åæ®ãåæ®µ| UIREP RPT -.->|æéç qc-report:template:ai-import å·²å ¥ system_menu| SYS1 </pre> <pre style="display:none" class="mmd-src" data-mmd="10-business-object"> erDiagram @@ -910,6 +1028,8 @@ erp_purchase_order ||--o{ erp_purchase_invoice : "æ¥ç¥¨" erp_purchase_invoice ||--o{ erp_finance_payment : "仿¬¾" hrm_employee ||--o| hrm_user_handover : "离èäº¤æ¥æ å°" qc_report_template ||--o{ qc_report_template_version : "å¤çæ¬" qc_report_template_version ||--o{ qc_report_instance : "å»ç»åºä»¶çæ¬" </pre> <pre style="display:none" class="mmd-src" data-mmd="11-ai-business-flow"> flowchart TD @@ -931,6 +1051,23 @@ R3[å·¥åºæ åå·¥æ¶+è®¾å¤æ¯å°æ¶äº§é+ä¼ éåç] --> R4[ç产æ¶é¿é¢æµ] R5[IPQC缺é·ç+设å¤ä¿å »ç¹æ£+ç©æå¯ç¨ç+å ³é®å·¥åº] --> R6[ç产é£é©é¢æµ] R7[è¿åº¦+å©ä½å¤©æ°+ä»»å¡å®æç¶æ+客æ·äº¤æ] --> R8[交ä»é¢æµÂ·è½å¦ææ¶+置信度+延误å ç´ ] W1[è´¨æ£åä¸ä¼ æ¢ææ£éªæ¥åæä»¶ blobId] --> W2{æ©å±åä¸å 容å¤å®} W2 -->|png/jpg/bmp/gif/webp| W3[ååèèµ° chatWithImage] W2 -->|PDF æææ¬å±| W4[PDFBox æ½åææ¬ â chat] W2 -->|PDF æ«æç| W5[PDFRenderer é页转PNG â æ¯é¡µ chatWithImage] W2 -->|docx/xlsx| W6["POI æ½åÂ·è¡¨æ ¼å½ä¸æç¨³å®ç½æ ¼ï¼gridSpan 补空å ä½ / vMerge ç»æ ¼æ âåä¸ï¼"] W2 -->|txt/csv| W7[ç´è¯»ææ¬ â chat] W2 -->|doc/xls| W8[æç»Â·æç¤ºå¦å为 docx/xlsx] W3 --> W9[å¤é¡µé页è¯å«] W4 --> W9 W5 --> W9 W6 --> W9 W7 --> W9 W9 --> W10["æ(type,props)å»éåå¹¶âæå¹³ç»ä»¶è稿+summary+warnings"] W10 --> W11[å¼¹çªé¢è§Â·äººå·¥ç¡®è®¤åè£ è¿è®¾è®¡å¨ç»å¸] W11 --> W12[äººå·¥è°æ´âä¿å为模æ¿çæ¬Â·è¿æ¯å¯ä¸è½åºç¹] WCL[ç»ä»¶ç§¯æ¨æ¸ åç±åç«¯ä»æ´»æ³¨å表çæå¹¶é请æ±ä¸ä¼ ] --- W9 WSAFE[åªäº§è稿ä¸è½åºÂ·æªæ³¨åtypeå¨è£ é é¶æ®µè¢«è·³è¿Â·æ°¸è¿äº§ä¸åºä»»æHTML] --- W10 KB1[ç¥è¯åºææ¡£ä¸ä¼ URL] --> KB2[POI/Tikaæåææ¬] --> KB3[åç约800tokensâåéåâMilvus] KB3 --> KB4[åæ®µæ£ç´¢æ¥å£ topK+ç¸ä¼¼åº¦éå¼] KB4 -.-> KB5[ææªæ¥å ¥ä¸å¡é®çæµç¨Â·å¾ 确认] docs/project-business/mermaid/01-business-mindmap.mmd
@@ -55,6 +55,13 @@ RQC éè´§æ£éª NCR ä¸åæ ¼å è´¨æ£æ¹æ¡ä¸ææ ç¼ºé· æºè½è´¨æ£æ¥å QC-REPORT æ¥å模æ¿ä¸å¤çæ¬ å¯è§åè®¾è®¡å¨ ç»ä»¶ç»å®ä¸å¤å®è§å HTML 渲æå¼æ æ¥ååºä»¶ä¸ç¼å· æ°æ®å¿«ç §å»ç» 设å¤ä¸æç 设å¤å°è´¦ ç¹æ£ä¸ä¿å » docs/project-business/mermaid/05-quality-flow.mmd
@@ -18,3 +18,66 @@ N2 -->|å¤ç½®æ¹å¼:éè´§1æ¥åº2è¿å·¥3è®©æ¥æ¥æ¶4é级使ç¨5| N3[å·²å¤ç½®7] N3 -->|å ³é| N4[å·²å ³é8] Q8[æ¯ææ°æ®:è´¨æ£æ¹æ¡Â·æ£éªææ ·缺é·åºÂ·ææ ç»æ] --- A1 Q8H["æ£éªææ çå±çº§å¯å¨è´¨æ£ææ 页维æ¤Â·æ¡ç®ç±»å æå±åç» å ¬å¼ æåºå·Â·åªå 许两å±Â·å åç»çº§èå å项"] --- Q8 A1 -->|åºä»¶ generate-from-qc| GATE{åºä»¶å éæ ¡éªÂ·æä¸å¡ä¼å çº§åªæ¥ç¬¬ä¸å¤} A2 -->|åºä»¶ generate-from-qc| GATE A3 -->|åºä»¶ generate-from-qc| GATE A4 -->|åºä»¶ generate-from-qc| GATE GATE -->|000 ç±»åéæ³| GE0[105_000 qcType ä¸å¨ 1~4] GATE -->|001 åæ®ä¸åå¨| GE1[105_001] GATE -->|002 æªå®ææ£éª| GE2[105_002·带å½åç¶æä¸æå] GATE -->|003 æªå¡«å¤å®| GE3[105_003·ååºå个å¯éå¤å®] GATE -->|005 模æ¿ç±»åä¸ç¬¦| GE5[105_005·两边类å䏿åé½ç»Â·å端æ reportType é¢è¿æ»¤] GATE -->|006 æ å¯ç¨æ£éªé¡¹| GE6[105_006] GATE -->|éè¿Â·åªå 许åç±»åå·²å叿¨¡æ¿| RP[æºè½è´¨æ£æ¥åå¹³å° qcreport] RP --> RPT[æ¥åæ¨¡æ¿ qc_report_template·å¯ç¨0åç¨1] RPT --> RPTV[模æ¿çæ¬ qc_report_template_version·è稿0å·²åå¸1å·²åç¨2·å½åçæ¬ä¸å¯åç¨ 101_010] RPTVSAVE[设计å¨ä¿å create æ update] -->|åå ¥ä¾§é¸é¨Â·è§å101_006 ä¸ç»å¸å®å ¨101_009 ä¸åæ ¼ä¸å¾ä¸è½åº| RPTV RPTVSAVE -.->|åªå¡æ°åå ¥Â·åéèç»å¸ä¸åæº¯æ ¸æ¥| CVNOTE[ç»å¸æ ¡éªè¦çä¸å¤æ³¨å ¥é¢Â·tagName 屿§å æ´æ®µCSS] RPTV -->|æå·²åå¸çæ¬æ¸²æ| RPI[æ¥åå®ä¾ qc_report_instance·ç¼å· QR+yyyyMMdd+4使µæ°´Â·æå¤©ç¬ç«] RPI --> SNAP[å»ç» data_snapshot·å¤å®åæ°æ®å« PASS FAIL ä¸åæ ¼ç] RPI --> RHTML[render_html 宿´ HTML ææ¡£Â·åºç«å·²å¥ç¦»äºä»¶å±æ§ä¸å±é©åè®®] RPI --> RREGEN[éæ°çæ regenerate·å»ç»å¿«ç §+å䏿¨¡æ¿çæ¬éæ¸²Â·æ°æ®ä¸ç¼å·ä¸å·并ä½åºå·²å½æ¡£PDF] RPI --> RPDF[导åºPDF å端è°Chromiumæå°å·²åHTMLÂ·å½æ¡£ system_storage_attachment·recordType=qc_report_instance] RPI --> RUI[æ¥åå®ä¾é¡µ /qc/report-instance·èåæè´¨é管ç parent_id=5500] RUI -->|æéç | RPERM["qc-report:instance:query æ¥è¯¢Â·:export 导åºPDF·:delete å é¤Â·:create éç¨åºä»¶Â·:generate è´¨æ£ååºä»¶(menu 1075416)"] RUI -->|忥 businessType=mes_qc_iqc ç表å + businessId| RBACK[åä¸å¼ è´¨æ£åå¯åºå¤ä»½æ¥å·åå²ççéè¦Â·ä¸åé夿¦æª] RUI --> RPREVIEW[详æ å¼¹çªÂ·iframe sandboxç©ºå±æ§æ¸²æ render_htmlÂ·æ°æ®å¿«ç §é¡µç¾å±ç¤ºå»ç» data_snapshot] A1 -.->|åç«¯å ¥å£Â·è¡æä½ãåºæ¥åãä» status=已宿å¯è§| UIREP[åºæ¥åå¼¹çªÂ·IQC IPQC OQC RQC åé¡µå ±ç¨] A2 -.->|åä¸| UIREP A3 -.->|åä¸| UIREP A4 -.->|åä¸| UIREP UIREP -->|åªå reportType 䏿¬åä¸è´Â·å¯ç¨Â·å½åçæ¬å·²åå¸ä¸ç»å¸é空·é¿å éä¸å¿ 被æçç»å| UIPICK[ç¨æ·é模æ¿] UIPICK -->|POST generate-from-qc| GATE UIREP -->|ç»æå°±å°å±ç¤º reportNo itemCount| UIRES["undecidableCount>0 èè²ä¿¡æ¯æ¡ï¼å¤ä¸ºè¿ç¨åæ°æ å¤å®è§å·é»è²ä¼å 乿¯å误æ¥ï¼Â·warnings ä¸ errors éæ¡å±ç¤ºÂ·ä¸è½åºäºåçä¸å°"] UIRES -->|æ¥çæ¥åå表| RUI RUI -->|è·¯ç± query å¸¦å ¥ businessType+businessId å¹¶é¢å¡«æç´¢| RBACK GATE -->|MesQcReportApi.getQcReportData| MESMAP[MES åå±åæ°å½ä¸Â·å头 / å¤å®ä¾æ®è¡ max min threshold / æ ·å indicator_result / 宿µå¼ result_detail] MESMAP --> RCTX[æ¥åä¸ä¸æ report+inspectionItems·ç±å端æ å°å¨äº§åºÂ·å端ä¸åèªå·±æ¼] RCTX --> RCITEM[æ£éªé¡¹=æ ·åÃææ 交åå±å¼Â·å ¨ææ è¿ä¸è¿æ»¤Â·å¤æ ·åæ¶æ ·åå·æè¿ remark] RCTX --> RCQC[æºå¸¦åæ®åå¤å® qcResult æ°Â·qcResultText ä¸æÂ·ä¸å¼æå¤å®å¹¶åäºä¸è¦ç] MESMAP -->|åç»é¡¹ item_type=2 ä¸å®çå项æ parent_id è¿å两å±Â·å½å±ä¸ç type ä¹ä¸çè¡åº| RCTXG RCTXG["åç»è¿å·ç»å¤´è¡èç»åä¸ formula_text å ¬å¼Â·ç»å 第ä¸è¡è span=è¯¥ç»æ»è¡æ°Â·åç»å ¶ä½è¡ span=1 ä¸ hidden 让ä½Â·åå¹¶çæ¯æ¨¡æ¿ columns é带 # çé£ä¸åï¼é»è®¤å峿£éªé¡¹ç®ï¼Â·åå¹¶è¡æ°æ¯æ¬å该ç»å®é è¡æ°èéä¸»æ°æ®å项æ°Â·è¯¥ç»æ¬åæ å项è¡åä¸åå¹¶"] --- RCTX MESMAP -.->|åç»é¡¹æ¬å没æå±äºå®çå项 / ç»å¤´æ¬å没æè¡ç±å端补ä¸è¡| RCWARN MESMAP -.->|ææ å·²å / æªå½å®æµå¼ï¼ç»å¤´éç¹è¡æå®ä¹æ å¼Â·ä¸æ¥ï¼ / æ ·åè¶ 3 ä¸ªÂ·è¿ warnings| RCWARN RCWARN[warnings 软æç¤ºÂ·ä¸è½åºÂ·äºåçä¸å°éèªè¡çç] --- RCTX RCITEM --- RCTX RCQC --- RCTX RCTX --> RPI RCTX --> RUNDEC["undecidableCount 没æå¤å®ç»è®ºç项æ°=å¿«ç §é result 为空串çé¡¹Â·å«æ å¤å®è§åä¸å¾ å¤å®ä¸¤ç§Â·ä¸è®¡å ¥åæ ¼ç忝䏿¥åç»è®ºÂ·çææ¶å¼¹æç¤ºä¸åå¡«"] RPT -->|è¿å ¥è®¾è®¡å¨å¹¶å¸¦ templateId·æé qc-report:template:ai-import| AIIMP[AIå¯¼å ¥ qc-report/ai-import/draft] AISRC["æºæä»¶ æ«æä»¶ç §ç·çµåçPDF·Word(.docx) Excel(.xlsx)·txt csv"] -->|ä¸ä¼ èµ° system Storage API å blobId·ç»ä¸º qc_report_template éä»¶| AIIMP AISRC -.->|å䏿¨¡æ¿éå¤å¯¼å ¥ææ¿æ¢å¤ç·å å æ§éä»¶åç»æ°ç·è¿å blob è¡ä¸ç£çæä»¶ä¸èµ·æ¸ | AIIMP AISRC -.->|docx ç Word 页ç页è读å并以 页ç é¡µè æ è®°å¹¶å ¥è¯å«å 容·页ç卿£æå页è卿£æåÂ·åæ¶è¿ warningsÂ·æ«æä»¶ç §çä¸åå½±å| AIIMP AIIMP -->|è¯å«Â·åªäº§è稿ä¸è½åº| AIRD[模æ¿è稿 æå¹³ç»ä»¶+屿§æ¸ å] AIHINT[ç§¯æ¨æ¸ 忝项另 hint éå说æÂ·æ¥èªå端注å表 aiHint·ç»ä»¶éåçå¯ä¸çç¸æ¥æº] -.->|éå以 hint 为å·æä¸é¨ç»ä»¶å°±ä¸éç¨éç¨ç»ä»¶åæ°| AIIMP AIIMP -->|è¡¨æ ¼æ«è¡çè½æ¬¾ç¾ç½²è¡å±è½æ¬¾ä¸å±æ°æ®è¡Â·ä¸å件页èåå¹¶ä¸äººåæ¥æç空æå¡«Â·ä¸å¾èªé ç»å®é®| AIRD AIIMP -.->|å件忝é»è®¤å¤æè¡¨å¤´åä¸¤å±ææ ·ååæ®µä¸åæ¶Â·åè¿ç»ä»¶ç columns headerSpans fields ä¸ä¸ªææ¬å±æ§è¦çÂ·ä¸æ¢ç»ä»¶| AIRD AIKEY[æ¥åå¯ç»å®é®ç½åå·ç±å端 ReportFields åå°çæÂ·ä¸æå] -.->|åªç¦ç¼é ä¸ç»å¯éé忍¡ååªè½ç·æ å ¨éæä¸¾| AIIMP AIRD -->|人工å¨è®¾è®¡å¨ç¡®è®¤Â·è£ é å¨ç¼è¯è¿ç»å¸åä¿å| RPTV AIRD -.->|ç»å¸ä¸ç ReportHeader ReportFooter æ¯æ£æéçæ¬å¤´è½æ¬¾Â·ä¸ PDF æ¯é¡µéå¤ççå¼é¡µç页è䏿¯ä¸åäº| RPTV AIIMP -.->|表头æåèªå·±æ¢æ¯åå忝䏿 ¼Â·å件鿝䏿 ¼çºµååå¹¶è·¨ä½ä¸ä¸ä¸¤å±ãä¸å±é£æ ¼å¨æåææ¬éåçâåä¸| AIRD AIIMP -.->|该段æ é¢ä¸ååä¸åä¸å·®æè®¤æçºµååå¹¶æ ¼ãåº rowspan=2 䏿 ¼ä¸ä¸å±è·³è¿è¯¥å·ä¸ä¸è´å°±è¢«å½æå¦ä¸å±åç»æ é¢ãç»è®ºäºåä¸ä¸åå°ä¸é| AIRD RPTVSAVE -.->|设计å¨ç»å¸ä¸è·ç»å®Â·ç»åæ ¼ç hidden å ä½ç¬¦ä¼è¢«æµè§å¨å½æç hidden 让æ´è¡å·¦ç§»ãæ«åæ¤æç«æç»æ¡| CVNOTE RPTVSAVE -.->|èªå®åä¸å¸¦å®½åº¦å£°æÂ·å宽åªè½ç±æå°å å®¹å®½åº¦åæ¨Â·è弿¯å ä½ç¬¦è¿ç±»ä¸å«ç©ºæ ¼çé¿ä¸²Â·æ´è¡¨ä¼è¢«æåºçº¸å¼ ãæ«åæ¤æç«æ¡| CVWIDTH CVWIDTH["æ èªå®åçåå æ ¼å¸¦ overflow-wrap:anywhere å 许é¿ä¸²æè¡Â·é»è®¤åææå·¥å®½åº¦å£°æä¸å Â·å®æµå äºåèææ£éªé¡¹ç®å项æ¤å° 30 ä½åç´ ãè¡é«ç¿»å å"] --- RPTVSAVE docs/project-business/mermaid/08-module-relation.mmd
@@ -19,6 +19,8 @@ AI[AI å¹³å° éä¹åé®+Milvus] -.->|å票OCR| ERP AI -.->|æ¥ä»·OCR| CRM AI -.->|è´¨æ£å¤å®ä¸çäº§é¢æµ| MES MES -.->|è´¨æ£åä¸ä¸æÂ·æ¬è½®ç±è°ç¨æ¹ä¼ å ¥ å¾ MesQcReportApi æé| QCR[æºè½è´¨æ£æ¥å qcreport 模æ¿çæ¬å®ä¾å¿«ç §] QCR -.->|PDF 彿¡£Â·åç»è½®æ¬¡| STG MES --> BI[BI 管çé©¾é©¶è± SQLé 置大å±] ERP --> BI CRM --> BI docs/project-business/mermaid/09-data-flow.mmd
@@ -37,3 +37,36 @@ IV -->|忬¾å¼ºå¶å å¼ç¥¨| RK RC -->|å ³èCRMå®¢æ·æ ¡éª| CU PA -->|å ³èæ¥ç¥¨ä¸ä¾åºå| PI subgraph S4 [æºè½è´¨æ£æ¥å qcreport] RPT[æ¥å模æ¿] --> RPTV[模æ¿çæ¬] RPTVSAVE[设计å¨ä¿å create æ update] -->|åå ¥ä¾§é¸é¨ è§å101_006 ç»å¸å®å ¨101_009 ä¸åæ ¼ä¸è½åº| RPTV RPTVSAVE -.->|åªå¡æ°åå ¥ åéèç»å¸ä¸åæº¯æ ¸æ¥| CVNOTE[ç»å¸æ ¡éªè¦ç tagName 屿§å æ´æ®µCSS ä¸å¤æ³¨å ¥é¢] RPTV --> RPI[æ¥åå®ä¾] RPI --> SNAP[æ°æ®å¿«ç §å»ç»] RPI --> RHTML[HTML 渲æäº§ç©] RPI --> RPDF[PDF 产ç©] AISRC[æºæä»¶ æ«æä»¶ç §ç·çµåçPDF·Word Excel·txt csv] -->|ä¸ä¼ èµ° system Storage API å blobId å¹¶ç»ä¸ºæ¨¡æ¿éä»¶| AIRD[AI è¯å«è稿] AIRD -->|人工å¨è®¾è®¡å¨ç¡®è®¤åä¿å| RPTV end QCDOC[è´¨æ£å IQC IPQC OQC RQC] -->|generate-from-qc·带 qcType qcId templateId| QCGATE{äºéæ ¡éª ç±»å åå¨ å·²å®æ å·²å¤å® 模æ¿ç±»å ææ£éªé¡¹} QCGATE -->|ä»»ä¸ä¸è¿Â·æä¸å¡ä¼å çº§åªæ¥ç¬¬ä¸å¤| QCERR[105_000~105_006] QCGATE -->|éè¿| MESQCAPI[MesQcReportApi åå±åæ°Â·å头 å¤å®ä¾æ®è¡ æ ·å 宿µå¼] MESQCAPI -->|çº¯å½æ°æ å°å¨å½ä¸ å«åå¤å® qcResult qcResultText| RPI MESQCAPI -.->|åç»æ é¢è·³è¿ ææ å·²å æªå½å®æµå¼ æ ·åè¶ 3| QCWARN[warnings 软æç¤ºÂ·ä¸è½åº] QCWARN -.-> RPI RPI -.->|undecidableCount=å¿«ç § result 为空串çé¡¹Â·çææ¶å¼¹æç¤º| QCFE RPI -.->|businessType=mes_qc_iqc ç表å + businessId åæ¥Â·ä¸åå¯åºå¤ä»½| QCFE RPT -.->|report_type æ°åå符串须çäº qcType·åç«¯æ®æ¤è¿æ»¤æ¨¡æ¿å表| QCGATE RPTV -->|模æ¿åç¨åä¸ååºæ°ä»¶Â·åå²å®ä¾ä¸åå½±å·å½åçæ¬ä¸å¯åç¨ 101_010| RPI RPT -->|è¿å ¥è®¾è®¡å¨å¸¦ templateId è° AI å¯¼å ¥| AIRD RPI -->|å表 详æ é¢è§ æå°| QCFE["å端æ¥åé¡µé¢ /qc/report-instance å·²è½å°Â·iframe 渲æ render_html + å¿«ç §é¡µç¾ + 导åºPDF"] RPDF -->|彿¡£ éä»¶ä¸å¿ recordType=qc_report_instance| PDFARCH[system_storage_attachment] AISRC -.->|å½å± éä»¶ä¸å¿ recordType=qc_report_template| PDFARCH AISRC -.->|å䏿¨¡æ¿éå¤å¯¼å ¥ææ¿æ¢å¤ç·å å æ§é件·è¿å blob è¡ä¸ç£çæä»¶ä¸èµ·æ¸ | PDFARCH QCFE -.->|æéç qc-report:instance:query/export/delete/create å·²å ¥ system_menu| SYS1[system_menu] QCGATE -.->|æéç qc-report:instance:generate å·²å ¥ system_menu id=1075416| SYS1[system_menu] QCDOC -.->|åç«¯å ¥å£ è¡æä½ãåºæ¥åãä» status=已宿å¯è§Â·åé¡µå ±ç¨åä¸å¼¹çª| UIREP[åºæ¥åå¼¹çªÂ·åªååç±»å å¯ç¨ 䏿çæçæ¬ç模æ¿] UIREP -->|鿍¡æ¿å POST generate-from-qc| QCGATE UIREP -.->|ç»æå°±å°å±ç¤º reportNo itemCount ä¸ undecidableCount åè¦| QCFE QCFE -.->|è¿å ¥æ¶å¸¦ businessType+businessId è·¯ç± query·é¢å¡«æç´¢è¡¨åçãä¸å¡åæ®ãåæ®µ| UIREP RPT -.->|æéç qc-report:template:ai-import å·²å ¥ system_menu| SYS1 docs/project-business/mermaid/10-business-object.mmd
@@ -34,3 +34,5 @@ erp_purchase_order ||--o{ erp_purchase_invoice : "æ¥ç¥¨" erp_purchase_invoice ||--o{ erp_finance_payment : "仿¬¾" hrm_employee ||--o| hrm_user_handover : "离èäº¤æ¥æ å°" qc_report_template ||--o{ qc_report_template_version : "å¤çæ¬" qc_report_template_version ||--o{ qc_report_instance : "å»ç»åºä»¶çæ¬" docs/project-business/mermaid/11-ai-business-flow.mmd
@@ -17,6 +17,23 @@ R3[å·¥åºæ åå·¥æ¶+è®¾å¤æ¯å°æ¶äº§é+ä¼ éåç] --> R4[ç产æ¶é¿é¢æµ] R5[IPQC缺é·ç+设å¤ä¿å »ç¹æ£+ç©æå¯ç¨ç+å ³é®å·¥åº] --> R6[ç产é£é©é¢æµ] R7[è¿åº¦+å©ä½å¤©æ°+ä»»å¡å®æç¶æ+客æ·äº¤æ] --> R8[交ä»é¢æµÂ·è½å¦ææ¶+置信度+延误å ç´ ] W1[è´¨æ£åä¸ä¼ æ¢ææ£éªæ¥åæä»¶ blobId] --> W2{æ©å±åä¸å 容å¤å®} W2 -->|png/jpg/bmp/gif/webp| W3[ååèèµ° chatWithImage] W2 -->|PDF æææ¬å±| W4[PDFBox æ½åææ¬ â chat] W2 -->|PDF æ«æç| W5[PDFRenderer é页转PNG â æ¯é¡µ chatWithImage] W2 -->|docx/xlsx| W6["POI æ½åÂ·è¡¨æ ¼å½ä¸æç¨³å®ç½æ ¼ï¼gridSpan 补空å ä½ / vMerge ç»æ ¼æ âåä¸ï¼"] W2 -->|txt/csv| W7[ç´è¯»ææ¬ â chat] W2 -->|doc/xls| W8[æç»Â·æç¤ºå¦å为 docx/xlsx] W3 --> W9[å¤é¡µé页è¯å«] W4 --> W9 W5 --> W9 W6 --> W9 W7 --> W9 W9 --> W10["æ(type,props)å»éåå¹¶âæå¹³ç»ä»¶è稿+summary+warnings"] W10 --> W11[å¼¹çªé¢è§Â·äººå·¥ç¡®è®¤åè£ è¿è®¾è®¡å¨ç»å¸] W11 --> W12[äººå·¥è°æ´âä¿å为模æ¿çæ¬Â·è¿æ¯å¯ä¸è½åºç¹] WCL[ç»ä»¶ç§¯æ¨æ¸ åç±åç«¯ä»æ´»æ³¨å表çæå¹¶é请æ±ä¸ä¼ ] --- W9 WSAFE[åªäº§è稿ä¸è½åºÂ·æªæ³¨åtypeå¨è£ é é¶æ®µè¢«è·³è¿Â·æ°¸è¿äº§ä¸åºä»»æHTML] --- W10 KB1[ç¥è¯åºææ¡£ä¸ä¼ URL] --> KB2[POI/Tikaæåææ¬] --> KB3[åç约800tokensâåéåâMilvus] KB3 --> KB4[åæ®µæ£ç´¢æ¥å£ topK+ç¸ä¼¼åº¦éå¼] KB4 -.-> KB5[ææªæ¥å ¥ä¸å¡é®çæµç¨Â·å¾ 确认] docs/qc_report_ai_import_frontend_integration.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,360 @@ # æºè½è´¨æ£æ¥å â AI å¯¼å ¥æ¨¡æ¿ å端èè°æ¹æ¡ > å端模åï¼`yudao-module-qcreport`ãæ¥å£åç¼ï¼`/admin-api/qc-report/ai-import` > æ¬è½®èå´ï¼**ä¸ä¼ ä¸ä»½å·²æçæ£éªæ¥åæä»¶ â AI 读æ â äº§åºæ¨¡æ¿è稿**ã > **æ¥å£ä¸è½åº**ï¼èç¨¿åªæ¯ãä¸ä¸²ç»ä»¶ + åèªç屿§ãï¼äººå·¥å¨è®¾è®¡å¨ç¡®è®¤åç¹æ¢æçãä¿åãæä¼åææ¨¡æ¿çæ¬ã ## æ¶åé¡µé¢ | é¡µé¢ | 说æ | æ¬è½®ç¶æ | |---|---|---| | 模æ¿è®¾è®¡å¨ï¼æ¢æï¼ | é¡¶æ ãAI å¯¼å ¥ãæé® â å¼¹çªéæä»¶ â è¯å« â é¢è§ â ãçæå°ç»å¸ã | å·²æ¥å ¥ | | 模æ¿å表页 | æ¬è½®**ä¸å**ãAI æ°å»ºæ¨¡æ¿ãå ¥å£ï¼è§ã注æäºé¡¹ã | ä¸å | ## ä¸å¡æµç¨ä¸æ°æ®å¸¦å ¥ 1. **è¿å ¥è®¾è®¡å¨**ï¼å¿ 须带 `templateId` è¿å ¥ï¼`/qc/report-template/designer?id=5`ï¼ã AI å¯¼å ¥åªå¨æ¢ææ¨¡æ¿ç设计å¨å ï¼`templateId` æ¯åç»ãç»éä»¶ãä¸ãæ¥å¿ççãç主é®ã 2. **ä¸ä¼ **ï¼ç¨æ·éæä»¶åè° system æ¨¡åæ¢æçä¸ä¼ æ¥å£æ¿ `blobId`ã **å¿ é¡»ä¿æç¨æ·çéæ©é¡ºåº**ââå端æè¿ä¸ªé¡ºåºçè§£ææ¡£é¡µåºã**è¿ä¸æ¥ä¸åè¯å«è¯·æ±**ï¼ç¨æ·ç¹å¤´æåã 3. **ç»éä»¶**ï¼æ¿å° `blobId` å**ç«å³**è° system æ¨¡åæ¢æçéä»¶ç»å®æ¥å£ï¼ææä»¶å½å±å°å½åæ¨¡æ¿ ï¼`application='file'`ã`recordType='qc_report_template'`ã`recordId=templateId`ï¼ã â `application` åå端æä¸¾éå¶ï¼**åªè½æ¯ `file` / `image` / `avatar`**ï¼ä¼ å ¶å®å¼ä¼ 500ã **å ç»åè¯å«**ï¼è¯å«å³ä½¿å¤±è´¥ï¼æä»¶ä¹å·²å½å±å°æ¨¡æ¿åä¸ï¼ä¸ä¼çææ²¡äººè®¤é¢çå¤å¿æä»¶ã 4. **è¯å«**ï¼è°æ¬ææ¡£ç `draft` æ¥å£ï¼å¸¦ä¸ `blobIds`ã`templateId`ã`schemaVersion`ã`catalog`ãå¯éç `hint`ã **è¿æ¡è¯·æ±è¦åç¬æ¾å®½è¶ æ¶å° 180 ç§**ï¼å次æé¿è¦å 5 次大模åè°ç¨ï¼é»è®¤è¶ æ¶ä¼å¨æ¨¡åè¿ååå°±ææï¼ã 5. **é¢è§**ï¼å¼¹çªåè¯å«åºçç»ä»¶ï¼ç±»åæ¾ç¤ºå + å·²è¯å«ç屿§ï¼ã`summary`ã`warnings`ã è¿ä¸æ¥**没æä»»ä½åæä½**ï¼ç¨æ·å¯ä»¥åå¤éæ°ä¸ä¼ ã 6. **çæå°ç»å¸**ï¼ç»å¸é空æ¶å å¼¹æ¿æ¢ç¡®è®¤ï¼**æ¿æ¢åæ æ³ç¨æ¤éæ¢å¤**ï¼â åç«¯è£ é 卿è稿ç¼è¯æç»å¸æ°æ®è£ è¿å»ã 7. **ä¿å**ï¼äººå·¥å¨è®¾è®¡å¨è°æ´åç¹**æ¢æç**ãä¿åãâ èµ°æ¢æççæ¬ä¿åæ¥å£ã **è¿æ¯å¯ä¸çè½åºç¹ï¼AI è¿æ¡è·¯æ²¡æç¬¬äºæ¡ä¿åè·¯å¾**ï¼å æ¤æ¢æçæ¨¡æ¿æ ¡éªï¼å«å¤å®è§åï¼èªå¨çæã **å¸¦å ¥å ³ç³»è¦ç¹** - `templateId` æ¯ä¸»çº¿ï¼ä»æ¨¡æ¿åè¡¨å¸¦å ¥è®¾è®¡å¨ãåå¸¦å ¥ AI å¯¼å ¥ä¸éä»¶ç»å®ã - `catalog`ï¼ç§¯æ¨æ¸ åï¼**æ¯æ¬¡è¯·æ±é½ä»å½åå端注å表ç°ç®ä¸éåä¼ **ï¼ä¸è¦å¨åºç¨éç¼åæå¸¸éï¼ ç¼åå®çäºæãæ³¨å表æ¯å¯ä¸çç¸æ¥æºãè¿æ¡åæææä½åºã - `blobIds` ç顺åºå³ææ¡£é¡µåºï¼å¤é¡µæä»¶æè¿ä¸ªé¡ºåºé页è¯å«ãæååå¹¶å»éã ## API | æ¹æ³ | è·¯å¾ | 说æ | æéç | |---|---|---|---| | POST | `/qc-report/ai-import/draft` | è¯å«æä»¶çææ¨¡æ¿è稿ã**忥è¿åãä¸è½åºãä¸å»ºçæ¬è¡** | `qc-report:template:ai-import` | > â æ¬è½®**æª**å¾ `system_menu` æå ¥ `qc-report:template:ai-import` æéè¡ï¼ä¸åºä»¶æ¥å£åä¸å£å¾ï¼ã > å½å `admin` æ¯è¶ 管ãç´æ¥æ¾è¡ï¼èè°ä¸åå½±åï¼**åç«¯é¡µé¢æ£å¼è½å°æ¶éä¸å¹¶è¡¥èå䏿鮿é**ï¼ > å¦åéè¶ ç®¡è§è²è°æ¬æ¥å£ä¼ 403ã ### 请æ±åæ° | åæ° | ç±»å | å¿ å¡« | 说æ | |---|---|---|---| | `blobIds` | Long æ°ç» | **æ¯** | ä¸ä¼ æä»¶å¯¹åºç blobIdã**顺åºå³ææ¡£é¡µåº**ï¼æå¤ 3 个ï¼è¯¥ä¸éç±å端 `yudao.qcreport.ai-import.max-files` å³å®ï¼è¶ 鿥 `1_070_104_005`ï¼ï¼å个æä»¶æå¤§ 20MB | | `templateId` | Long | **æ¯** | ç®æ 模æ¿ç¼å·ï¼ç¨äºæ¥å¿ççä¸éä»¶å½å± | | `schemaVersion` | String | **æ¯** | åºå®ä¼ `1.1`ãä¸è®¾è®¡å¨åæºï¼ä¸¤ç«¯ä¸ä¸è´æ¶åç«¯ç´æ¥æ¥é | | `catalog` | Object æ°ç» | **æ¯** | å¯ç¨ç»ä»¶ç§¯æ¨æ¸ åï¼è§ã`catalog` çæé æ¹å¼ãï¼æå¤ 40 项 | | `hint` | String | å¦ | ç¨æ·è¡¥å 说æï¼ä¼æ¼è¿æç¤ºè¯ï¼ä¾å¦ãè¿æ¯ IQC æ¥ææ£éªæ¥åã | **`catalog` ä¸é¡¹çç»æ** | åæ®µ | ç±»å | å¿ å¡« | 说æ | |---|---|---|---| | `type` | String | **æ¯** | ç»ä»¶ç±»åï¼å¦ `QualityTable`ã**大å°åææ**ï¼æ¨¡åä¼åæ ·æè¿ç»æ | | `label` | String | å¦ | ç»ä»¶æ¾ç¤ºåï¼ä¾æ¨¡åçè§£è¯ä¹ | | `category` | String | å¦ | ç»ä»¶åç±»ï¼å¦ `basic` / `data` / `result` | | `hint` | String | **æ¯** | 该ç»ä»¶ç**éå说æ**ï¼ç¨æ¥æ¾ä»ä¹ãä¸è¦ç¨æ¥æ¾ä»ä¹ã模åéå以å®ä¸ºåï¼è§ãéåæå¼å¿ 须鿏 å带åºã | | `fields` | Object æ°ç» | å¦ | 该ç»ä»¶å 许填åç屿§ | > `hint` æ 为**æ¯**䏿¯å 为å端强å¶ï¼å端æ¶å°ç©ºå¼ä¸æ¥éï¼ï¼èæ¯å 为**æ¼å¸¦å® = 模åéåçç**ï¼ > ç»ä»¶ä¹é´å¤§éå¯äºç¸æ¿ä»£ï¼æ¥åæ¬å¤´å¯å `Heading` / `Text` / `ReportHeader`ï¼ï¼ > 模ååªç䏿 `label` æ¶ä¼æå段æç®åãæéç¨çé£ä¸ªã宿µæ¼å¸¦åç表ç°å°±æ¯ > ã页ç页èä½ç½®ç¨äºææ¬ç»ä»¶ãã **`catalog[].fields[]` ä¸é¡¹çç»æ** | åæ®µ | ç±»å | å¿ å¡« | 说æ | |---|---|---|---| | `key` | String | **æ¯** | 屿§ keyï¼å¦ `itemsPath` | | `label` | String | å¦ | åæ®µæ¾ç¤ºå | | `type` | String | å¦ | åæ®µç±»åï¼å¦ `text` / `number` / `boolean` / `enum` | | `required` | Boolean | å¦ | æ¯å¦å¿ å¡«ï¼**åªç½®çä¸ç½®å** | | `bindable` | Boolean | å¦ | æ¯å¦æ¯ææ°æ®ç»å®ï¼**åªç½®çä¸ç½®å** | | `enumOptions` | Object æ°ç» | å¦ | æä¸¾å¯éå¼ï¼å ç´ å½¢å¦ `{ label: '横å', value: 'landscape' }`ã**ä» å¨ `type=enum` æ¶ææä¹** | ### ååºå段 | åæ®µ | ç±»å | 说æ | |---|---|---| | `components` | Object æ°ç» | è¯å«åºçç»ä»¶èç¨¿ï¼ææ¥åä»ä¸å°ä¸ç顺åºï¼å·²åè¿åå¹¶å»éä¸éæ³é¡¹è¿æ»¤ | | `components[].type` | String | ç»ä»¶ç±»åï¼**ä¿è¯æ¯ `catalog` é声æè¿ç type** | | `components[].props` | Object | ç»ä»¶å±æ§ãæ¸ åéæªå£°æç key 已被åç«¯ä¸¢å¼ | | `page` | Object | AI çæµççº¸å¼ é ç½®ï¼**å¯è½ä¸ºç©º**ï¼ä¸ºç©ºè¡¨ç¤ºæ²¿ç¨å½å模æ¿ççº¸å¼ ï¼ | | `page.size` | String | `A3` / `A4` / `A5` / `Letter` | | `page.orientation` | String | `portrait` / `landscape` | | `page.margin` | Object | `top` / `right` / `bottom` / `left`ï¼åä½ mm | | `summary` | String | AI 对è¿ä»½ææ¡£çä¸å¥è¯è¯´æï¼å±ç¤ºå¨é¢è§å¼¹çªé¡¶é¨ | | `warnings` | String æ°ç» | 软æç¤ºï¼è¢«è·³è¿çé¡µãæ æ³è¡¨è¾¾çåºåã被丢å¼çæªç¥ç»ä»¶ã**å件带页ç/页è已被æ è®°å¹¶å ¥è¯å«å 容**ç | | `durationMs` | Long | è¯å«æ»èæ¶ï¼æ¯«ç§ï¼ï¼å«å¤æ¬¡æ¨¡åè°ç¨ | **ååºç¤ºä¾** ```json { "code": 0, "data": { "components": [ { "type": "ReportHeader", "props": { "title": "æ¥ææ£éªæ¥å" } }, { "type": "QualityTable", "props": { "itemsPath": "inspectionItems" } }, { "type": "Result", "props": { "conclusion": "{{report.conclusion}}" } } ], "page": { "size": "A4", "orientation": "portrait" }, "summary": "è¿æ¯ä¸ä»½ IQC æ¥ææ£éªæ¥åï¼å«è¡¨å¤´ãæ£éªé¡¹ç®è¡¨æ ¼ä¸ç»è®ºæ ã", "warnings": ["第 2 页çå¤å®è§åè¯´ææ æ³ç¨å¯ç¨ç»ä»¶è¡¨è¾¾ï¼å·²ä¸¢å¼"], "durationMs": 12400 } } ``` > â **ååºé没æ `rawText` åæ®µ**ï¼è¿æ¯ææçï¼å®å ¨è§£æä¸åºç»ææ¶æ¥å£ç´æ¥æé > ï¼`1_070_104_012`ï¼ï¼ååºä½æ¯éè¯¯ç»æè䏿¯æ¬ååºï¼å¡ä¸ä¸ªæä¸ºç©ºçåæ®µåªä¼è®©äººè¯¯ä»¥ä¸º > ãè§£æå¤±è´¥æ¶åç«¯è½æ¿å°æ¨¡ååæããæ¨¡ååæåè¿äºæå¡ç«¯æ¥å¿ã ## `catalog` çæé æ¹å¼ `catalog` 䏿¯æåç常é表ï¼èæ¯**ä»è®¾è®¡å¨å®é 使ç¨çç»ä»¶æ³¨å表ç°ç®åºæ¥çæ´¾çè§å¾**ã | è¦ç¹ | 说æ | |---|---| | **æ¥æº** | 设计å¨å®é 使ç¨çé£ä¸ªç»ä»¶æ³¨å表ï¼`src/components/quality`ï¼ï¼å®æ¯ç»ä»¶æ¸ åç**å¯ä¸çç¸æ¥æº** | | **使¶ç®** | æ¯æ¬¡åèµ·è¯å«è¯·æ±åç°ç®ä¸é | | **å¿ é¡»å 廿³¨å** | 注å表**åªææ¾å¼æ³¨åè¿ææå 容**ã设计å¨åå§åæ¶å·²åè¿ä¸æ¬¡ï¼å¼¹çªéåè¡¥ä¸æ¬¡æ¯å¹ççå åº | | **奿ä»ä¹** | å¥ç¦»ä¸å¯åºååçå®ç°æåï¼å¾æ ãæå»ºå½æ°ãæ ¡éªå½æ°ãé»è®¤å¼ï¼ï¼åªçãæ¨¡åéè¦ç¥éçãï¼ç±»åãæ¾ç¤ºåãåç±»ã**éå说æ**ãåæ®µ | | **为ä»ä¹ä¸å¨å端硬ç¼ç ** | å端æä¸ä»½å°±æ¯ç¬¬äºä¸ªçç¸æ¥æºï¼ä¸æ¯æåçï¼å端å ç»ä»¶/æ¹å¿ å¡«åæ®µæ¶å端ç«å»çæãåç«¯å¯æ¬éé»è¿æï¼æ¨¡åéå³ä¼ç¼åºæ³¨åè¡¨éæ ¹æ¬ä¸åå¨ç `type` | | **è¾¹ç为ä»ä¹æ²¡è¢«åå¼±** | æ¸ åè¿æç¤ºè¯åçå¯ä¸åºå£æ¯ãè¢«æ¨¡åææ JSON å符串ããçæ£æå ³çæ¯**åç«¯è£ é å¨å¯¹ç活注å表æ¥**ï¼æªæ³¨åç `type` ä¼è¢«è·³è¿ï¼**æ°¸è¿äº§ä¸åºä»»æ HTML** | | **åç½®æ ¡éª** | 注åè¡¨ä¸ºç©ºæ¶æé 彿°**ç´æ¥æé**ï¼ä¸è¦åä¸ä¸ªç©ºæ¸ ååºå»ââå端ä¼å¤ä¸ºå ¥åéæ³ï¼æ¥åºæ¥çéåçå®åå éäºå¥½å å± | ### éåæå¼å¿ 须鿏 åå¸¦åº `hint` æ¯**ç»ä»¶éåçå¯ä¸çç¸æ¥æº**ï¼åç»ä»¶å®ä¹åæºï¼æ¹æ³¨å表éæä¸ªç»ä»¶ç `hint`ï¼ä¸æ¬¡è¯·æ±èªå¨çæï¼ å端ä¸éè¦ï¼ä¹ä¸åºè¯¥ï¼åæ¥ä»»ä½ä¸ä»½å¯æ¬ã | è¦ç¹ | 说æ | |---|---| | **åå¨åª** | æ¯ä¸ªç»ä»¶å®ä¹ç䏿¡ `aiHint` ææ¡ï¼éæ¸ ååºååå¸¦åº | | **åä»ä¹** | ãç¨æ¥æ¾ä»ä¹ã+ã**ä¸è¦**ç¨æ¥æ¾ä»ä¹ããåè ææ¯å ³é®ââæ¨¡å误ç¨çæ ¹å æ¯ãä¸ç¥éæä¸ªéç¨ç»ä»¶ä¸è¯¥ç¨å¨è¿éã | | **æä¹ç¨** | å端åªè´è´£æå®åæ ·åºååè¿æç¤ºè¯ï¼ä¸æ¡æ½åè§åè¦æ±æ¨¡åãé忶以 hint 为åï¼æ¸ åéåå¨ä¸é¨ç»ä»¶å°±ä¸è¦éèç¨éç¨ç»ä»¶åæ°ã | | **ä¸åä¼ææ ·** | 模ååªçå°ç»ä»¶ä¸æåï¼ä¼æå段æç®åçé£ä¸ªã宿µçç¶å°±æ¯ï¼æ¥åæ¬å¤´ä¸è½æ¬¾å ¨è½å°äºéç¨ææ¬ç»ä»¶ä¸ | | **å设计å¨çé¢çå ³ç³»** | `hint` **ä¸è¿è®¾è®¡å¨çé¢**ï¼å®æ¯ç»æ¨¡åçç说æï¼ä¸æ¯ç»ä»¶çå±ç¤ºåæå±æ§æç¤º | ## è稿 â ç»å¸ï¼ç¼è¯å¥çº¦ è稿**ä¸è½ç´æ¥å¡è¿ç»å¸**ãå®è¦å ç»å端çè£ é å¨ç¼è¯æç»å¸æ°æ®ï¼å交ç»è®¾è®¡å¨è½½å ¥ï¼ä¸ãæå¼ä¸ä»½å·²ä¿åçæ¨¡æ¿ãèµ°å®å ¨å䏿¡è·¯ï¼ã **è£ é å¨ï¼`assembleQualityCanvas`ï¼å ¥å / åºå** | æ¹å | åå | ç±»å | 说æ | |---|---|---|---| | å ¥ | `components` | è稿ç»ä»¶æ°ç» | å³ååºéç `components`ï¼å¯ä¸ºç©º | | åº | `grapes` | Object | ç»å¸æ°æ®ï¼åç»è®¾è®¡å¨çè½½å ¥æ¹æ³ | | åº | `skipped` | Object æ°ç» | 被跳è¿çç»ä»¶ï¼`{ type, label, reason }`ã**`reason` æ¯ç»äººçç䏿ï¼ç´æ¥å±ç¤º** | | åº | `problems` | String æ°ç» | è£ è¿å»äºä½æé®é¢ï¼å¿ å¡«ç¼ºå¤±ãæ¶å°ç»æåæ°æ®èåè½é»è®¤å¼ç | **é项è¡ä¸ºï¼ä¸æ¢æãä»ç»å¸æ¶éè¯ä¹ãäºä¸ºéè¿ç®ï¼** | æ åµ | è¡ä¸º | |---|---| | `type` åä¸å°æ³¨å表éçå®ä¹ | è¿ `skipped`ï¼**ç»§ç»å¤çä¸ä¸ä¸ª**ï¼ä¸æ´æ¹å¤±è´¥ | | `type` 为空 / 缺失 | åä¸ï¼è¿ `skipped` | | AI æ¼äºæäºå段 | ç¨ç»ä»¶å£°æçé»è®¤å¼è¡¥é½ï¼ä¸ä¼åºç°ãç»ä»¶ç¼ºå±æ§ãï¼ | | AI ç¼äºæ¸ åå¤ç屿§ key | **éé»ä¸¢å¼**ï¼å±æ§åºåååªè¾åºå£°æè¿ç keyï¼ | | æä¸ªå段æ¶å°å¯¹è±¡ / æ°ç» | è®°ä¸æ¡ `problems`ï¼è¯¥å段æé»è®¤å¼å¤çï¼é²æ¢ `[object Object]` 被åè¿ç»å¸ï¼ | | å¿ å¡«åæ®µä¸ºç©º | è®°ä¸æ¡ `problems`ï¼**ç»ä»¶ä»ç¶è£ è¿ç»å¸**ï¼ç¨æ·å¯ä»¥å¨è®¾è®¡å¨éè¡¥ï¼ | | ä¸ä¸ªç»ä»¶é½è£ ä¸è¿å» | **ä¸å¨ç»å¸**ï¼åªæ¥éãè½½å ¥å¨ä½ä¼æ¸ 空æ¤éæ ä¸å·²æå 容ï¼ç½ç½æè¿å»ä¸çç»å¸å´ä»ä¹é½æ¢ä¸æ¥ | **è½½å ¥å¨ä½ç两个å¯ä½ç¨ï¼å¿ é¡»è®©ç¨æ·ç¥æ ï¼** - **æ¸ ç©ºæ¤éæ **ï¼è£ å®ä¹å**æ æ³ç¨æ¤éæ¢å¤**忥çç»å¸ãæä»¥ç»å¸é空æ¶è¦å 弹确认ï¼å¹¶å»ºè®®ç¨æ·å ä¿åå½åå 容ã - **ä¼ç¨ä¼ å ¥çå¤å®è§åè¦çå½åè§å**ï¼è£ é è·¯å¾**æ¾å¼æ²¿ç¨å½åå·²ç¼è¾çè§å**ï¼ä¸ä¼æç¨æ·å好çå¤å®è§åé黿¸ 空ã > **è£ é 产ç©ä¸è½ç´æ¥è½åºã** åºå `grapes` éæ¯ä¸ªèç¹ç `components` 仿¯**æªè§£æç HTML å符串**ââå®å¿ é¡»å è¿ç»å¸ï¼GrapesJS 伿宿æèç¹æ ï¼æè½ä¿åãç»è¿è®¾è®¡å¨ç´æ¥æè¿ä¸²åè¿ `schema_json`ï¼å端渲æå¨ä¸ä¼è§£æå®ï¼åºä»¶æ¶ç»ä»¶ä¼æ¸²æç©º divã > > åçï¼è¥è¦ç¨åºåå°æç»ä»¶æ¾è¿ç»å¸ï¼**è¦æèç¹å¯¹è±¡äº¤ç»ç»å¸ï¼ä¸è¦èªå·±æ¼ HTML 串åè§£æ**ï¼HTML è§£æå¨ä¼æå±æ§å转å°åï¼`data-qc-itemsPath` â `data-qc-itemspath`ï¼ï¼å±æ§é¢æ¿ä¸æ¸²æé½ä¼ç»ä¸ä¸ã设计å¨å é¨èµ°çå°±æ¯ãç´æ¥ä¼ 对象ãè¿æ¡è·¯å¾ã ## AI å¯å¡«ç模æ¿ç»æå±æ§ï¼åå®ä¹ / 表头跨å / æ ·ååæ®µï¼ åä»¶çæ£éªè¡¨**å**常常æ¯ç»ä»¶é»è®¤åå¤ï¼ãæ£æµæ¹æ³ããæ åè¦æ±ããç»è®ºãçï¼ï¼æ ·åä¿¡æ¯åçåæ®µä¹å¸¸ä¸é»è®¤å 项ä¸åãä¸ºæ¤æ°å¢ä¸ä¸ªå¯å¡«å±æ§ï¼AI 伿åä»¶çåä¸å段åè¿å»ã**è¿ä¸ä¸ªå±æ§é½æ¯çº¯ææ¬å符串ï¼ä¸æ¯æ°ç»**ââ屿§å¼å¨ç»å¸ä¸æ¯æ éï¼æ°ç»åä¸è¿å»ã | 屿§ | æå¨åªä¸ªç»ä»¶ | ä½ç¨ | |---|---|---| | `columns` | åç»æ£éªé¡¹è¡¨ | è¦ç该表çåï¼åæ é¢ + æ¯åç»å®çæ°æ® | | `headerSpans` | åç»æ£éªé¡¹è¡¨ | 表头åä¸ä¸ä¸¤å±æ¶ï¼æè¿°**ä¸é¢é£ä¸å±** | | `fields` | æ ·åä¿¡æ¯ | è¦çæ ·åä¿¡æ¯åå±ç¤ºçåæ®µ | ### `columns` çåæ³ ä¸è¡å符串ï¼åä¸åä¹é´ç¨ `|` åéï¼æ¯ååä½ãåæ é¢=ç»å®è¡¨è¾¾å¼ãï¼**å¨ç¬¬ä¸ä¸ª `=` å¤åå**ï¼æä»¥åæ é¢éä¸è½åºç° `=`ï¼ã - åæ é¢åå `#` = 该åè¦**纵ååå¹¶è·¨ä½æ´ç»**ï¼æ¾ç»åçé£ä¸åï¼ï¼ä¸è¡¨åªæä¸å该å ã - **å¡«äº `columns` æ´è¡¨å°±æå®æ¥**ï¼ãæ¾ç¤ºåºå· / æ¾ç¤ºæ£æµè¦æ± / æ¾ç¤ºå®æµå¼ / æ¾ç¤ºåä½ / æ¾ç¤ºå¤å®ãè¿äºå¼å ³å ¨é¨å¤±æï¼å®ä»¬åªç®¡é»è®¤åï¼ã - åæ é¢ä¸ç»å®è¡¨è¾¾å¼é½ä¼å HTML 转ä¹ï¼ç¨æ·åä»ä¹é½ä¸ä¼åæå¯æ§è¡çæ ç¾ã **å¯ç¨çç»å®è·¯å¾æ¯ç½åå**ï¼åç½ååå¤çè·¯å¾ä¼å¨æ ¡éªæ¶æ¥éï¼`{{index}}` è¡åºå·ã`{{item.itemName}}` æ£éªé¡¹ç®åã`{{item.group.childName}}` å项åã`{{item.checkMethod}}` æ£æµæ¹æ³ã`{{item.requirement}}` æ£æµè¦æ±ã`{{item.standardValue}}` æ åå¼ã`{{item.actualValue}}` 宿µå¼ã`{{item.unit}}` åä½ã`{{item.resultText}}` å¤å®ç»è®ºã`{{item.remark}}` 夿³¨ã **ç»åç»è¡¨å `columns` æ¶å¿ é¡»ä¿çãå项ãå**ï¼`å项={{item.group.childName}}`ï¼ï¼ä¸å®ä¸å `#`ãåç»è¡¨çé»è®¤åæ¬æ¥å°±æè¿ä¸åï¼AI æååå ¨æ¶æå®¹ææ¼æå®ââ䏿¼ï¼å项忴å仿¥å䏿¶å¤±ï¼èå项å¾å¾ææ¯æ åçæ£èæ ¸ç对象ã宿µï¼ä¸åæè¿æ¡æ¶ 3/3 è½®é½æå项å丢æï¼åªååº 4 åï¼ï¼åæå 5/5 è½®ä¿çãä¸ä¸åä»¶ç 6 åéå对é½ã ### ã#ãé£ä¸åå°åºæä¹åå¹¶ç åå¹¶**ä¸å¨å端ç®ï¼ä¹ç»å¸ä¸çä¸è§**ï¼å ¨é¨ç±æ°æ®ä¸åãåºä»¶æ¶æçæï¼ | ç¯è | è°å | åä»ä¹ | |---|---|---| | æ åºåå¹¶å | æ¨¡æ¿ | `columns` éç»è¯¥åæ é¢å `#`ï¼ç»ä»¶æ®æ¤åªå¨è¿ä¸æ ¼å `rowspan` / `hidden` 两个ç»å® | | ç®åºåå¹¶å è¡ | å端 | ç»ç第ä¸è¡ä¸å `span = æ¬å该ç»çå®é è¡æ°`ã`hidden = 空串`ï¼ç»å å ¶ä½è¡ä¸å `span = 1`ã`hidden = hidden` | | 渲æè§è§åå¹¶ | å端 / Java 渲æå¨ | 两边é½å¨ãæ±å¼ç»æä¸ºç©ºä¸²ãæ¶ä¸è¾åºè¯¥å±æ§ãäºæ¯ç»å¤´è¡ç `hidden` æ¶å¤±ãé£ä¸æ ¼å¸¦ç `rowspan` æ¾ç¤ºï¼å ¶ä½è¡ç `hidden="hidden"` è®©æ´æ ¼ `display:none`ï¼æä½ç½®è®©ç»ä¸æ¹çåå¹¶æ ¼ | 䏿¡ç±æ¤æ¨åºçå£å¾ï¼ - **åå¹¶è¡æ°æ¯ãæ¬å该ç»å®é åºäºå è¡ãï¼ä¸æ¯ææ ä¸»æ°æ®é该åç»æäºå¤å°ä¸ªå项ã** æ¬åå°å½ä¸¤ä¸ªåé¡¹ï¼æ ¼åå°±åªè·¨é£å è¡ã - **该ç»å¨æ¬å没æåé¡¹è¡æ¶ä¸åå¹¶**ï¼å端æ `span = 1` ä¸åï¼ï¼ç¬ç«å½å ¥é¡¹åçââæ¬æ¥å°±åªæä¸è¡ï¼æ æè°è·¨è¡ã - **`hidden` åªè½é ã屿§å¨ä¸å¨ã表达**ï¼æä»¥è¿ä¸ªå¼å¿ é¡»æ¯å符串ï¼`hidden="false"` ç §æ ·éèï¼åªæç©ºä¸²è½è¡¨ç¤ºãä¸è¾åºè¯¥å±æ§ = è¿ä¸æ ¼å¯è§ããå端ä¸è¦æå®å½å¸å°å¼å¤çã ### èªå®åçå宽ï¼é¿ä¸²ä¸è®¸æè¡¨æ ¼æåºçº¸å¼ èªå®å**ä¸å¸¦ä»»ä½å®½åº¦å£°æ**ï¼é»è®¤å带ï¼åºå· 48pxãæ£æµè¦æ± 30%ã宿µå¼ 26%â¦â¦ï¼ï¼æä»¥æ´å¼ 表çå宽åªè½ç±ååç**æå°å 容宽度**忍ãèæ¥åéç弿¯ `{{item.standardValue}}` è¿ç§ä¸å«ç©ºæ ¼çé¿ä¸²ï¼æå°å å®¹å®½åº¦å°±æ¯æ´ä¸²é¿åº¦ââå åå èµ·æ¥è½»æè¶ è¿ A4 æ£æå®½åº¦ï¼æµè§å¨åªè½æè¡¨æ ¼æåºçº¸å¼ ï¼ææ«ä¸åè¢«æ¤æäºåå 个åç´ çç«æ¡ã ä¿®æ³ï¼**åªå¨èªå®åæ¶**ç»åå æ ¼å `overflow-wrap:anywhere`ãå®ä¼å䏿å°å 容宽度计ç®ï¼`break-word` ä¸ä¼ï¼ï¼åå®½å æ¤ä¸å被é¿ä¸²ç»æ¶ï¼åºä»¶æ¶å¼æ¬æ¥å°±çï¼ãçç®±å¹²ç¥æ³ãã13.2ãï¼ï¼æè¡æå°è§¦åã | åºæ¯ï¼A4 纵åï¼æ£æå®½ 672pxï¼ | ä¿®å | ä¿®å | |---|---|---| | èªå® 6 åï¼å¼ä¸ºå ä½ç¬¦ | æ´è¡¨ 810pxãæº¢åº 138pxãæ«å 29px | æ´è¡¨ 672pxã䏿º¢åºãæ«å 97px | | èªå® 6 åï¼å¼ä¸ºç宿°æ® | æ¬å°± 672px | 672pxï¼å宽åå¸ä¸å | | **é»è®¤å**ï¼åç»è¡¨ 7 åï¼ | 807pxãæº¢åº 135px | **ä¿æåæ ·** | æåä¸è¡æ¯å»æä¸ºä¹ï¼é»è®¤åç宽度声æå·²ç»å®ä½çé¢ï¼åå 䏿¡æè¡åèä¼è®©æ²¡å宽度çãæ£éªé¡¹ç®ããå项ã被æ¤å° 30 ä½åç´ ãè¡é«ä» 59px æ¶¨å° 283pxï¼å®æµï¼ãå æ¤è¿æ¡åªæå¨èªå®åä¸ï¼**没æ `columns` çå鿍¡æ¿äº§ç©éåä¸å**ï¼æ¢é以 sha256 å®çï¼ã > èªå®åç屿§æ¯éæ¬è½®ä¸èµ·è¿çï¼å鿍¡æ¿ä¸å·²åå¸çæ¬é䏿¡é½æ²¡æï¼å ¨åºå®æµ `data-qc-columns` / `data-qc-headerSpans` / `data-qc-fields` å½ä¸æ°å为 0ï¼ï¼å æ¤è¿æ¡æ¹å¨å¯¹æ¢ææ¥åé¶å½±åã ### `headerSpans` çåæ³ åæ ·æ¯ `|` åéçä¸è¡åç¬¦ä¸²ï¼æ¯æ®µåä½ãæ é¢^è·¨è¶åæ°ãï¼**卿åä¸ä¸ª `^` å¤åå**ï¼ä¸åè·¨è¶åæ°æ¶æ 1 ç®ã - **ä¸é¢é£ä¸å±ç± `columns` ååçæ é¢èªå¨æ¼åºï¼ä¸è¦éå¤å**ââ`headerSpans` åªæè¿°ä¸é¢é£ä¸å±ã - **åæ®µè·¨è¶åæ°ä¹åå¿ é¡»çäºåæ°**ã对ä¸ä¸æ¶æ´å±ä¼è¢«ä¸¢å¼ãéååå±è¡¨å¤´ï¼ä¸æ¥éãä¸å´©ï¼ï¼æä»¥è¿æ¡ä¸æ»¡è¶³æ¶ç¨æ·çå°çæ¯ã两å±è¡¨å¤´æ²¡çæãè䏿¯ä¸æ¡é误ã - åä»¶æå³è¾¹é£åãç»è®ºã/ãå¤å®ãå¨åä»¶éä¸å±äºä¸å±åç»æ 颿¶**è¦åç¬ææ®µ**ï¼`ç»è®º^1`ï¼ãä¸ç¹ææ¶æ¨¡åä¼å¾çäºæå®çåæ°å¹¶è¿ãæ£éªç»æãï¼åæ `æ£éªç»æ^6`ï¼ï¼è¡¨å¤´å°±æç»è®ºåç»å°æ£éªç»æåºä¸ã - **æåèªå·±æ¢æ¯åå忝䏿 ¼æ¶**ï¼å件鿝䏿 ¼çºµååå¹¶ãå æ»¡ä¸ä¸ä¸¤å±è¡¨å¤´ï¼æåææ¬é表ç°ä¸ºä¸å±è¡¨å¤´é£ä¸æ ¼åçãâåä¸ãï¼ï¼ä»ç §å䏿®µãè·¨è¶åæ°å 1ï¼ä½è¯¥æ®µæ é¢å¿ é¡»ä¸ `columns` éè¿ä¸åçåæ é¢**ä¸åä¸å·®**ã两è ç¸åæ¶æ¥å伿å®ååºä¸æ ¼è·¨ä¸¤è¡ï¼`rowspan`ï¼ï¼ä¸å±ä¸åéå¤åºè¿ä¸æ ¼ï¼ä¸ä¸è´æ¶ä¼è¢«å½æå¦ä¸å±çæ°åç»æ é¢ï¼ãç»è®ºãäºåå¨è¡¨å¤´ä¸å°ä¸¤éã - è¿æ¯æ¬è½®ä¿®æçä¸å¤ç缺é·ï¼æ¨¡åæ¤å 5/5 è½®é½å `ç»è®º^1`ãååä¹å«ãç»è®ºãï¼ä¸¤å±åå°ä¸æ¬¡ï¼ç¨æ·çå°çå°±æ¯ãå¤äºä¸ä¸ªç»è®ºããè¡¥ä¸ã两边ä¸åä¸å·® â åæä¸æ ¼è·¨ä¸¤è¡ãè¿æ¡å¤æ®åï¼ç´è¿ 5/5 è½®åæä¸ºä¸æ ¼ã - åä¾è§å®æµï¼æ 1/5 è½®æ¨¡åæ `columns` éé£ä¸åæ¹åæãå¤å®ãã`headerSpans` å´ä»åçåä»¶ä¸çãç»è®ºãï¼ä¸¤è¾¹å¯¹ä¸ä¸åéåå¤å°ä¸ä¸ªè¡¨å¤´ãæç¤ºè¯éå æ¤è¡¥äºä¸å¥ãä¸è¦èªä½ä¸»å¼ æå®æ¹åæè¿ä¹è¯ãå¹¶åæåæï¼å¤è· 5/5 è½®ä¸ååºç°ã ### `fields` çåæ³ è¯æ³ä¸ `columns` å®å ¨ç¸åï¼å段å=ç»å®è¡¨è¾¾å¼ï¼`|` åéï¼ï¼ä½**ä¸å 许 `#` åå¹¶æ è®°**ï¼é£æ¯åç»è¡¨æ ç»åçï¼ãæ¯è¡æ¾å ç»ä»ç±ãæ¯è¡å段æ°ã屿§æ§å¶ã ç»å®è¡¨è¾¾å¼åªè½åæ¥åä¸ä¸æéç¡®å®åå¨çåæ®µï¼ä¸ `columns` ååãä¸è®¸èªé é®ã约æï¼ï¼åä»¶éæãä¸ä¸æé没æçæ ç®ï¼å¦ãäº§åæ°éããåè±åç§ããçäº§æ¥æããæææ¥æãï¼ä¼è¢«ä¸¢å¼ï¼å¹¶å¨ `summary` é说æã åæ®µæ°ä¸æ¯ãæ¯è¡å段æ°ãçæ´æ°åæ¶ï¼**å件常è§**ï¼å¦åä»¶ 5 é¡¹ãæ¯è¡æ¾ 2 ç»ï¼ï¼æ«è¡ç±æåä¸ä¸ªå¼æ ¼æ¨ªåè·¨æä½ä¸çåï¼å³ä¾§ä¸ä¼çåºåªæè¾¹æ¡çç©ºæ ¼ãè¿æ¯æ¸²ææçåºå®è¡ä¸ºï¼**ä¸éè¦å¨ `fields` éå任使 è®°**ï¼ä¹ä¸ä¼å æ¤å¢ååæ®µæ ¼æ°ã ### ç»å¸æ¾ç¤ºä¸åºä»¶ç»æçå·®å¼ è®¾è®¡å¨ç»å¸**䏿±è§£ç»å®**ââ`{{item.group.childName}}` è¿ç±»å ä½ç¬¦åæ ·ççãç±æ¤æä¸å¤å¿ é¡»åç¬å¤çï¼åç»æ£éªé¡¹è¡¨çç»åæ ¼åæ `hidden="{{item.group.hidden}}"`ï¼å¨æµè§å¨ç¼éãæ `hidden` 屿§ãå°±æç«ï¼äºæ¯ç»å¸éè¿ä¸æ ¼è¢« `display:none`ï¼è¯¥è¡åé¢çæ ¼æ´ä½å·¦ç§»ä¸æ ¼ï¼æ«ååªå©è¡¨å¤´ãè¢«æ¤æç«æç»æ¡ââç»å¸ä¸çå°çæçä¸åºä»¶ç»æå¯¹ä¸ä¸ã 设计å¨ä¸ºæ¤åªå¨**ç»å¸ææ¡£å **æ³¨å ¥ä¸æ¡å¼å¯¼æ ·å¼ï¼æãå¼è¿åå¨å ä½ç¬¦ãç `hidden` è¿åæå¯è§æ ¼ãåºä»¶æ¶è¯¥å¼å·²è§£æç©ºä¸²æ `hidden`ï¼éæ©å¨ä¸åå½ä¸ï¼æ¥å渲æä¸ä¸ªåé½ä¸åå½±åï¼è¿æ¡æ ·å¼åªå å¨ç»å¸ iframe éï¼ä¸åè¿ Schemaãä¸é模æ¿ä¿åã å ä½ç¬¦å¸¦æ¥ç第äºå¤å·®å¼æ¯**宽度**ï¼å ä½ç¬¦æ¯çå¼é¿å¾å¤ï¼æä¸æå¾ç ´çº¸å¼ å¨ä¸¤ä¾§è¡¨ç°ä¸åãè¿ä¸å¤ä¸èµ°ç»å¸æ ·å¼ï¼èæ¯æ `overflow-wrap:anywhere` åè¿èªå®åçåå æ ¼æ ·å¼éââ两侧ç¨åä¸å¥è§åï¼æè§å³æå¾ï¼è¯¦è§ä¸ä¸èï¼ã ### è°å¨ä»ä¹æ¶åæ ¡éªè¿ä¸ä¸ªå±æ§ åªæ**ä¿å / åå¸å**ç屿§æ ¡éªä¼æ£æ¥å®ä»¬ï¼åå®ä¹è¯æ³ãè·¨åæ°å计ã`#` ç¨å¨æ ·åä¿¡æ¯ä¸çï¼ï¼è®¾è®¡è¿ç¨ä¸çä¸é´æä¸è§¦åãæ ¡éªä¿¡æ¯èµ°æ¢æç `problems` ééï¼**é黿**ï¼ï¼ç»ä»¶ä»ä¼è£ è¿ç»å¸ï¼ç¨æ·å¯ä»¥å¨è®¾è®¡å¨éæ¹ã > **å·²ç¥è¾¹ç**ï¼AI å¯¼å ¥è·¯å¾ä¼è·è¿å¥æ ¡éªï¼è®¾è®¡å¨é**æå·¥ç¼è¾**åç¹ä¿å**ç®åä¸è·**ï¼`validateQualityProps` åªå¨è£ é å¨é被è°ç¨ï¼ãä¹å°±æ¯è¯´æå·¥å¡«éçåå®ä¹ä¸ä¼å¨ä¿åæ¶è¢«æ¦ä½ï¼åªä¼å¨åºä»¶æ¶ä»¥ãç»å®åä¸å°å¼ãç形弿´é²ãè¿æ¯æ¢æç¼ºå£ï¼æ¬è½®æªæ¹ã ## åæ®µå±ç¤ºè§å | åæ®µ | å±ç¤ºä½ç½® | 说æ | |---|---|---| | `summary` | å¼¹çªé¡¶é¨ | æåè²æç¤ºæ¡ã为空æ¶ä¸å±ç¤º | | `warnings` | å¼¹çªä¸é¨ | è¦åè²æç¤ºæ¡ï¼éæ¡ `·` ååºã**å¿ é¡»å±ç¤º**ââ宿¿è½½ãæä»¶éæåªäºå 容没被è¯å«å°ã | | `components` | å¼¹çªä¸é¨å表 | æ¯è¡ï¼åºå· + ç±»åæ¾ç¤ºå + ç±»ååå§å¼ + å·²è¯å«ç屿§ï¼`åæ®µæ¾ç¤ºå=å¼`ï¼ãç±»ååä¸å°å®ä¹æ¶é¢å¤æãæªæ³¨åï¼å°è¢«è·³è¿ãæ è®° | | `components` æ¡æ° | åè¡¨ä¸æ¹ | ãè¯å«åº N 个ç»ä»¶ãï¼ææªæ³¨å项æ¶è¿½å ãM 个ç»ä»¶è®¾è®¡å¨ä¸è®¤è¯ãæ è®° | | `durationMs` | åè¡¨ä¸æ¹ | ãèæ¶ N ç§ã | | `page` | ä¸è¿å¼¹çª | å¹¶å ¥è®¾è®¡å¨ççº¸å¼ è®¾ç½®ååè½½å ¥ç»å¸ï¼è§ãä¸å¡è§åã | | `skipped` / `problems` | è½½å ¥åå¼¹çª | ãæ N å¤éè¦çæãï¼éæ¡ååº | ## ä¸å¡è§å说æ | åºæ¯ | è§å | |---|---| | **åªä½è稿ï¼ä¸è½åº** | æ¥å£ä¸å»ºæ¨¡æ¿ãä¸å»ºçæ¬è¡ãä¸åç»å¸ã**å¯ä¸çè½åºç¹æ¯è®¾è®¡å¨æ¢æçãä¿åã**ï¼æä»¥æ¢æçæ¨¡æ¿æ ¡éªï¼å«å¤å®è§åï¼èªå¨çæ | | **å¤é¡µé页è¯å«åå»éåå¹¶** | æ«æä»¶/ç §çæé¡µååºä¸æ¬¡è稿ï¼åæãç±»å + 屿§ãå®å ¨ç¸åå»éåå¹¶ã页çãæ é¢ã表头å¨å¤é¡µéå¤åºç°æ¶åªç第ä¸ä»½ | | **`.doc` / `.xls` æ§æ ¼å¼ä¸æ¯æ** | ç´æ¥æ¥ `1_070_104_003`ï¼æç¤ºå¦å为 `.docx` / `.xlsx`ã`.docx` / `.xlsx` / `.pdf` / å¾ç / `.txt` / `.csv` æ¯æ | | **ã空ç½ãä¸ãæåãå弿¥** | æ©å±ååæ³ä½å 容为空 â `1_070_104_005`ï¼æ©å±å忳使件æä¸å¼ï¼æåã带å¯ç ï¼ â `1_070_104_015`ã两è ç»ç¨æ·çå¨ä½å®å ¨ä¸åï¼ä¸è½åæä¸æ¡ | | **PDF èµ°åªæ¡ééç±ãææ²¡æææ¬å±ãå³å®** | çµåç PDFï¼æææ¬å±ï¼èµ°ææ¬ééï¼æ´ä»½ 1 次è°ç¨ï¼æ«æç PDFï¼æ ææ¬å±ï¼é页转å¾åæå¾è¯å« | | **è¡¨æ ¼åªè¯å«ç»æ** | è¡¨æ ¼ç**å ·ä½æ°æ®è¡ä¸ä¼åè¿æ¨¡æ¿**ï¼`QualityTable` çæ°æ®æºä¸å¾æ¯ `inspectionItems`ï¼ç宿°æ®å¨åºä»¶æ¶ç±ä¸å¡åæ®æä¾ã**è¿ä¸ç¹å¿ é¡»åç¨æ·è¯´æ**ï¼å¦åç¨æ·ä¼ä»¥ä¸ºæä»¶éçæ°æ®ä¹å¯¼è¿æ¥äº | | **åä»¶å/åæ®µä¸é»è®¤ä¸åæ¶æ¯è¦çï¼ä¸æ¯æ¢ç»ä»¶** | æ£éªè¡¨åæ¯é»è®¤å¤ï¼æ£æµæ¹æ³ / æ åè¦æ± / ç»è®ºâ¦ï¼â å `columns`ï¼è¡¨å¤´åä¸ä¸ä¸¤å± â å `headerSpans`ï¼æ ·åä¿¡æ¯å段ä¸é»è®¤å 项ä¸å â å `fields`ã**ä¸å¾ä¸æ¢ç»ä»¶**ââæ¢æå«çç»ä»¶åªä¼æå¤åºæ¥çå 容丢æãä¸è çè¯æ³ãç½åå䏿 ¡éªè§ä¸æãAI å¯å¡«ç模æ¿ç»æå±æ§ã | | **ç»å®è¡¨è¾¾å¼åæ ·ä¿ç** | æä»¶éåºç° `{{report.reportNo}}` è¿ç±»å ä½ç¬¦æ¶ï¼AI ä¼åæ ·ä¿çï¼ä¸ä¼æ¿æ¢æå®çå°çåé¢å¼ | | **AI ä¸å¾èªé ç»å®é®** | æç¤ºè¯æç¡®ç¦æ¢æ¨¡ååææ°ç `{{...}}` é®ï¼å¹¶ä¸**æå¯ç¨çé®å ¨éåäºåºæ¥**ï¼æ¥åçº§åæ®µåèªå端 `ReportFields`ï¼æ¥åå·ãæ ·ååãè§æ ¼ãæ¹å·ãæ£éªåãæ£éªæ¥æãç»è®ºãåæ ¼çã计æ°ç 23 个ï¼åå°çæã䏿åï¼ï¼æ£éªé¡¹å段åèªè¡¨æ ¼ç»ä»¶çç»å®è·¯å¾ã**åªç¦ç¼é ãä¸ç»å¯ééåæ¶æ¨¡ååªè½é ç**ââ宿µ 5/5 è½®é½ç¼åºäº `{{report.productionDate}}` / `{{report.expiryDate}}` è¿ç±»ä¸åå¨çé®ï¼æ¥åä¸çç½å¹¶è§¦åãç»å®åä¸å°å¼ãåè¦ | | **`.docx` ç Word 页ç/页èä¼è¢«æ è®°å¹¶å ¥å 容** | Word ææ¡£ç**页çä¸é¡µè**ï¼æ¯é¡µéå¤çå ¬å¸æ¬å¤´ãååçµè¯çï¼åæ¬ä¸å¨æ£ææµéï¼ç°å·²è¢«è¯»å并以 `[页ç]` / `[页è]` æ è®°æ¼è¿è¯å«å 容ï¼é¡µç卿£æåã页è卿£æåï¼ï¼åæ¶å¾ `warnings` éå 䏿¡æç¤ºã**æ«æä»¶/ç §çä¸åå½±å**ï¼å®ä»¬ç页çé¡µèæ¬æ¥å°±æ¯é¡µé¢åç´ çä¸é¨å | | **è¡¨æ ¼æ«è¡çè½æ¬¾ç¾ç½²è¡å±äºè½æ¬¾** | æ£éªå / å®¡æ ¸äºº / æ¹å人 / æ¥æè¿ç±»ç¾ç½²è¡**常æå¨æ£éªè¡¨æ ¼çæåä¸è¡**ãå®ä¸ç®ãå ·ä½æ°æ®è¡ãï¼åºè¢«ååºæ¥ä¸å件页èåææ¾è¿ `ReportFooter`ï¼`ReportFooter` ä¸ `QualityTable` 两æ¡éå说æéåèªåæäºè¿æ¡è¾¹çï¼ | | **ç¾ç½²è¡ç人å䏿¥æç空** | ç¾ç½²è¡**ä¿çæ ç®åãå¼ç空**ï¼å½¢å¦ãæ£éªåï¼\_\_\_\_ãå®¡æ ¸äººï¼\_\_\_\_ãæ¥æï¼\_\_\_\_ãï¼ï¼æå°åºæ¥ç±äººæå¡«ã人å䏿¥ææ¯æ¯ä¸ä»½æ¥ååèªçæ°æ®ï¼åæ»åä»¶ä¸çé£å 个人ä¼è®©æææ¥åå°æåä¸ä¸ªæ£éªå | | **è½æ¬¾å 许å¤è¡ï¼æ¢è¡è¦ççæè¡** | ç¾ç½²è¡ä¸å件页èåæä¸æ®µ**å¤è¡**ææ¬äº¤ç» `ReportFooter`ï¼å è¡å 容**å¿ é¡»æè¡æ¾ç¤º**ï¼ä¸è½ç³æä¸çã屿§å¼éä¿ççæ¯åå§æ¢è¡ï¼**æè¡åç卿¸²æäº§ç©è¿ä¸ä¾§**ââç»ä»¶çæçç»å¸ `content` 鿝 `<br>`ï¼æ¸²æå¼ææåæ ·è¾åºã`Text` ç»ä»¶åæ ·æ¯æå¤è¡ | | **å¤å®è§å䏿°æ®ç»å®ä¸ç± AI çæ** | AI åªåºç»ä»¶çéæå±æ§ï¼å¤å®è§åãæ°æ®ç»å®ä¸å¾ä¿çé»è®¤å¼ï¼ç±äººå·¥å¨è®¾è®¡å¨éé ç½® | | **çº¸å¼ çåå¹¶è§å** | AI çåºççº¸å¼ å¹¶å ¥å½å设计å¨ççº¸å¼ è®¾ç½®ï¼**æå段åå¹¶**ï¼AI 没çå°çåæ®µæ²¿ç¨ç¨æ·å½å设置ï¼ï¼åéè½½å ¥å¨ä½ä¸èµ·çæã**ä¸åæ¥çè¯ï¼ä¿åæ¶ä¼è¢«æ¨¡æ¿ä¿åæ¥å£ææ§å¼åå** | | **AI çä¸å°çº¸å¼ æ¶ä¸è¦ç** | `page` 为空æ¶**å¿ é¡»ä¼ å½å设计å¨ççº¸å¼ è®¾ç½®**ï¼ä¸è½ä¼ 空ãä¼ ç©ºä¼è®©è½½å ¥å¨ä½éåé»è®¤ A4ï¼æç¨æ·å·²ç»è°å¥½ççº¸å¼ æææ¹æ | | **ç»ä»¶æ°ä¸é** | è¯å«ç»ææå¤ 200 个ç»ä»¶ï¼è¶ åºæªæå¹¶å¨ `warnings` éæç¤ºè¡¥å½ | | **ä»»ä½å个ç»ä»¶åæé½ä¸å½±åæ´ä½** | æªç¥ç±»åãç»æå屿§ãå¿ å¡«ç¼ºå¤±é½åªè¿ `warnings` / `problems`ï¼å ¶ä½ç»ä»¶æ£å¸¸è¿åã**åªæãä¸ä¸ªç»ä»¶é½æ²¡è¯å«åºæ¥ãææ´ä½æ¥é** | | **åè½å¯è¢«è¿ç»´å ³æ** | é¨ç½²ç¯å¢ä¸è½åºç½æ¶ä¼å ³æï¼`yudao.qcreport.ai-import.enabled=false`ï¼ï¼æ¤æ¶æ¥å£æ¥ `1_070_104_000`ãå端åºåæ ·æç¤ºï¼ä¸è¦å½æç³»ç»å¼å¸¸ | ### ãæ£ææ¬å¤´/è½æ¬¾ãä¸ãæ¯é¡µéå¤ç页ç页èã䏿¯ä¸åäº è¿æ¡å£å¾å®¹ææ··æ·ï¼å端èè°æ¶å¡å¿ 对é½ï¼ | | ç»å¸ä¸ç `ReportHeader` / `ReportFooter` | æå¡ç«¯åºä»¶æ¶ç页ç页è | |---|---|---| | **ä½ç½®** | **æ£ææµé**ç顶鍿¬å¤´å / åºé¨è½æ¬¾åï¼éæ£æåªåºç°ä¸æ¬¡ | æå°æ¶**æ¯é¡µéå¤**ççå¼åºå | | **ç±è°çæ** | AI 仿件éè¯å«åºæ¥ãè£ è¿ç»å¸ï¼ç¨æ·å¯ç¼è¾ | æå¡ç«¯ PDF åºä»¶é ç½®ï¼`PdfPrintOptions`ï¼å çï¼ç®ååªæé¡µç | | **æ¬è½®æ¹å¨** | `.docx` ç Word 页çè¢«è¯»è¿æ¥åï¼æ¨¡å伿宿ªè¿æ£æé¡¶é¨ç `ReportHeader` | **æªæ¹å¨** | ä¹å°±æ¯è¯´ï¼æ Word 页ç认æ `ReportHeader`ï¼æ¯**æå®æ¬è¿æ£æé¡¶é¨**ï¼**䏿¯**è¿åæ¯é¡µéå¤çæå°çå¼ã çæ£æ¯é¡µéå¤çé£ä¸å±ä»ç±æå¡ç«¯ PDF åºä»¶é ç½®æ§å¶ã**ä¸è¦åå端æ¿è¯ºãå¯¼å ¥åä¼å¾å°æ¯é¡µéå¤ç页çã**ã ## 注æäºé¡¹ - **ä¸è¦ç¼å `catalog`**ï¼æ¯æ¬¡è¯·æ±ç°ç®ãçç±è§ã`catalog` çæé æ¹å¼ãã - **`catalog` æ¯é¡¹å¿ 须带 `hint`**ï¼éåæå¼æ¯å®å¯ä»¥è¢«æ¨¡åçè§çå¯ä¸åºå£ã注åè¡¨éæ°å¢ç»ä»¶æ¶ï¼ 该ç»ä»¶ç `aiHint` è¦ä¸å¹¶åå ¨ââæ¼ä¸æ¡ï¼é£ä¸ªç»ä»¶å°±åºæ¬ä¸ä¼è¢«æ¨¡åéä¸ã - **`blobIds` é¡ºåºææä¹**ï¼å¤é¡µæä»¶ææ°ç»é¡ºåºç解页åºï¼å端è¦ä¿åºï¼ä¸è¦ç¨æ åºéåæ¿è½½ã - **ä¸ä¼ ä¸è¯å«æ¯ä¸¤æ¥**ï¼ç¨æ·é宿件**ä¸å**è¯å«è¯·æ±ï¼çç¨æ·ç¹ãå¼å§è¯å«ãæåãä¸ä¼ å¨ AntDV çä¸ä¼ ç»ä»¶éè¦å å ³æå®å ç½®çèªå¨ä¸ä¼ ééï¼ç±æé®ç»ä¸è§¦åã - **ç»éä»¶è¦å äºè¯å«**ï¼å ç»åè¯å«ï¼è¯å«å¤±è´¥æä»¶ä¹å·²å½å±å°æ¨¡æ¿ï¼ä¸ä¼çå¤å¿æä»¶ã - **è¯å«è¦æ¾å®½è¶ æ¶**ï¼è¿æ¡è¯·æ±åç¬è®¾ 180 ç§ãç¨é»è®¤è¶ æ¶ä¼å¨æ¨¡åè¿åå就被ææï¼èä¸é误信æ¯çä¸åºæ¯è¶ æ¶è¿æ¯æ¨¡åæäºã - **ä¸è¦å¨å¼¹çªéåæè¯å«é误**ï¼å端已æã维度 + å®é å¼ + 坿§è¡å¨ä½ãç»äºç²¾ç¡®ææ¡ï¼ç±è¯·æ±å±ç»ä¸å¼¹åºå³å¯ï¼å¼¹çªéåå ä¸å±åèä¼çä½å®ã - **è½½å ¥æ¶è¦ä¼ å½åè§å**ï¼è£ é è·¯å¾å¦æä¸ä¼ å½åå·²ç¼è¾çå¤å®è§åï¼çäºæç¨æ·å好çè§åé黿¸ 空ã - **è½æ¬¾ç屿§è¾å ¥æ¡æ¯åè¡ç**ï¼å¯ç»å®å段ç»ä¸ç¨èªå®ä¹æ§ä»¶ï¼ä¸æ + åè¡è¾å ¥æ¡ï¼ã**AI å¯¼å ¥åºæ¥çå¤è¡è½æ¬¾å¼ä¼åæ ·ä¿çãä¹è½æ£ç¡®æè¡**ï¼ä½åªè¦ç¨æ·å»ç¼è¾é£ä¸ªè¾å ¥æ¡ï¼æ¢è¡å°±ä¼è¢«ææãéæ°ååä¸çãè¦åãå¨è®¾è®¡å¨éä¹è½æå·¥æ²å¤è¡è½æ¬¾ãï¼å¾æè¯¥æ§ä»¶çè¾å ¥æ¡æ¢æå¤è¡è¾å ¥æ¡ââ该æ§ä»¶è¢«å ¨é¨å¯ç»å®åæ®µå ±ç¨ï¼å±äºéè¦åç¬è¯ä¼°çæ¹å¨ã - **è½½å ¥ä¼æ¸ 空æ¤éæ **ï¼ç»å¸é空æ¶å 弹确认并建议å ä¿åï¼ææ¡è¦**æè¯´æ æ³ç¨æ¤éæ¢å¤**ã - **`skipped` / `problems` éæ¡å±ç¤º**ï¼ä¸è¦åªæ¥æ¡æ°ï¼ç¨æ·è¦ç §çæ¹ã - **æ¬è½®ä¸åãAI æ°å»ºæ¨¡æ¿ã**ï¼è®¾è®¡å¨å¿ 须带 `templateId` è¿å ¥ï¼AI å¯¼å ¥åªå¨æ¢ææ¨¡æ¿ç设计å¨å ãæ³åãä¸ä¼ æä»¶ç´æ¥é æ°æ¨¡æ¿ãéå 建模æ¿å£³ï¼æ¯å¦ä¸æ¡æµç¨ã - **æ¬è½®ä¸å AI èªå¨åå¸çæ¬**ï¼ä¹ä¸åæ¤éã - **æä»¶ç±»åå¨å端åªå峿¶çº¦æ**ï¼æ©å±åç½åå + ææ½åºæç¤ºï¼ï¼**æå¨æ ¡éªå¨å端**ï¼åç«¯å«æè¿éç 3 个æä»¶ä¸é彿å®å ¨è¾¹çã - **模æ¿å é¤ä¼è¿å¸¦æ¸ çéä»¶**ï¼æ¬è½®è¡¥é½äºãå 餿¨¡æ¿æ¶æ¸ çå ¶åä¸éä»¶ãçå¨ä½ï¼AI å¯¼å ¥çæºæä»¶ä¸ä¼å¨æ¨¡æ¿å é¤ååææ°¸ä¹ å¤å¿ãå端ä¸éè¦å¤çï¼ä½éªæ¶æ¶å¼å¾ç¡®è®¤ä¸æ¬¡ã ## é误ç | é误ç | æ¥æ | 触ååºæ¯ | |---|---|---| | `1_070_104_000` | AI å¯¼å ¥åè½æªå¯ç¨ï¼`yudao.qcreport.ai-import.enabled` å½å为 falseï¼ï¼è¯·è系管çåå¼å¯ååè¯ | è¿ç»´æå¼å ³å ³æäº `false` | | `1_070_104_001` | ä¸ä¼ çæä»¶ï¼blobId=Xï¼ä¸åå¨æå·²è¢«æ¸ çï¼è¯·éæ°ä¸ä¼ ååè¯ | `blobId` æ¥ä¸å°ãæä»¶å·²è¢«æ¸ çæç¼å·ä¼ é | | `1_070_104_002` | 䏿¯æçæä»¶ãXãï¼æ©å±åï¼Yï¼ãä» æ¯æ PDFãWord(.docx)ãExcel(.xlsx)ãå¾ç(png/jpg/jpeg/bmp/gif/webp) 䏿æ¬(txt/csv)ï¼è¯·è½¬æ¢æ ¼å¼åéæ°å¯¼å ¥ | æ©å±åä¸å¨ç½ååå | | `1_070_104_003` | 䏿¯æçæä»¶ãXãï¼æ§ç Office æ ¼å¼.Yï¼ãè¯·ç¨ Office æå¼åãå¦å为ã.docx æ .xlsx åéæ°å¯¼å ¥ | ä¸ä¼ äº `.doc` / `.xls` | | `1_070_104_004` | æä»¶ãXãå¤§å° YMBï¼è¶ è¿åæä»¶ä¸é ZMBï¼è¯·å缩ææååéæ°å¯¼å ¥ | è¶ è¿ `maxFileSizeMb`ï¼é»è®¤ 20MBï¼ | | `1_070_104_005` | æä»¶ãXãæªè§£æåºä»»ä½å¯ç¨å 容ï¼å ± N 页ï¼ãè¥æ¯æ«æä»¶ï¼è¯·ç¡®è®¤å辨çä¸ä½äº 200 DPIãæåæ 严é徿æé®æ¡ï¼æ¨èæ¹ç¨çµåç PDF æ Word/Excel | 空æä»¶ï¼ä¼ çä»¶çç¼ç ä¸åæ¯æçå¾çï¼**å 容为纯空ç½ç `.docx` / `.xlsx` / `.txt`**ã`.docx` / `.xlsx` æä¸å¼ï¼æåã带å¯ç ï¼èµ° `1_070_104_015`ï¼ä¸ä¼è½å°è¿é | | `1_070_104_006` | 䏿¬¡æå¤å¯¼å ¥ N 个æä»¶ï¼æ¬æ¬¡æäº¤äº M 个ï¼è¯·åæ¹å¯¼å ¥ | è¶ è¿ `maxFiles`ï¼é»è®¤ 3ï¼ | | `1_070_104_007` | æä»¶ãXãå ± N 页ï¼è¶ è¿å次è¯å«ä¸é M 页ï¼è¯·æåååæ¹å¯¼å ¥ | è¶ è¿ `maxPagesPerFile`ï¼é»è®¤ 5ï¼ã**ä¸é黿ªæ** | | `1_070_104_008` | ç»ä»¶æ¸ ååæ°ä¸åæ³ï¼åå ï¼ï¼è¯·å·æ°é¡µé¢åéè¯ | `catalog` 为空ã嫿²¡æ `type` ç项ãè¶ 40 项ãåºååè¶ 32KB | | `1_070_104_009` | ç»ä»¶æ¸ åç Schema çæ¬ä¸ºãXãï¼æå¡ç«¯åªæ¥åãYãï¼è¯·å·æ°é¡µé¢åéè¯ | `schemaVersion` ä¸è®¾è®¡å¨ä¸ä¸è´ï¼é常æ¯åç«¯å·²æ´æ°èå端æªåçï¼ | | `1_070_104_010` | AI è¯å«è°ç¨å¤±è´¥ï¼å·²èæ¶ Nmsï¼ï¼åå ã请å°ãAI 大模åãä¸ç¡®è®¤å¯¹è¯æ¨¡åå·²å¯ç¨ä¸å¯é¥ææï¼æç¨åéè¯ | 模åæå¡å¼å¸¸ãç½ç»ä¸éãå¯é¥æ æ | | `1_070_104_011` | AI è¯å«è¶ æ¶ï¼æ¬æ¬¡å·²ç N ç§ï¼ãæä»¶é¡µæ°ææ°éè¾å¤æ¶èæ¶æ´é¿ï¼è¯·åå°æä»¶æ°éï¼ææ¹ç¨çµåç PDF / Word / Excel | 模åæéææä»¶è¿å¤§ã**æ¥å£ä¾§è¶ æ¶é»è®¤ 60 ç§**ï¼å端 `yudao.ai.timeout`ï¼ä¸åç«¯é£æ¡ axios ç 180 ç§æ¯ä¸¤éç¬ç«çé¸ï¼ | | `1_070_104_012` | AI è¿åçå å®¹æ æ³è§£æä¸ºæ¨¡æ¿è稿ï¼å·²æ¶å° N 个å符ï¼ãè¥æä»¶å 容å¾é¿ï¼è¯·æåååæ¹å¯¼å ¥ï¼è¥æç»å¤±è´¥ï¼è¯·æ¹ç¨æå·¥è®¾è®¡ | 模åè¾åºä¸æ¯é¢æ JSONï¼è¢«æªæã夹带解éæåçï¼ãåå§è¾åºå·²åè¿æå¡ç«¯æ¥å¿ | | `1_070_104_013` | AI æªè½ä»æä»¶ãXãä¸è¯å«åºä»»ä½å¯ç¨ç»ä»¶ã请确认该æä»¶æ¯æ£éªæ¥åï¼è¥ç¡®å®æ¯ï¼è¯·æ¹ç¨æå·¥è®¾è®¡æ¨¡æ¿ | ä¸ä¸ªç»ä»¶é½æ²¡è¯å«åºæ¥ã**è¿æ¯å¯ä¸ä¸ä¸ªãæ´ä½å¤±è´¥ãçè¯ä¹** | | `1_070_104_014` | æ¬æ¬¡æäº¤çæä»¶å计 N 页ï¼è¶ è¿å次è¯å«ä¸é M 页ï¼åæä»¶ä¸é K 页ï¼ï¼è¯·åå°æä»¶æ°éæåæ¹å¯¼å ¥ | æ¯ä¸ªæä»¶é½æ²¡è¶ ãå èµ·æ¥è¶ `maxPagesPerRequest`ï¼é»è®¤ 8ï¼ | | `1_070_104_015` | æä»¶ãXãæ æ³ä½ä¸º .Y æå¼ï¼å 容已æåæåå¯ç ä¿æ¤ã请å ç¨ Office æå¼è¯¥æä»¶ï¼ç¡®è®¤è½æ£å¸¸æ¾ç¤ºåãå¦å为ã.docx / .xlsx åéæ°å¯¼å ¥ï¼è¥æä»¶ææå¼å¯ç ï¼è¯·å è§£é¤å¯ç ä¿æ¤ | `.docx` / `.xlsx` è§£ææå¼å¸¸ï¼POI æä¸å¼ï¼ã**ã空ç½ãä¸ãæåãæ¯ä¸¤åäºï¼æ¥æåå¼**ï¼æ¤åæåæä»¶è¯¯æ¥ `1_070_104_005`ï¼è®©ç¨æ·å»æ¥å辨çï¼ï¼å·²çº æ£ | > 注ï¼`templateId` **ä¸åå卿§æ ¡éª**ï¼å®åªç¨äºæ¥å¿ççä¸éä»¶å½å±ãå端å¡å¿ å ç¨çå®åå¨çæ¨¡æ¿ id è¿è®¾è®¡å¨ã docs/qc_report_configurable_columns_design.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,239 @@ # è´¨æ£æ¥åã表达å缺å£ã设计ï¼å¯èªå®åçæ£éªè¡¨ + å¯é åæ®µçæ ·åä¿¡æ¯ > ç¶æï¼**å¾ ç¡®è®¤**ãæ¬æåªåæ¹æ¡ï¼ä¸å¨ä»£ç ã > 触åèæ¯ï¼AI å¯¼å ¥å·²è½æ£ç¡®éå° `GroupedQualityTable`ï¼éåé®é¢å·²ä¿®ï¼è§å¼åè¿åº¦ §äºååï¼ï¼ > ä½ç¨æ·æ¯å¯¹åä»¶åæåºãè稿ä»ä¸ååä»¶ããç»æ ¸æ¥ï¼å©ä¸çå·®è·**å ¨é¨æ¯ãå¯ç¨ç§¯æ¨ç表达åä¸å¤ã**ï¼ > 䏿¯è¯å«é®é¢ââæå齿½åºæ¥äºï¼æ¯ç»ä»¶è£ ä¸ä¸ã --- ## 1. è¯æ®ï¼å·®è·å°åºå¨åª ç¨æ·åä»¶ãç¾äºæ¨¡ç.docxãï¼142373 åèï¼ä¸å½åè稿çé项对æ¯ï¼ | åä»¶éæç | è稿éçä¸åº | åå | |---|---|---| | è¡¨æ ¼åãæ£æµæ¹æ³ãï¼`GB 5009.3-2016` / `ANA.MTH-000034`ï¼ | **æ´å丢å¼** | `GroupedQualityTable` ç 7 ååæ»å¨ä»£ç 常éé | | è¡¨æ ¼åãç»è®ºãï¼`åæ ¼Checkout`ï¼ | æ´åä¸¢å¼ | åä¸ | | ä¸¤çº§è¡¨å¤´ãæ£éªç»æ \| ç»è®ºã | æå¹³ | ç»ä»¶åªæ¯æåå±è¡¨å¤´ | | æ ·åä¿¡æ¯ 7 项ï¼äº§ååç§° / äº§åæ°é / è§æ ¼ / çäº§æ¥æ / æ¹å· / åè±åç§ / æææ¥æï¼ | åªå© 6 项ï¼ä¸ä¸åä»¶ç 6 项**䏿¯å䏿¹** | `SampleInfo` çåæ®µåæ»å¨ä»£ç 常éé | | é¦è¡æ ç®å `产ååç§°Material Names` | æ¨¡åæ¿éç¨ç»ä»¶ç¡¬åæç¬ç«ä¸è¡ | 没æä»»ä½ç»ä»¶è½æ¿æ¥ã表头éçæ ç®åã | 两å¤ãåæ»ãçä½ç½®ï¼ - `mom-pro2-before/src/components/quality/inspection/grouped-quality-table.ts` ç `COLUMNS` 常éï¼7 é¡¹ï¼ - `mom-pro2-before/src/components/quality/header/sample-info.ts` ç `SAMPLE_FIELDS` 常éï¼6 é¡¹ï¼ --- ## 2. 两æ¡ç¡¬çº¦æï¼å è®²æ¸ æ¥ï¼å®ä»¬å³å®æ¹æ¡å½¢ç¶ï¼ ### 2.1 屿§åè®®åªè®¤æ éââä¸è½ç¨ãæ°ç»å±æ§ã `QualityProps = Record<string, boolean | number | string | undefined>`ï¼ `QualityFieldType` åªæ `boolean | enum | number | string | text` äºç§ã 屿§æç»è½å°ç»å¸èç¹ç `attributes` ä¸ï¼åä½ `data-qc-<key>`ï¼ï¼è `ReportTemplateSchema.QualityComponentNode.attributes` 乿¯å符串æ å°ã â **æ°å¢ä¸ä¸ªãåæ°ç»ã屿§ç±»åè¦å¨åè®®æ¬èº«**ï¼å±æ§é¢æ¿æ§ä»¶ãç»å¸åºååãschema å¥çº¦æµè¯ã 两侧渲æå¨ï¼ï¼ä»£ä»·è¿è¶ æ¶çãåå®ä¹å¿ é¡»è½è¢«ç¼ç æ**ä¸ä¸ªæ éå符串**ã ### 2.2 渲æå¨ä¸è®¤è¯ç»ä»¶ç±»åââä¸éè¦å¨æ¸²æå¨ è¿æ¯å¥½æ¶æ¯ï¼å·²æ ¸æ¥ï¼ - å端 `HtmlRenderer.renderAttributes` åªå两件ä¸ç»ä»¶æ å ³çäºï¼è·³è¿ `data-quality-type` / `data-qc-*`ã æ¦æ `on*` ä¸å±é©åè®®ï¼**没æä»»ä½æç»ä»¶ç±»å忝çé»è¾**ï¼ä¹æ²¡æå±æ§ç½ååã - å端 `CanvasSafety.validate` æ¯é»ååï¼`script` ç坿§è¡æ ç¾ãäºä»¶å±æ§ãæ´æ®µ CSSï¼ï¼ 䏿¯æ ç¾/屿§ç½åå â `colspan` / `rowspan` æ¬æ¥å°±æ¾è¡ã - è¡å¾ªç¯é `data-qc-repeat` / `data-qc-repeat-row` è¿ä¸¤ä¸ªéç¨å±æ§é©±å¨ã â **æ¬è½®ä¸éè¦æ¹ `HtmlRenderer`ã`BASE_CSS`ã`CanvasSafety`**ï¼ä¹ä¸éè¦æ¹ schema çæ¬å·ã ç»ä»¶ä¾§æ¹å¨éä¸å¨ã注å表éçä¸ä¸ª definitionããè¿ä¹æ£æ¯è´¨éç»ä»¶åè®®ç设计åè¡· ï¼`core/types.ts` å¼å¤´ï¼ãæ°å¢è´¨éç»ä»¶ï¼æ³¨åä¸ä¸ªæ° definitionï¼ä¸éè¦æ¹å¨ GrapesJS éæä»£ç ãï¼ã --- ## 3. æ¹æ¡ ### 3.1 æ»ååï¼ä¸æ°å¢ç»ä»¶ï¼ç»ç°æç»ä»¶å ãå¯éè¦çã屿§ **æ¨èåæ³**ï¼ç» `GroupedQualityTable` å 两个**å¯é**屿§ `columns` / `headerSpans`ï¼ç» `SampleInfo` å ä¸ä¸ª**å¯é**屿§ `fields`ã - 屿§**ç¼ºå¸æ¶è¡ä¸ºéåä¸å**ï¼`buildContent` éåè½å°ç°æå¸¸éï¼â åéåå·²å叿¨¡æ¿é¶å½±åï¼ ä¸éè¦æ°æ®è¿ç§»ï¼ä¸éè¦æ¹ `defaults`ã - **䏿°å¢ç¬¬ä¸ä¸ªæ£éªè¡¨ç»ä»¶**ãå½å 13 个ç»ä»¶éå·²ç»æä¸¤ä¸ªæ£éªè¡¨ï¼`QualityTable` / `GroupedQualityTable`ï¼ï¼ åå ä¸ä¸ªå°±æ¯ä¸ä¸ªåç±»ç«äºï¼**æå¯è½æåå修好çéåç¨³å®æ§å次æç ´**ï¼åè§ Â§äºååï¼ éåä¸ç¨³çæ ¹å å°±æ¯æç¤ºè¯éå¤äºä¸å¤æåå¹³éºè¡¨çæªè¾ï¼ç»ä»¶æ°éå¢å æ¯åä¸ä¸ªæºçï¼ã ç¨æ·çæ¶çæ¥èªãåè½æ¹ãï¼ä¸æ¯ãå¤ä¸ä¸ªç»ä»¶ãã ### 3.2 å/åæ®µæè¿°çè¯æ³ **åè¡å符串**ï¼åä¸åä¹é´ç¨ `|` åéï¼æ¯ååä½ `åæ é¢=ç»å®è¡¨è¾¾å¼`ï¼ ``` åºå·={{index}}|æ£éªé¡¹ç®={{item.itemName}}|æ£æµæ¹æ³={{item.checkMethod}}|æ åè¦æ±={{item.standardValue}}|宿µå¼={{item.actualValue}}|åä½={{item.unit}}|å¤å®={{item.resultText}} ``` 䏿¡çº¦æï¼é½æ¥èª 2.1ï¼ 1. **å¿ é¡»åè¡**ï¼è¿ä¸²ä¸è¥¿æç»æ¯ç»å¸èç¹çä¸ä¸ª HTML 屿§å¼ï¼æ¢è¡å¨å±æ§éæ¯åæ³ç使æè¢« ç¼è¾å¨/åºååç¯èæ¹åï¼ç»ä¸æåè¡å¤çã 2. **`=` åªä½ç¬¬ä¸ä¸ªåé符**ï¼æ é¢ä¸è¡¨è¾¾å¼é½ä¸å è®¸å« `=`ï¼è¡¨è¾¾å¼éç `{{...}}` åæ ·ä¿çã 3. **æ é¢éä¸å 许åºç° `|`**ï¼éè¦ç«çº¿æ¶ç¨å ¨è§ `ï½`ã **纵ååå¹¶åï¼åç»è¡¨ä¸ç¨ï¼**ï¼åæ é¢åå `#` 表示ãè¿ä¸åè¦è·¨ä½æ´ç»ãï¼æ²¿ç¨ç°æ `GroupedQualityTable` ç `rowspan` / `hidden` æºå¶ï¼ ``` #æ£éªé¡¹ç®={{item.itemName}}|å项={{item.group.childName}}|... ``` ### 3.3 两级表头ï¼å¯éï¼ `headerSpans` æè¿°**ä¸é¢é£ä¸å±**表头ï¼åå æ ¼åä½ `æ é¢^è·¨è¶åæ°`ï¼ ``` æ£éªç»æ^3|ç»è®º^2 ``` ä¸å±è¡¨å¤´ç± `columns` éååçæ é¢èªå¨æ¼åºï¼é¡ºåºå³å顺åºï¼ã约æï¼ **ååå æ ¼è·¨è¶åæ°ä¹åå¿ é¡»çäº `columns` çåæ°**ï¼å¦åä¿åæ¶ç»åºç²¾ç¡®æ¥éï¼ > è¡¨å¤´è·¨åæ°å计 5ï¼æ£éªè¡¨çå®é åæ°ä¸º 7ï¼ä¸¤è å¿ é¡»ç¸çãè¯·æ£æ¥ã表头跨åãéåæ®µæ«å°¾ç `^` æ°åã ï¼æ `error-message-precision`ï¼ç»åºå ·ä½ç»´åº¦ + 两侧å®é å¼ + 坿§è¡å¨ä½ãï¼ ### 3.4 æ ·åä¿¡æ¯å段å¯é åä¸ä¸ªè¯æ³ï¼ç¨å¨ `SampleInfo` ä¸ï¼ ``` æ ·åç¼å·={{report.sampleNo}}|产ååç§°={{report.productName}}|è§æ ¼={{report.spec}}|çäº§æ¥æ={{report.produceDate}}|æ¹å·={{report.batchNo}}|åè±åç§={{report.potatoVariety}}|æææ¥æ={{report.expireDate}} ``` æ¯è¡æ¾å ç»ä»ç±ç°æç `pairsPerRow` 屿§æ§å¶ï¼å¸å±é»è¾ä¸åã --- ## 4. å¿ é¡»ä¸å¹¶ä¿®çæ°æ®ä¾§ç¼ºå£ï¼ãæ£æµæ¹æ³ãç®åä¸å¨æ¸²æä½ç¨å è¿æ¯æ¬è½®æå®¹ææ¼æãä¸**ä¸ä¿®åæ°åè½å½¢åè设**ç䏿¡ã | å± | ç°ç¶ | |---|---| | MES 忮侧 `MesQcReportItemRespDTO` | **å·²æ** `checkMethod`ï¼æ£æµæ¹æ³ï¼ã`tool`ï¼æ£æµå·¥å ·ï¼ | | qcreport 弿 `InspectionItem` | **没æ**è¿ä¸¤ä¸ªå段 | | `ReportContext.itemMap()` ä¸åçå端ä½ç¨å | åªæ `index / itemCode / itemName / standardValue / actualValue / unit / requirement / group.* / upperLimit / lowerLimit / result / resultText / remark` | â å°±ç®åå®ä¹éåäº `æ£æµæ¹æ³={{item.checkMethod}}`ï¼æ¸²ææä¹åä¸å°å¼ï¼åªä¼æãç»å®åä¸å°å¼ã è®°ä¸æ¡ç¼ºå£ãçä¸ç空ç½ã **须补çé¾è·¯**ï¼ç¼ºä¸ä¸å¯ï¼ä¸ä¸¤ä¾§å段åå¿ é¡»éåä¸è´ï¼ï¼ 1. `MesQcReportContextMapper`ï¼æ DTO ç `checkMethod`ï¼å¿ è¦æ¶å« `tool`ï¼æ å°è¿ `InspectionItem` 2. `InspectionItem`ï¼å åæ®µ + è¿ `copy()` 3. `ReportContext.itemMap()`ï¼ä¸åå°å端ä½ç¨å 4. `ReportContextCodec`ï¼å»ç»å¿«ç §ç读åï¼å¦åå岿¥å鿏²ææ¶ä¸¢å¼ï¼ 5. å端 `engine/context.ts` ç `ReportContextItem`ï¼éååååæ®µ 6. 对æ fixture ä¸ç¸å ³åæµ **注æ**ï¼`resultText`ï¼å¤å®ï¼æ¯**弿ç®åºæ¥ç**ï¼`InspectionItem.applyResult`ï¼ï¼ä¸æ¯ MES ç»çï¼ ãç»è®ºãå妿è¦çæ¯äººå·¥æçç»è®ºï¼è¿è¦ç¡®è®¤å®æ¯å¦å·²æä¸åï¼`MesQcReportContextMapper` 注éæå° ã人工æçæ£éªå¤å®ï¼ä¸å¼æç®åºç result/resultText/conclusion åå两å¤ãï¼ãè¿ä¸æ¡**å¾ ç¡®è®¤ååå®**ã --- ## 5. 被å¦çå¤é | å¤é | 妿çåå | |---|---| | åçåå®ä¹ç¨ JSON å符串 | 模åè¦è¾åºè½¬ä¹åçåµå¥ JSONï¼`"columns":"[{\"label\":â¦}]"`ï¼ï¼å®æµè¿ç±»è½¬ä¹æ¯æ¨¡ååºéçéç¾åºï¼å±æ§é¢æ¿éäººä¹æ²¡æ³ææ¹ãåè¡è¯æ³ä¿¡æ¯éç¸åè两边é½å¥½åã | | ç» `QualityFieldType` å æ°ç»ç±»å | è¦å¨åè®®ã屿§é¢æ¿ãç»å¸åºååãschema å¥çº¦æµè¯ï¼ä»£ä»·è¿è¶ æ¶çï¼è§ 2.1ï¼ã | | æ°å¢ç¬ç«çãå¯èªå®åæ£éªè¡¨ãç»ä»¶ | ä¸ `QualityTable` / `GroupedQualityTable` 䏿¹ç«äºï¼éåç¨³å®æ§é£é©å¤§ï¼è§ 3.1ï¼ï¼ç¨æ·è¿è¦ç»´æ¤ä¸¤å¥åç±»ç»ä»¶ã | | åä¸ä¸ªå¯è§åçåç¼è¾å¨ï¼å¢å ææ½ï¼ | æ¶çç¡®å®æ´é«ï¼ä½ææ¬æ¯åè çæ°åï¼ä¸éè¦æ°ç屿§é¢æ¿æ§ä»¶ç±»åã**建议å ç¨æå¡«å符串éªè¯ä»·å¼**ï¼çæéæ±åè¿ä»£ââ飿¶åç¬¦ä¸²è¯æ³å·²ç»æ¯ç°æçå卿 ¼å¼ï¼ä¸ç¨è¿ç§»ã | | æ `SampleInfo` ç `dataSchema` æ¹æå¨æ | ç»å®éæ©å¨åªè½æä¸¾éæ `dataSchema`ï¼å¨æåä¼è®©ãéæ°æ®å段ãå ¥å£å¤±æã**ä¿æéæ**ï¼`columns`/`fields` 屿§è®¾ä¸º**ä¸å¯ç»å®**ï¼`bindable` ä¸å£°æï¼ï¼ç±äººæå宿´ä¸²ã | --- ## 6. AI ä¾§çå½±åï¼æ¬è½®çéç¹é£é©ï¼ æ°å±æ§å¿ 须让模åè½å¡«ï¼å¦å AI å¯¼å ¥ä»äº§ä¸åºè¿äºåã 1. **æ¸ åèªå¨å¸¦ä¸**ï¼`buildQualityComponentCatalog()` æ `propertySchema` åæ ·æ å°æ `fields`ï¼ æ°å±æ§å£°æä¸º `type: 'text'` å³èªå¨è¿æç¤ºè¯ï¼**ä¸éè¦æ¹ `catalog.ts`**ã ç¨ç°æ `text` ç±»åï¼è䏿¯èªé ä¸ä¸ªæ°ç±»åï¼è¿è½è®©å±æ§é¢æ¿ç´æ¥æ¸²ææå¤è¡è¾å ¥æ¡ï¼é¶æ¹å¨ã 2. **æç¤ºè¯è¦è¡¥è§å**ï¼éè¦æ°å¢ä¸æ¡æ½åè§åï¼è¯´æãå件鿣éªè¡¨é¤æ ååä¹å¤è¿æå«çå ï¼æ£æµæ¹æ³ãç»è®ºã夿³¨çï¼æ¶ï¼ç¨ `columns` æè¿äºåæ `æ é¢=ç»å®` åè¿å»ãï¼ å¹¶ç»åºå¯ç¨ç»å®è·¯å¾ç½ååã 3. **`aiHint` å¿ é¡»éå**ï¼`GroupedQualityTable` ç hint è¦è¯´æãå䏿¬ç»ä»¶é»è®¤ä¸åæ¶ç¨ `columns` è¦çãï¼ ä¸**ä¸è½**让模å以为ãåä¸ä¸è´å°±è¦æ¢ç»ä»¶ãââå¦ååä¼åå°éåä¸ç¨³ã 4. **éªæ¶å£å¾**ï¼AI å¯¼å ¥çéªæ¶å¿ é¡»å å«ã忝å¦ä¸åä»¶çå**éå对é½**ãï¼ èä¸åªæ¯ãtype é对äºããè¿æ¯æ¬è½®ä»ç¨æ·åé¦éå¦å°çï¼`GroupedQualityTable` é对ä¸çäºæ¥å对äºã --- ## 7. å½±åé¢ï¼é¢ä¼°æ¹å¨æ¸ åï¼ **å端ï¼`mom-pro2-before/src/components/quality/`ï¼** - `inspection/grouped-quality-table.ts`ï¼`propertySchema` å `columns` / `headerSpans`ï¼`type: 'text'`ï¼ï¼ `buildContent` éè§£æï¼ç¼ºå¸åè½ç°æ `COLUMNS`ï¼`validate` å è·¨åæ°æ ¡éª - `header/sample-info.ts`ï¼`propertySchema` å `fields`ï¼`type: 'text'`ï¼ï¼`buildContent` åçåè½ - æ°å¢ä¸ä¸ª**å ±ç¨çè§£æå½æ°**ï¼å说æä¸² â åå®ä¹æ°ç»ï¼ï¼ä¸¤ä¸ªç»ä»¶å ±ç¨ï¼é¿å 两份解æé»è¾æ¼ç§» - `aiHint` ä¸ `validate` çææ¡ **å端ï¼`yudao-module-qcreport`ï¼** - æ°æ®ä¾§ 6 å¤ï¼è§ §4 - æç¤ºè¯ï¼`QcReportTemplatePromptBuilder.EXTRACTION_RULES` å 䏿¡è§å - **ä¸éè¦**å¨ `HtmlRenderer` / `BASE_CSS` / `CanvasSafety` / schema çæ¬å· **ä¸å¨**ï¼`docs/sql/config_export_all_*.sql`ï¼æ è¡¨ç»æãæ é ç½®æ°æ®åæ´ï¼ã --- ## 8. å ¼å®¹æ§ - æ°å±æ§é½æ¯**å¯é**ä¸**ä¸åè¿ `defaults`** â å·²ä¿å模æ¿ç `attributes` éæ²¡æè¿ä¸ªé®ï¼ `buildContent` èµ°åè½åæ¯ï¼äº§ç©éåä¸åã - `schemaVersion` ä¿æ `1.1`ï¼`ReportTemplateSchemaCompatibilityTest` å®çæ¯ãæ§ JSON 读å¾åºãï¼ å 屿§ä¸å½±åï¼`QualityComponentNode.attributes` æ¯å符串æ å°ï¼æ°é®å¤©ç¶å ¼å®¹ã - å·²åå¸çæ¬ï¼å»ç»å¿«ç §ï¼ä¸éç®ï¼ä¸åå½±åã --- ## 9. éªè¯è®¡å 1. åæµï¼ä¸¤ç±»ç»ä»¶åè¦çã屿§ç¼ºå¸ â 产ç©ä¸æ¹åéåä¸è´ãï¼é²åå½ç**第ä¸ä¼å 级**ç¨ä¾ï¼ã ã屿§å¨åº â åæ°/ååº/è¡¨å¤´è·¨åæ£ç¡®ããè·¨åæ°ä¸å¹é çæ¥éææ¡ã 2. è§£æå½æ°ççº¯å½æ°åæµï¼éæ³è¾å ¥ï¼ç¼º `=`ã空æ é¢ã`|` åºç°å¨æ é¢éï¼åèªæ¥ä»ä¹éã 3. åå端ä¸è´æ§ï¼æ°å产åºç HTML è¦è¿ `FrontendConformanceTest` ä¸ `CanvasSafetyTest`ï¼ `rowspan`/`hidden` çåç»æºå¶å¨è¦çåä¹åä»è¦å·¥ä½ã 4. æ°æ®ä¾§ï¼`{{item.checkMethod}}` å¨çå®è´¨æ£åä¸åå¾å°å¼ï¼ä¸èµ·ç¼ºå£åè¦ï¼ã 5. AI å¯¼å ¥ï¼ç宿件 + 空 hintï¼æ£æ¥**åéå对é½**ï¼ä¸åªç typeã 6. åéåå½ï¼`mvn -o -pl yudao-module-qcreport -am test` å ¨ç»¿ï¼å端 `pnpm typecheck`ã --- ## 10. æç¡®ä¸å - ä¸åå¯è§ååç¼è¾å¨ï¼å éªè¯åç¬¦ä¸²æ¹æ¡çä»·å¼ï¼ - 䏿°å¢æ£éªè¡¨ç»ä»¶ - 䏿¹å±æ§åè®®ï¼ä¸å æ°ç»ç±»åï¼ - 䏿¹ `HtmlRenderer` / `BASE_CSS` / `CanvasSafety` - ä¸å¨ schema çæ¬å·ãä¸åæ°æ®è¿ç§» - 䏿¹ `docs/sql/config_export_all_*.sql` --- ## 11. å¾ è£å³ 1. **`headerSpans`ï¼ä¸¤çº§è¡¨å¤´ï¼æ¯å¦æ¬è½®å°±å**ï¼ä¸åçè¯ãæ£éªç»æ | ç»è®ºãè¿å±ä»è¿åä¸äºï¼ ä½åè¦çå·²ç»è½è§£å³ãæ£æµæ¹æ³/ç»è®ºå丢失ãè¿ä¸ªæ´å¤§ç缺å£ã 2. **ãç»è®ºãåååªä¸ªå¼**ï¼äººå·¥æçå¤å®ï¼`MesQcReportContextMapper` éæå°çé£ä¸å¤ï¼è¿æ¯ 弿ç®ç `resultText`ï¼åéä¼è®©æ¥åç»è®ºä¸åæ®ç»è®ºä¸ä¸è´ï¼å±äºä¸å¡å£å¾ï¼éä½ ç¡®è®¤ã 3. **`tool`ï¼æ£æµå·¥å ·ï¼æ¯å¦ä¸å¹¶ä¸å**ï¼å件没æè¿ä¸åï¼å ä¸ä¸åï¼éè¦æ¶åå ã 4. **æ ·åä¿¡æ¯çé»è®¤å段è¦ä¸è¦ä¿æç°ç¶**ï¼6 项ï¼ï¼æ¹äºé»è®¤å¼ä¼è®©**æ°å»º**çæ ·åä¿¡æ¯åä¸ç°å¨ä¸åã å»ºè®®ä¿æä¸åï¼åªè®©ãå¯é ãæä¸ºç¨æ·è½æ¹çè½åã docs/qc_report_generate_from_qc_frontend_integration.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,251 @@ # è´¨æ£å â è´¨æ£æ¥å åºä»¶ å端èè°æ¹æ¡ > å端模åï¼`yudao-module-qcreport`ãæ¥å£åç¼ï¼`/admin-api/qc-report/instance` > 䏿¸¸æ°æ®æºï¼`yudao-module-mes`ï¼`MesQcReportApi`ï¼æ¬æ¨¡åä¸ç´æ¥è¯» MES è¡¨ï¼ > æ¬è½®èå´ï¼**ç± MES è´¨æ£åï¼IQC/IPQC/OQC/RQCï¼ç´æ¥åºæ¥å**ãå端åªééãåªå¼ åæ® + ç¨åªå¼ 模æ¿ãï¼ > æ°æ®å ¨é¨ç±åç«¯ä» MES åï¼**å端ä¸åèªå·±æ¼ `context`**ã ## æ¶åé¡µé¢ | é¡µé¢ | è·¯ç± | 说æ | ç¶æ | |---|---|---|---| | MES è´¨æ£åå表ï¼IQC/IPQC/OQC/RQC å个页é¢ï¼ | `/mes/qc/iqc`ã`ipqc`ã`oqc`ã`rqc` | è¡æä½æ°å¢ãåºæ¥åãï¼éæ¨¡æ¿ â è°æ¬æ¥å£ â è·³æ¥åå®ä¾å表ï¼å·²å¸¦çéæ¡ä»¶ï¼ | **å·²è½å°** | | æ¥åå®ä¾å表页 | `/qc/report-instance` | çæåæ `businessType` + `businessId` 忥å¾å°ï¼æç´¢è¡¨åå·²å ãä¸å¡åæ®ãåæ®µå¹¶æ¯æè·¯ç±å¸¦å ¥ | å·²è½å°ï¼`views/mes/qc/report/instance/index.vue`ï¼ | | 模æ¿è®¾è®¡å¨ | `/qc/report-template` | æ æ¹å¨ï¼æ¨¡æ¿éå å¨è®¾è®¡å¨éé 好æ£éªé¡¹è¡¨æ ¼å¹¶åå¸ï¼ | å·²è½å° | > ä¸ `qc_report_instance_frontend_integration.md` çåå·¥ï¼é£ä»½è®²çæ¯ãè°ç¨æ¹èªå·±å夿°æ®ãçéç¨åºä»¶ï¼ > æ¬ä»½è®²çæ¯ãæ°æ®ä» MES åãçä¸ç¨å ¥å£ãä¸¤è æç»é½è½å°åä¸ä¸ªå®ä¾è¡¨ãåä¸ä¸ªå表页ã ## ä¸å¡æµç¨ä¸æ°æ®å¸¦å ¥ 1. **è´¨æ£å â åºæ¥å**ï¼ç¨æ·å¨è´¨æ£å**åè¡¨è¡æä½**éç¹ãåºæ¥åãâ å端弹çªååºå¯éæ¨¡æ¿ ï¼**åªååº `reportType` ä¸è¯¥åæ®ç±»åä¸è´ç模æ¿**ï¼â è° `generate-from-qc`ã 2. **å端å éæ ¡éª**ï¼æä¸å¡ä¼å 级ï¼åªæ¥ç¬¬ä¸å¤ï¼ä¸ä¸æ¬¡æå¤æ¡ï¼ï¼ ç±»ååæ³ â åæ®åå¨ â 已宿æ£éª â 已填å¤å® â æ¨¡æ¿ç±»åå¹é â æå¯ç¨æ£éªé¡¹ã 3. **åç«¯åæ°å¹¶åºä»¶**ï¼`MesQcReportApi` è£ é è´¨æ£ååå±ç»æï¼å头 / å¤å®ä¾æ® / æ ·å / 宿µå¼ï¼ â å½ä¸ææ¥åä¸ä¸æ â èµ°**æ¢æç** `generate` æµç¨ï¼çæ¬å叿 ¡éªãç¼å·çæãå¿«ç §å»ç»ãå¤å®å¼æå ¨é¨ç §æ§ï¼ã 4. **ç»æåå°å端**ï¼å¼¹çªå å°±å°å±ç¤ºç»æï¼`reportNo` / `itemCount` / `undecidableCount` / `warnings` / `errors`ï¼ï¼ åºé¨ãæ¥çæ¥åå表ã跳转æ¥åå®ä¾å表页并带ä¸ç鿡件ã 5. **忥**ï¼æ¥åå®ä¾åè¡¨é¡µç¨ `businessType=mes_qc_iqc` + `businessId=1` å³å¯æ¥åºè¯¥åæ®åºè¿çæææ¥åã **å¸¦å ¥å ³ç³»è¦ç¹** - `qcType` + `qcId` æ¯ä¸»çº¿ï¼ä»è´¨æ£åå表è¡å¸¦å ¥ã`qcType` ç¨åå ¸ `mes_qc_type` ç**æ°å¼**ï¼`1`/`2`/`3`/`4`ï¼ã - `templateId` ç±ç¨æ·å¨å¼¹çªééã**åç«¯å¿ é¡»å æ `reportType` è¿æ»¤æ¨¡æ¿å表**ï¼å¦åç¨æ·ä¼éå°åç«¯å¿ ç¶æç»çç»åã - `version` ä¸ä¼ æ¶åç«¯åæ¨¡æ¿ç `currentVersion`ï¼è¦æ±å·²åå¸ï¼ã设计å¨ååäºè稿æ³ç«å»åºä»¶æéè¦æ¾å¼ä¼ ã - `reportNo` ä¸ä¼ ç±å端èªå¨çæï¼`QR + æ¥æ + 4 使µæ°´`ï¼ã - **å表 â æ¥åå®ä¾ç忥çéæ¯è·¯ç± query å¸¦å ¥**ï¼è·³è½¬æ¶å¸¦ `businessType`ï¼è¡¨åï¼å¦ `mes_qc_oqc`ï¼ ä¸ `businessId`ï¼è´¨æ£å主é®çå符串形å¼ï¼ä¸¤ä¸ªåæ°ï¼æ¥åå®ä¾åè¡¨é¡µå¨æè½½æ¶æå®ä»¬é¢å¡«è¿æç´¢è¡¨åï¼ ç¨æ·è½å°å³çå°ååºçæ¥åï¼ä¸éè¦èªå·±åçä¸éã ## UI å ¥å£ï¼å·²è½å°ï¼ | ä½ç½® | å ç´ | å±ç¤ºè§å | 说æ | |---|---|---|---| | è´¨æ£ååè¡¨è¡æä½ | ãåºæ¥åãæé® | **ä» å½ `status === 已宿` æ¶æ¾ç¤º** | ä¸ãç¼è¾ããå é¤ãï¼ä» è稿å¯è§ï¼äºæ¥ï¼é¿å ç¨æ·å¯¹æªå®æçåæ®ç¹ | | å¼¹çªæ é¢ | `åºæ¥å - {åæ®ç¼å·}` | â | ç¼å·ç±å表è¡å¸¦å ¥ï¼ä» ç¨äºå±ç¤º | | å¼¹çªé模æ¿åº | å鿍¡æ¿å表 | ç±å端 `/qc-report/template/selectable` è¿åï¼`reportType` 䏿¬åç±»åä¸è´ã`status=å¯ç¨`ã**`currentVersion` æåççæ¬å¤äºãå·²åå¸ããä¸è¯¥çæ¬çç»å¸æå¯æ¸²æçå 容** | æ å¯é项æ¶ç»ç©ºç¶ææç¤ºï¼å¼å¯¼å 廿¨¡æ¿è®¾è®¡å¨é 好并åå¸ | | å¼¹çªç»æåº | æåæç¤º + åè¦åº | çææååå°±å°åæ¢ï¼ä¸å ³å¼¹çª | `undecidableCount > 0` ç¨**èè²ä¿¡æ¯æ¡**ï¼ä¸æ¯é»è²åè¦ï¼ï¼è¿é¡¹æ°éç»å¤§å¤æ°æ¯ãè¿ç¨åæ°ãè¿ç±»æ£å¸¸æ åµï¼æ é»ä¼å¨å 乿¯ä»½æ¥åä¸è¯¯æ¥ | | å¼¹çªåºé¨ | ãæ¥çæ¥åå表ã | ç»æåºæåºç° | å ³å¼¹çªåè·³æ¥åå®ä¾åè¡¨å¹¶å¸¦ä¸ `businessType` + `businessId` | > **å¼¹çªæ é¢ä¸å¸¦ç±»å䏿å**ï¼ä»ä¿æè¿ä¸ªåæ³ï¼ï¼`mes_qc_rqc` è¿ä¸è·¯æ¾ææªè¾åæ§ââ > å端æä¸¾ã表注éãèåæéå项é½åä½ãéè´§æ£éªãï¼åªæèåç¶é¡¹ä¸ `mes_qc_type` åå ¸åä½ãéææ£éªãã > ç°å·²ç»ä¸ä¸º**ãéè´§æ£éªã**ï¼çç±ï¼RQC çå¯¹è±¡åæ¶è¦çç产éæãéå®éè´§ãå®åéè´§ï¼ > ãéè´§æ£éªãæ¯è½è¦çä¸è ç说æ³ï¼ä¸å®æ¬å°±æ¯å¤æ°æ´¾ï¼ãæ é¢ä»ä¸å¸¦ç±»ååï¼å°ä¸å¤éè¦è·çåçææ¡ã ## API | æ¹æ³ | è·¯å¾ | 说æ | æéç | |---|---|---|---| | POST | `/qc-report/instance/generate-from-qc` | ç±è´¨æ£åçææ¥åï¼æ°æ®ä» MES åï¼ | `qc-report:instance:generate` | | GET | `/qc-report/template/selectable` | åãå¯ç¨äºåºä»¶ãçæ¨¡æ¿å表 | `qc-report:template:query` | > `qc-report:instance:generate` æ¯**ç¬ç«æéç **ï¼ä¸ `:create`ã`:query` é½ä¸éç¨ã **`GET /qc-report/template/selectable` 请æ±åæ°ï¼** | åæ° | ç±»å | å¿ å¡« | 说æ | |---|---|---|---| | `reportType` | String | æ¯ | æ¥åç±»åï¼å³ `mes_qc_type` ç**æ°å¼å符串**ï¼å¦ `"1"` / `"4"`ãä¼ `"IQC"` è¿ç±»è±æåä¼ä¸ä¸ªé½æ¥ä¸å° | **ååºï¼** `Template[]`ï¼**æ°ç»ï¼ä¸æ¯åé¡µç»æ**ï¼ï¼åæ®µä¸æ¨¡æ¿å页æ¥å£ä¸è´ã **为ä»ä¹åå¼è¿ä¸ªæ¥å£ï¼è䏿¯å¨åç«¯è¿æ»¤æ¨¡æ¿åé¡µç»æï¼** 两æ¡è¿æ»¤æ¡ä»¶é½è½å¨**çæ¬è¡¨**ä¸ï¼å页æ¥å£è¿åç `currentVersion` åªæ¯æ¨¡æ¿è¡¨ä¸çä¸ä¸ªå符串ï¼çä¸å°çæ¬ç¶æä¸ç»å¸å 容ã 1. **çæ¬æªåå¸**ï¼åç¨ä¸ä¸ªçæ¬**ä¸ä¼**æ¸ ç©ºæ¨¡æ¿ç `currentVersion`ï¼äºæ¯ä¼åºç°ã`currentVersion` æå¼ãä½é£ä¸ªçæ¬å·²åç¨ãçæ¨¡æ¿ï¼è¿ç§æ¨¡æ¿éä¸ååºä»¶å¿ ç¶å¤±è´¥ï¼`1_070_101_003`ï¼ã 2. **ç»å¸æ²¡æå¯æ¸²æå 容**ï¼GrapesJS æãæå¼è¿è®¾è®¡å¨ä½ä¸ä¸ªç»ä»¶é½æ²¡æ¾ãç项ç®åæ `{"assets":[],"styles":[]}`ââå®<b>é空</b>ï¼æä»¥æ§å¤æ®ä¼æ¾å®è¿å»ï¼æ¸²ææ¶ååä¸å°æ ¹ç»ä»¶ãbody 渲ææç©ºä¸²ï¼**䏿¥é**ï¼ç¨æ·æç»æ¿å°ä¸å¼ ç½çº¸ PDFã 两æ¡é½æ¯ãçèµ·æ¥è½éãéäºå¿ åºé®é¢ãï¼å¤æå®ä»¬è¦è¯»çæ¬è¡ï¼æ¾å¨åç«¯åææ¯åç¹ï¼ å端æ¿çåé¡µç»æèªå·±æ¨å¯¼ä¼å¾å°ãçèµ·æ¥å¯¹ä½ä¼æ¼ãçç»è®ºã > å·²è¡¥å ¥ `system_menu`ï¼`id=1075416`ï¼åç§°ãæ¥åå®ä¾è´¨æ£ååºä»¶ãï¼ç±»å 3ï¼æé®ï¼ï¼ > æå¨ãæ¥åå®ä¾ãï¼`parent_id=1075410`ï¼ä¸ï¼`sort=5`ãéè¶ ç®¡è§è²éæäºè¯¥èååæå¯è§æé® / å¯è°ç¨æ¥å£ > ï¼è¶ 管ç±ä»£ç ç´è¿å ¨é¨èåï¼ä¸åå½±åï¼ã ### 请æ±åæ° | åæ° | ç±»å | å¿ å¡« | 说æ | |---|---|---|---| | `qcType` | Integer | **æ¯** | è´¨æ£ç±»åï¼`1` æ¥ææ£éª(IQC) / `2` è¿ç¨æ£éª(IPQC) / `3` åºè´§æ£éª(OQC) / `4` éè´§æ£éª(RQC) | | `qcId` | Long | **æ¯** | è´¨æ£åç¼å·ï¼åå¼ è¡¨åèªç主é®ï¼ | | `templateId` | Long | **æ¯** | æ¥å模æ¿ç¼å·ã**å ¶ `reportType` å¿ é¡»ä¸ `qcType` ä¸è´** | | `version` | String | å¦ | 模æ¿çæ¬å·ï¼å¦ `v1.0`ãä¸ä¼ å忍¡æ¿ç `currentVersion` | | `reportNo` | String | å¦ | æ¥åç¼å·ãä¼ äºå°±ç¨å®ï¼æå·ä¼æ¥éï¼ï¼ä¸ä¼ ç±å端èªå¨çæ | **请æ±ç¤ºä¾** ```json { "qcType": 1, "qcId": 1, "templateId": 1, "version": "v1.1" } ``` ### ååºå段 | åæ®µ | ç±»å | 说æ | |---|---|---| | `id` | Long | æ¥åå®ä¾ç¼å·ï¼æ¿å®å»æ¥è¯¦æ / 跳转 | | `reportNo` | String | æç»çæçæ¥åç¼å· | | `templateVersion` | String | æ¬æ¬¡ä½¿ç¨ç模æ¿çæ¬å· | | `itemCount` | Integer | åå ¥æ¥åçæ£éªé¡¹æ¡æ° | | `undecidableCount` | Integer | **没æå¤å®ç»è®ºç项æ°**ï¼ãæ å¤å®è§åã+ è§åæ§è¡å¤±è´¥çãå¾ å¤å®ãï¼ï¼è§ãä¸å¡è§åã | | `warnings` | Array\<String\> | 软æç¤ºï¼æ ·åçåèãæ²¡æç»çåç»é¡¹ã为ç»å¤´è¡¥åºæ¥çè¡ãææ å·²å é¤ãæªå½å ¥å®æµå¼ | | `errors` | Array\<String\> | æ¸²æææ°æ®ç¼ºå£ï¼ä¸éç¨åºä»¶æ¥å£åä¸å¥æºå¶ï¼ | **ååºç¤ºä¾** ```json { "code": 0, "data": { "id": 38, "reportNo": "QR20260919-0001", "templateVersion": "v1.1", "itemCount": 14, "undecidableCount": 5, "warnings": [ "ä»¥ä¸ 2 个åç»é¡¹å¨æ¬åçæ£éªæç»é没æèªå·±çè¡ï¼å¤åæ¯æ£éªæ¨¡æ¿åªéäºå项ï¼ï¼æ¥åé为å®ä»¬åè¡¥äºä¸è¡æ¥æ¾ç¤ºç»åä¸å ¬å¼ï¼ãæ°´åãé ¸å«éã", "ä»¥ä¸ 1 ä¸ªææ æ¯ææ åºéçåç»é¡¹ï¼ä½æ¬å没æå±äºå®ä»¬çåé¡¹ï¼æ¥åéæç¬ç«æ£éªé¡¹ååºãç»åä¸åå¹¶ï¼ãè²åº¦ã" ], "errors": [] } } ``` ### æ¥åä¸ä¸ææ°å¢å段ï¼è®¾è®¡å¨å¯ç»å®ï¼ åºä»¶æ¶å端åè¿æ¥åçæ¯ä¸ªæ£éªé¡¹ä¸ï¼æ°å¢äºä¸é¢ 4 ä¸ªåæ®µãå®ä»¬ä¸å¨è¿æ¡æ¥å£çååºéï¼èæ¯ä½ç°å¨**æ¥åä¸ä¸æ**ä¸ï¼ 便¨¡æ¿å¨è®¾è®¡å¨éç»å®ï¼ãåç»æ£éªé¡¹è¡¨ãç»ä»¶å·²å ç½®è¿äºç»å®ï¼ä¸è¬ä¸éè¦æå·¥é ï¼ï¼ | åæ®µ | ç±»å | åå¼ | 说æ | |---|---|---|---| | `item.requirement` | String | ç»å¤´è¡ = è¯¥ææ çå ¬å¼ï¼å ¶ä½è¡ = åæ®ä¸çæ åè¦æ± | ãæ£æµè¦æ±ãåãä¸ `standardValue` å¹¶åä¸åå¹¶ï¼ä¸¤è æ¥æºä¸åï¼ä¸ä¸ªæ¯ææ ä¸»æ°æ®çå ¬å¼ï¼ä¸ä¸ªæ¯åæ®è¡çæ åè¦æ±ï¼ | | `item.group.childName` | String | ç»å å项 = èªå·±çååï¼ç»å¤´è¡ä¸ç¬ç«é¡¹ = 空串 | ãå项ãå | | `item.group.span` | Integer | ç»å 第ä¸è¡ = è¯¥ç»æ»è¡æ°ï¼åç»å ¶ä½è¡ä¸ç¬ç«é¡¹ = `1` | ãæ£éªé¡¹ç®ãåçåå¹¶è¡æ° | | `item.group.hidden` | String | ç»å 第ä¸è¡ä¸ç¬ç«é¡¹ = 空串ï¼åç»å ¶ä½è¡ = `hidden` | `hidden` 屿§é ãå¨ä¸å¨ãèµ·ä½ç¨ï¼åæ `hidden="false"` ç §æ ·éèï¼ï¼æä»¥åªæã空串 = è¿ä¸æ ¼å¯è§ãè¿ä¸ä¸ªå¯å¤å®çå½¢æ | **没æåç»çæ¥åä¸ä¼å¸¦ `group`**ï¼æ´ä¸ªæ¥åä¸ä¸ªåç»é½æ²¡ææ¶ï¼`item.group` è¿ä¸ªé®ä¸åºç°ï¼æ¨¡æ¿éä¹å°±ä¸ä¼å¤åºä¸å±ç©ºæ ¼åã 䏿¦æ¥åéæåç»ï¼å**æ¯ä¸é¡¹é½ä¼å¸¦ `group`**ï¼ç¬ç«é¡¹ç»ç©ºä¸²ä¸ `1`ï¼ââ åæ®µç¼ºå¸ä¼è®©å并屿§è¢«è·³è¿ï¼åèæè¯¥éèçæ ¼åé²åºæ¥ã **ä¸¤ä¸ªåæ®µå¿ é¡»æå¯¹ä½¿ç¨**ï¼`item.group.span` ä¸ `item.group.hidden` åªä½ç¨å¨ãæ£éªé¡¹ç®ãè¿ä¸åä¸ã ä¸è¦æå®ä»¬ç»å°å«çåï¼å¦ãæ£æµè¦æ±ãï¼ï¼ç»å å项åèªçæ£æµè¦æ±è¦éè¡æ¾ç¤ºï¼å¹¶è¿ç»å¤´æ ¼ä¼æå®ä»¬ç¿»æã ## åæ®µå±ç¤ºè§å | åæ®µ | å±ç¤ºä½ç½® | 说æ | |---|---|---| | `undecidableCount` | çææååçæç¤ºæ¡ | **ä¸è¦å½ææ 鿥è¦**ã宿°çæ¯ã没æç»è®ºç项ãï¼å ¶ä¸ç»å¤§å¤æ°æ¯ãè¿ç¨åæ°ãè¿ç±»æ£å¸¸æ åµï¼è¯æ ·è´¨é mãç§°éç¶ m0 ââ åªä¾å ¬å¼åæ°ï¼æ¬æ¥å°±æ²¡æå¯å¤å®çè§æ ¼ï¼ãæç¤ºææ¡å»ºè®®ãæ¬æ¬¡æ N 项没æå¤å®ç»è®ºãï¼**è¥ç¡®å®è¦åºå**ãæ£å¸¸æ è§åãä¸ãè§å没è·éãï¼è¯·ä»¥æ¥å详æ é忣éªé¡¹ç `resultText` 为åï¼`æ å¤å®è§å` / `å¾ å¤å®`ï¼ï¼ä¸è¦ç¨è¿ä¸ªæ»æ°åæ¨ | | `warnings` | çææååçæç¤ºæ¡ | å»ºè®®å¯æå ï¼å å®¹å·²ç»æ¯å®æ´ä¸æå¥åï¼ç´æ¥åæå±ç¤ºå³å¯ï¼ä¸è¦æ¹å | | `errors` | åä¸ | æ°æ®ç¼ºå£ï¼ä¸éç¨åºä»¶æ¥å£åä¹ | | `reportNo` | æåæç¤ºã跳转å详æ é¡µå¤´é¨ | **ä¸å¡å¯ä¸é®**ï¼å»ºè®®å¸¦ãå¤å¶ç¼å·ã | | `id` | ä¸å±ç¤º | ç¨æ¥è·³è½¬ `/qc/report-instance` 详æ | ## ä¸å¡è§å说æ | åºæ¯ | è§å | |---|---| | **è°æä¾æ°æ®** | å端ãå端**ä¸è¦**èªå·±æ¼ `context` ä¼ è¿æ¥ï¼æ¬æ¥å£ä¸æ¥å `context` åæ° | | **ç±»åä¸åæ³** | `qcType` ä¸å¨ 1~4 â `1_070_105_000`ï¼æ¥æéååºæ¶å°çå¼ä¸åä¸ªåæ³å¼ | | **åæ®ä¸åå¨** | â `1_070_105_001`ï¼æ¥æå¸¦åæ®ç±»å䏿åä¸ ID | | **åæ®æªå®ææ£éª** | ç¶æä¸æ¯ã已宿ãâ `1_070_105_002`ï¼æ¥æå¸¦**å½åç¶æä¸æå**ï¼ç¨æ·ä¸ç¼ç¥éå¡å¨åªä¸æ¥ | | **åæ®æ²¡æå¤å®** | å·²å®æä½ `checkResult` 为空 â `1_070_105_003`ï¼æ¥æååºå个å¯éå¤å® | | **模æ¿ç±»åä¸ç¬¦** | æ¨¡æ¿ `reportType` â `qcType` â `1_070_105_005`ï¼æ¥æ**åæ¶ç»åºæ¨¡æ¿ç±»åä¸åæ®ç±»å两边ç䏿å**ã**å端åºå¨éæ¨¡æ¿æ¶å°±æä¸ç¬¦çè¿æ»¤æ**ï¼è¿æ¡æ¯å åº | | **æ¨¡æ¿æ²¡é æ¥åç±»å** | æ¨¡æ¿ `report_type` 为空 â åæ · `1_070_105_005`ï¼ä½æ¥ææ¹ææç¤ºã请å ç»è¯¥æ¨¡æ¿è®¾ç½®æ¥åç±»åã | | **没æå¯ç¨æ£éªé¡¹** | â `1_070_105_006`ï¼æ¥æè¯´æå ·ä½åå ï¼å¦ãåªæåç»æ é¢ãï¼ | | **è½åºå¤ä»½æ¥åå** | **è½**ãåä¸å¼ è´¨æ£åå¯ä»¥åå¤åºä»¶ï¼æ¯æ¬¡çæä¸ä»½ç¬ç«æ¥åï¼åå²ççéè¦ï¼ã**ä¸åéå¤çææ¦æª**ï¼å端ä¹ä¸å¿ æ¦ | | **æ£éªé¡¹æä¹æ¥ç** | è´¨æ£åãå¤å®ä¾æ®è¡ãï¼`max_threshold`/`min_threshold`/æ åå¼ï¼ä¸ã宿µå¼æç»ãï¼ææ ·å Ã ææ ï¼**交å**å±å¼ã**ä¸å任使æ è¿æ»¤**ï¼å ¨é¨è¿æ¥å | | **å¤ä¸ªæ ·åæä¹å** | åä¸ä¸ªï¼æ ·å Ã ææ ï¼å±å¼æ**䏿¡**æ¥åæ£éªé¡¹ãæ ·åç¼å·åªå¨**æ ·åæ° > 1** æ¶æè¿è¯¥é¡¹ç夿³¨ï¼å¦ãæ ·å S1ãï¼ï¼å䏿 ·åæ¶ä¸åï¼é¿å 污æ | | **æ ·åå¤ªå¤æä¹å** | æ ·åæ°è¶ è¿ 3 æ¶ï¼æ¥å头çãæ ·åç¼å·ãåæ®µ**ç空**å¹¶å `warnings`ï¼ååºå 3 个ä¸å N æ»æ°ï¼ï¼ä¸æªææç»æ°æ® | | **åç»ä¼ææ ·ï¼æ¬è½®æ¹å¨ï¼** | åç»é¡¹**ä¸å被跳è¿**ãåç»ç¶é¡¹ï¼å¦ãæ°´åãï¼ä½ä¸ºä¸è¡è¿æ¥åï¼ç»å å项紧éå ¶åï¼ç»å卿¥åä¸çºµååå¹¶ ââ æ¥åè½çåºåªäºå项å±äºåªä¸ç»ï¼ç¶é¡¹çå ¬å¼ä¹ä¸å丢 | | **ç»åä¸å ¬å¼æ¾åª** | ç»åæ¾ãæ£éªé¡¹ç®ãåå¹¶åä¸åå¹¶ï¼å ¬å¼æ¾ç»å¤´é£ä¸è¡çãæ£æµè¦æ±ãåã**ç»å å项åèªçæ£æµè¦æ±ä»éè¡ä¿ç**ï¼äºè ä¸äºç¸è¦ç | | **è°æ¯ç»å¤´** | åç«¯æææ ç `parent_id` å¤å½å±ï¼ä¸çè¡åºãä¸çæ°å¼ç±»åï¼ï¼å¹¶ç»æ¯è¡ä¸å `group.span`ï¼åå¹¶å è¡ï¼ä¸ `group.hidden`ï¼è¿ä¸æ ¼è¦ä¸è¦è®©ä½ï¼ãå端**ä¸éè¦ä¹ä¸ä¼**èªå·±ç®åå¹¶è¡æ° | | **åç»é¡¹æ¬å没æå项** | æç¬ç«æ£éªé¡¹ååºãç»åä¸åå¹¶ï¼å¹¶æååæ¶è¿ `warnings`ï¼ãâ¦æ¯ææ åºéçåç»é¡¹ï¼ä½æ¬å没æå±äºå®ä»¬çå项â¦ãï¼ | | **ç»å¤´æ¬å没æè¡** | è¥æ£éªæ¨¡æ¿åªå¾äºå项ï¼å端**为ç»å¤´è¡¥ä¸è¡**æ¥æ¾ç»åä¸å ¬å¼ï¼å¹¶å¨ `warnings` é说æä¸ºåªå 个ç»å¤´è¡¥äºè¡ãè¡¥åºæ¥çè¡å®æµå¼ä¸ºç©ºï¼å¤å®ä¸ºãæ å¤å®è§åãï¼ä¸å½±ååæ ¼çä¸ç»è®º | | **ææ è¢«å äºä¼ææ ·** | åæ®è¡å¼ç¨äºä¸ä¸ªå·²è¢«å é¤çææ â 该æ¡è·³è¿ï¼`warnings` éæ¥ãæ N æ¡æç»æå¼ç¨çæ£éªææ 已被å é¤ã | | **没å½å®æµå¼ä¼ææ ·** | è¯¥é¡¹ç §å¸¸è¿æ¥åï¼ä½ `warnings` æç¤ºã第 N 项ãXãå°æªå½å ¥å®æµå¼ãï¼**ç»å¤´çéç¹è¡é¤å¤**ï¼å®æå®ä¹æ²¡æå®æµå¼ï¼å¼é½å¨å项è¡ä¸ï¼æ¥åºæ¥æ¯è¯¯æ¥ï¼ | | **æ¥åè¡åºåäº** | 仿¤åçãè¡è¡¨é¡ºåºãæ¹ä¸ºæææ ç `sort_order` ååºï¼ç¼ºå¤±ææåï¼åå¼ä¿æè¡åºï¼ï¼ç»å å项æåèªç `sort_order`ãè¿æ ·æ¥å顺åºä¸æ£éªåç设计æå¾ä¸è´ï¼é»è®¤ä¸éè¦å端åä»»ä½äº | | **åªå¼ ç»å¸ç»ä»¶è½æ¸²æåç»** | éè¦æ¨¡æ¿éç¨**ãåç»æ£éªé¡¹è¡¨ãï¼`GroupedQualityTable`ï¼**ãç¨å®ä¹å¤ç表ï¼å¦åºç¡ãæ£éªé¡¹è¡¨æ ¼ãï¼ä¸ä¼åºç°åå¹¶ææ ââ æ°æ®ç §å¸¸ä¸åï¼åªæ¯æ¨¡æ¿æ²¡ç»ãäºè ç¨å ¶ä¸ï¼æåç»ç¨åè ï¼çº¯å¹³å±ç¨åè | | **å¤å®è°ç®** | æ¥å级ç `result`/`resultText`/`conclusion`/`passRate` ç±**å端å¤å®å¼æ**ç®ï¼é项è§å â è§æ ¼ä¸ä¸é â æ°æ®èªå¸¦ç»æï¼ï¼ä¸éç¨åºä»¶ä¸è´ | | **è´¨æ£åçåå¤å®å»åªäº** | åè¿æ¥åç `report.qcResult` / `report.qcResultText` 两个**æ°å段**ï¼ä¸å¼æå¤å®**å¹¶åãäºä¸è¦ç**ãè´¨æ£åä¸çå¤å®æ¯äººå·¥æçæ¿ï¼åæ ¼/ç¹é/ä¸åæ ¼éè´§/ä¸åæ ¼æ¥åºï¼ï¼å¼æåªæä¸æè£ ä¸ä¸å®ï¼æä»¥åå¼å | | **`undecidableCount` æä¹ç®** | æ°ãå¿«ç §é `result` 为空串ç项ããè¿æ£æ¯å¼æå¯¹ãå¾ å¤å®ãçå®ä¹ï¼ä¹é¡ºå¸¦æãå¡å¨è§åæ¥éä¸ãç项ç®è¿å»ï¼ä¸å¦ç®ä¸éå¤å®æ¡ä»¶ | | **åå¤å®æ¯æ°å** | `qcResult` æ¯æ°åå符串ï¼`"1"`~`"4"`ï¼ï¼`qcResultText` ææ¯ä¸æï¼åæ ¼/ç¹é/ä¸åæ ¼éè´§/ä¸åæ ¼æ¥åºï¼ã**ç»å¸ä¸æ¾ç¤º `qcResultText`** | | **模æ¿å¿ 须已åå¸** | ä¸éç¨åºä»¶ä¸è´ï¼èç¨¿çæ¬ä¼è¢«æï¼`1_070_101_003`ï¼ãå¤ç¨æ¢æé误ç ï¼æ¬æ¥å£ä¸å¦é | | **模æ¿å·²åç¨** | å¤ç¨ `1_070_100_002` | | **æ¥åç¼å·æå·** | å¤ç¨ `1_070_102_001`ã已软å é¤çç¼å·**ä¸åæ¶** | | **æ°æ®å¿«ç §å»ç»** | åºä»¶æ¶æå¤å®åçæ°æ®æ´ä»½å»ç»ãMES éç忮忥æä¹æ¹ï¼**é½ä¸å½±åè¿ä»½å岿¥å** | ## 注æäºé¡¹ - **`engine/context.ts` ç `qcResult` / `qcResultText` 已补é½**ï¼ç±»åæ¥å£ + `emptyReport()` 空串é»è®¤å¼ï¼ï¼ 设计å¨çç»å®é¢æ¿éç°å¨å¯ä»¥éå°è¿ä¸¤ä¸ªå段ï¼ç»å¸ä¸ä¹è½ç»ãè¥è¦ç¨å®ä»¬ï¼æ¨¡æ¿éèªè¡ç»å°æä¸ªææ¬ç»ä»¶ä¸ã - **模æ¿åè¡¨è¦æ `reportType` è¿æ»¤**ï¼æ¨¡æ¿ç `report_type` åçæ¯åå ¸æ°å¼å符串ï¼`"1"`/`"2"`/`"3"`/`"4"`ï¼ï¼ ä¸ `qcType` **ç´æ¥ç¸çæ¯è¾**å³å¯ï¼ä¸è¦æ¿ `"IQC"` è¿ç±»è±æå廿¯ã - **`qcType` ä¼ æ°å¼ä¸ä¼ å符串å**ï¼å端æ `1`~`4` è¯å«ï¼ä¼ `"IQC"` ä¼å¾å° `1_070_105_000`ã - **ä¸è¦ç¼å `warnings`**ï¼å®åªå¨æ¬æ¬¡ååºéåºç°ï¼**ä¸è½åº**ãäºååçå岿¥åæ¶çä¸å°å½æ¶æåªäºæç¤ºã éè¦çç请å¨åºä»¶æåæ¶èªå·±è®°ä¸ä»½ã - **`warnings` ä¸ `errors` åå·¥**ï¼`warnings` æ¯ãæ°æ®åèç软æç¤ºãï¼è·³è¿ãæªæã缺å¼ï¼ï¼ `errors` æ¯ã模æ¿ç»å®äºä½æ²¡æ°æ®çåæ®µãè¿ç±»æ¸²ææç¼ºå£ã**两è é½è¦å±ç¤º**ï¼ä¸è¦åªæ¾ç¤ºå ¶ä¸ä¸ä¸ªã - **`itemCount` ä¸çäºè´¨æ£åçè¡æ°**ï¼ææ 被å ä¼è¢«è·³è¿ãä¸ä¸ªææ 夿¡å®æµå¼ä¼å±å¼æå¤æ¡ã ç»å¤´å¨æ¬å没æè¡æ¶å端ä¼è¡¥ä¸è¡ï¼æä»¥è¿ä¸ªæ°**åªè½ç¨æ¥å±ç¤º**ï¼ä¸è¦æ¿å®å»åè´¨æ£åè¡æ°åæè¨ã - **é¨ååæ®µå»æä¸å¸¦è¿å»**ï¼MES ä¾§æ£éªé¡¹ä¸çãæ£éªæ¹æ³ãï¼`checkMethod`ï¼ä¸ãæ£éªå·¥å ·ãï¼`tool`ï¼ å¨æ¥åä¸ä¸æé**æ²¡ææ¿è½½ä½**ï¼æ¬è½®æªä¼ éãè¥æ¥åè¦å±ç¤ºè¿ä¸¤åï¼éè¦å æ© `InspectionItem` å¥çº¦ ï¼é£æ¯æ¹å¨å端å¯è§çæ°æ®å¥çº¦ï¼éåç¬ç¡®è®¤åååï¼ã - **è´¨æ£å䏿²¡æãé¨é¨ã**ï¼æ¥åç `department` åæ®µæä¸ºç©ºï¼ä¸è¦æ¿å«çåæ®µç¡¬åã - **æ¹æ¬¡å·ååªä¸ªå段æç±»åä¸å**ï¼æ¥ææ£éªåä¾åºåæ¹å·ï¼åºè´§/éè´§åæ¹æ¬¡å·ï¼è¿ç¨æ£éªä¸¤è 齿²¡æï¼åªæå·¥åå·ï¼ãè¿æ¯å端æä¸å¡è¯ä¹å®çï¼å端ä¸ç¨ç®¡ã - **æ¥åå®ä¾è½åºæ¶ `businessType` æ¯è¡¨å**ï¼`mes_qc_iqc` / `mes_qc_ipqc` / `mes_qc_oqc` / `mes_qc_rqc`ï¼ï¼ ä¸å端路ç±/åå ¸éçè±ææä¸¾ååä½ä¸åå ¸æ°å¼ä¸åï¼åæ¥æ¶**ç´æ¥ç¨è¿å个å符串**ã - **失败æ¶ä¸è¦éè¯åºä»¶**ï¼äºéæ ¡éªå¤±è´¥é½æ¯ãæ°æ®æéæ©ä¸å¯¹ãï¼éè¯åæ ·ä¼å¤±è´¥ãææ¥ææç¤ºå»æ¹æ°æ®ææ¢æ¨¡æ¿ã - **å¤±è´¥æ¥æç±è¯·æ±æ¦æªå¨èªå¨å¼¹åºï¼ä¸å¡ä»£ç ä¸è¦å弹䏿¬¡**ï¼`code != 0` æ¶æ¦æªå¨å·²æå端 `message` æ¾ç¤ºæ toastï¼è°ç¨å¤åªå `catch {}` å³å¯ï¼éå¤æç¤ºä¼è®©ç¨æ·çå°ä¸¤æ¡ä¸æ ·çæ¥éã - **æé**ï¼æ¬æ¥å£æéç `qc-report:instance:generate`ï¼å·²è¡¥å ¥ `system_menu`ï¼`id=1075416`ï¼ã ## é误ç ### æ¬æ¥å£æ°å¢ï¼`1_070_105_xxx`ï¼ | é误ç | æ¥æ | 触ååºæ¯ | |---|---|---| | `1_070_105_000` | è´¨æ£ç±»åã9ãä¸åæ³ï¼åªæ¯æ IQCï¼æ¥ææ£éªï¼ãIPQCï¼è¿ç¨æ£éªï¼ãOQCï¼åºè´§æ£éªï¼ãRQCï¼éè´§æ£éªï¼ | `qcType` ä¸å¨ 1~4 | | `1_070_105_001` | æ¥ææ£éªçè´¨æ£åï¼ID=999999ï¼ä¸åå¨æå·²è¢«å é¤ï¼è¯·å·æ°å表åéæ°éæ© | `qcId` æ¥ä¸å° | | `1_070_105_002` | æ¥ææ£éªçè´¨æ£åãIQC20260919001ãå½åç¶æä¸ºãè稿ãï¼å°æªå®ææ£éªï¼ä¸è½çææ¥åãè¯·å æè¯¥åæ®æäº¤å¹¶å®ææ£éªå¤å®ååçæ | åæ®ç¶æéã已宿ã | | `1_070_105_003` | æ¥ææ£éªçè´¨æ£åãIQC20260919001ãå°æªå¡«åæ£éªå¤å®ï¼å¤å®ä¸ºç©ºï¼ï¼ä¸è½çææ¥åã请å å¨è¯¥åæ®ä¸å¡«åå¤å®ç»è®ºï¼åæ ¼ / ç¹é / ä¸åæ ¼éè´§ / ä¸åæ ¼æ¥åºï¼ååçæ | å·²å®æä½ `checkResult` 空 | | `1_070_105_005` | æ¥å模æ¿çæ¥åç±»åæ¯ãåºè´§æ£éªãï¼ä¸æ¬æ¬¡è´¨æ£åçç±»åãæ¥ææ£éªãä¸ä¸è´ï¼æ æ³çææ¥åãè¯·éæ°éæ©ä¸å¼ æ¥ææ£éªçæ¥åæ¨¡æ¿ | 模æ¿ä¸åæ®ç±»åä¸ç¬¦ï¼ææ¨¡æ¿æªé ç±»åï¼ | | `1_070_105_006` | æ¥ææ£éªçè´¨æ£åãIQC20260919001ãï¼ID=1ï¼æ²¡æä»»ä½æ£éªææ ï¼æ æ³çææ¥åã请å å¨è¯¥åæ®ä¸å½å ¥æ£éªææ ä¸å®æµå¼ååçæ | åæ®æ²¡æä»»ä½æ£éªææ è¡ãè¥å ¶ä¸é¨åæç»å¼ç¨çææ å·²è¢«å é¤ï¼æ¥æä¼å¨æ¬å·éè¡¥ä¸å¥ãå ¶ä¸ N æ¡æç»æå¼ç¨çæ£éªææ 已被å é¤ã | ### å¤ç¨æ¢æé误ç ï¼æ¬æ¥å£ä¸å¦é 坿¬ï¼ | é误ç | æ¥æ | 触ååºæ¯ | |---|---|---| | `1_070_100_000` | 模æ¿ä¸åå¨ | `templateId` æ¾ä¸å° | | `1_070_100_002` | 该模æ¿å·²åç¨ï¼ä¸è½çææ¥å | æ¨¡æ¿ `status=1` | | `1_070_101_000` | 模æ¿çæ¬ä¸åå¨ | `version` ä¼ äºä½æ¥ä¸å° | | `1_070_101_003` | 该模æ¿çæ¬æªåå¸ | `version` æåè稿 | | `1_070_101_007` | 该模æ¿è¿æ²¡æå·²åå¸ççæ¬ï¼æ æ³çææ¥åï¼è¯·å å¨è®¾è®¡å¨ä¸åå¸ä¸ä¸ªçæ¬ | ä¸ä¼ `version` ä¸ `currentVersion` 为空 | | `1_070_101_008` | 该模æ¿çæ¬æ²¡æç»å¸å å®¹ï¼æ æ³çææ¥åï¼è¯·å å¨è®¾è®¡å¨ä¸è®¾è®¡å¹¶ä¿å模æ¿å 容 | çæ¬æè®°å½ä½ç»å¸ç©º | | `1_070_102_001` | æ¥åç¼å·ãXã已被å ç¨ï¼è¯·æ¢ä¸ä¸ªç¼å·ï¼æç空ç±ç³»ç»æ QR+æ¥æ+æµæ°´ èªå¨çæ | æå®äºéå¤ç¼å· | > **ä¸åå§æ¹æ¡ææ¡£çå离ï¼å·²è½å°å£å¾ï¼**ï¼æ¹æ¡ææ¡£ §7 éåç `105_004`ï¼æ¨¡æ¿ä¸åå¨ï¼ä¸ `105_007`ï¼æ¨¡æ¿æ å·²åå¸çæ¬ï¼ > **æªå®ç°**ï¼æ¹ä¸ºå¤ç¨ä¸é¢çæ¢æç ãçç±ï¼é£å æ¡ææ¡æ¬æ¥å°±æ¯ç²¾ç¡®çï¼åé ä¸éåªä¼å¤ä¸¤æ¡ä¼æ¼ç§»ç坿¬ã docs/qc_report_grouped_inspection_design.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,187 @@ # 夿æ£éªé¡¹ï¼åç»æ£éªé¡¹ï¼è¿å â 鿱䏿¹æ¡ > ç¶æï¼**å·²å®ç¨¿ï¼å¼å·¥**ãå¥çº¦ä¸ç»æè§ç¬¬åèï¼å®ç°æ¸ åè§ç¬¬äºèã > å ³èï¼`docs/qc_report_ai_import_frontend_integration.md`ï¼AI å¯¼å ¥èè°ï¼ã`docs/æºè½è´¨æ£æ¥åå¹³å°-å¼åè¿åº¦.md`ï¼è¿åº¦ççï¼ã > > åæ´è®°å½ï¼2026-09-19ï¼ï¼ç¨é¡¹ç®èªå¸¦ç `grapesjs@0.23.6` + Chromium 宿µåï¼**æ¨ç¿»äºå稿ãç¨ç»å®æ ·å¼éèéå¤åå æ ¼ãçåæ³**ï¼å®æµä¸å¯è¡ï¼è§ç¬¬ä¸èãéèééãï¼ï¼æ¹ä¸º `hidden` 屿§ï¼åæ¶æç¨æ·å·²ææ¿ç两æ¡å£å¾ï¼æ å¤å®è§åãå ¬å¼è½æ ¼ï¼ä¸åå§ docx ç宿µç»æåè¿å¥çº¦ã ## ä¸ãèæ¯ä¸ç®æ ååæ¥èª `ç¾äºæ¨¡ç.docx`ï¼æ£éªé¡¹æ¯**两å±**ç ââ ä¸ä¸ªåç»ï¼æ°´å / é ¸å«é / æ£ä¸éå«éï¼ï¼ç»å æè¥å¹²å项ï¼è¿ç¨åæ° + ç»æï¼ãè¿ç§ç»æç®åæ æ³è¿åï¼æ¥åé表ç°ä¸ºãä¸å æ£è£ åæ°è¡ï¼çä¸åºå±äºåªä¸ç»ï¼å ¬å¼ä¹ä¸¢äºãã 宿µ `ç¾äºæ¨¡ç.docx` çæ£éªç»æè¡¨ï¼`word/document.xml`ï¼27 è¡ï¼ï¼**项ç®åç¨ `w:vMerge` 纵ååå¹¶**è·¨ä½æ´ç»çè¡ï¼å项åç¬å ä¸åï¼å ¶ä½åï¼æ£æµæ¹æ³/æ åè¦æ±/ç»æï¼éè¡ç¬ç«ãå³ãç»ååå¹¶ãå项åè¡ãæ¯è¿ä»½æ¥åçåççå¼ ââ æ¬æ¹æ¡è¦è¿åçå°±æ¯å®ã **å·²ææ¿çä¸å¡å£å¾**ï¼ç¨æ·ç¡®è®¤ï¼ï¼ | å 容 | å£å¾ | |---|---| | ç¶é¡¹ï¼åç»ï¼çãæ£æµè¦æ±ãé£ä¸æ ¼ | æ¾ç¤º `mes_qc_indicator.formula_text` å ¬å¼ | | ç»å å项ï¼å¦ãæ°´åç»æ wããé ¸å«éç»æ w3ã | ææ¬èº«ç»ææ¾ç¤ºï¼ä¸åç¹æ®å¤ç | | 没æè§æ ¼ä¸ä¸éç项 | å¤å®åæ¾ç¤ºãæ å¤å®è§åãï¼**ä¸è®¡å ¥åæ ¼ç忝ãä¸å䏿¥åç»è®º**ï¼å·²äºä¸ä¸è½®å®ç°ï¼è§ `docs/æºè½è´¨æ£æ¥åå¹³å°-å¼åè¿åº¦.md`ï¼ | ## äºãæå±ï¼å±çº§åå¨äºæ°æ®åºï¼ä½æä¸å¤è¢«åæ 宿µ `mes_qc_indicator`ï¼å®¿ä¸» `2a8dea946ada`ï¼ï¼ | id | name | item_type | parent_id | result_type | formula_text | sort_order | |---|---|---|---|---|---|---| | 21 | æ°´å | 2 | 0 | NULL | `w = (m1 - m0) / m à 100%` | 30 | | 22â25 | è¯æ ·è´¨é m / è¯æ ·+ç§°éç¶ m1 / ç§°éç¶ m0 / æ°´åç»æ w | 1 | 21 | 1 | â | 1â4 | | 26 | é ¸å«é | 2 | 0 | NULL | `w3 = c à V à 0.06005 / m à 100%` | 40 | | 27â30 | æ æ¶²æµåº¦ c / æ¶èæ»´å®æ¶²ä½ç§¯ V / è¯æ ·è´¨é m / é ¸å«éç»æ w3 | 1 | 26 | 1 | â | 1â4 | | 31 | æ£ä¸éå«é | 2 | 0 | NULL | `纯度 = 100% - æ°´å - é ¸å«é - å ¶å®æè´¨` | 50 | | 32 | æ£ä¸éå«é /% | 1 | 31 | 1 | â | 1 | | 19/20/33/34 | å¤è§ / è²åº¦ / å¯åº¦ / æå ç | 1 | 0 | 3/1 | â | 10/20/60/70 | **å½å±ä¾æ®æ¯ `parent_id`ï¼ä¸æ¯è¡åº**ï¼`item_type=2` æ¯åç»å¤´ï¼å项 `sort_order` æç»å éæ°ä» 1 æï¼è**è¡è¡¨éç¶é¡¹é£ä¸è¡ä¸å®çå项并ä¸ç¸é»**ï¼iqc_id=1 é æ°´å å¨ç¬¬ 1 è¡ï¼å®çå项å¨ç¬¬ 11â14 è¡ï¼ã é另宿µï¼å䏿¬¡æ¥è¯¢ï¼ï¼è¯¥å**ææè¡**ç `standard_value` / `max_threshold` / `min_threshold` / `check_method` **å ¨ä¸º NULL** ââ è¿ç¨åæ°æ²¡æè§æ ¼ï¼ç»å¤´æ´æ¯åªæå ¬å¼ãè¿æ¡å³å®äºãæ£æµè¦æ±åæ¾å ¬å¼ãä¸ä¼è¦çæä»»ä½å·²æå 容ã ä¸å¤æå±ï¼ 1. `MesQcIndicatorDO` åªæ å° `id/code/name/type/tool/resultType/resultSpecification/remark` ââ `parent_id`ã`item_type`ã`formula_text`ã`unit_text`ã`sort_order` ä¸ä¸ªé½æ²¡æ å°ï¼å±çº§å° DO 就丢ã 2. `MesQcReportItemRespDTO` 没æä»»ä½ç¶/ç»å段ï¼ç±»æ³¨éæåãæ¥åä¾§ä¸éè¦ç¥éæ ·åä¸å®æµå¼çå±çº§å ³ç³»ï¼éè¡æ¸²æå³å¯ãã 3. `MesQcReportApiImpl.buildItems` ç¨ `indicator.getResultType() == null` å½ãè¿æ¯åç»æ é¢ãç夿®ï¼æç¶é¡¹ä¸¢è¿ `groupItemNames` èµ° warnings è䏿¯å ¥æ¥åã**æ¹åæ¯åç**ï¼è¢«ä¸¢æçæ°å¥½æ¯å¸¦å ¬å¼çç»åï¼æ°´å/é ¸å«é/æ£ä¸éå«éï¼ï¼ç䏿¥çæ°å¥½æ¯è¿ç¨åæ°è¡ã ## ä¸ã弿è½è¡¨è¾¾ä»ä¹ï¼æ¢é宿µç»è®º èæ¬ `.qc-conformance/verify-group-repeat.ts`ï¼åªè¯»ï¼ä¸æ¹ä»£ç ä¸å¨åºï¼ï¼äº§ç© `group-probe-p4.html`ã`group-probe-p5.html`ã **å å³çº¦æ**ï¼è¯» `report-evaluator.ts:237` + `ReportContext.itemMap` å¾å°ï¼ï¼å¤å®é¶æ®µæä¸ä¸æé建æ `{ report, inspectionItems }` **两个é®**ï¼ä¸¤ä¾§ä¸è´æ¶å£ãä»»ä½èªå®ä¹é¡¶å±æ°ç»ï¼`inspectionGroups` ä¹ç±»ï¼é½è¿ä¸äºæ¸²æä½ç¨å ââ æ¢é P3 宿µæ¸²æåº 0 è¡ï¼ä¸ä¸æ¥éã**åç»ä¿¡æ¯åªè½æå¨æ¯ä¸ªæ£éªé¡¹ä¸ã** | ç»æ | ç»æ | |---|---| | P1 æå¹³äº¤éï¼ç»å¤´è¡ä¸æç»è¡åä¸å¥åå æ ¼ï¼æ rowspanï¼ | å¯ç¨ï¼ä½ç»å¤´è¡å¿ é¡»æ¯æ°ç»éçä¸é¡¹ â **污æ total / passCount** | | P2 æ¯è¡é¦åé½å `rowspan` | 渲æåº `rowspan="5"`ã`rowspan="1"`ï¼ä½æ¯è¡é½èªå¸¦é¦å â åéä½ï¼**ä¸å并䏿´ç³** | | P3 é¡¶å±èªå®ä¹æ°ç» | 渲æ 0 è¡ï¼ä¸ä¸æè¢«æ¶å£ï¼éé»ä¸¢å¼ï¼ | | P4 åµå¥ repeatï¼å¤å±æç»ã`tbody` å åæ `children`ï¼ | å¯ç¨ï¼`rowspan` ç»å®çæãé¡ºåºæ£ç¡®ï¼ä½ç»å¿ é¡»ä½ä¸ºä¸é¡¹è¿æ°ç» â åæ ·æ±¡æç»è®¡ï¼ä¸éè¦ `tbody` å¥ `tbody` | | **P5 æå¹³ + rowspan + éèéå¤åå æ ¼** | **å¯ç¨ï¼éç¨** | **P5 宿µ**ï¼æµè§å¨ï¼`border-collapse: collapse`ï¼ï¼ - é¦è¡ä¸¤æ ¼ `<td rowspan="4">æ°´å</td><td rowspan="4">w = â¦</td>`ï¼é«åº¦ = 4 è¡ä¹åï¼åå¹¶æç«ï¼ - ç»å 第 2â4 è¡åä¸¤æ ¼è¢« `display:none` åé¤åï¼è¿äºè¡çå¯è§åå æ ¼ `left` ä¸è¡¨å¤´ç¬¬ 3â7 å**éåç¸ç** â å坹齿 æã æ¢é彿¶å¹¶äº**两**æ ¼ï¼ç»å + å ¬å¼ï¼ï¼æç»ç»æåªå¹¶ç»å䏿 ¼ï¼è§ §4.1 çå£å¾è¯´æï¼ï¼æ¯æ¢éç»è®ºççåé ââ æºå¶ç¸åï¼åªæ¯å°å¹¶ä¸æ ¼ï¼ç¬¬ 4 åç §å¸¸éè¡åå¼ã ### éèééï¼å稿çåæ³ä¸å¯è¡ï¼2026-09-19 ä¿®æ£ï¼ å稿åã`group.hidden` ç¨å¸å°ï¼`buildContent` éåæå®ææ `display:none`ããè¿**åä¸å°**ï¼æä¸¤å¤ç¡¬å²çªï¼ - `binding.ts:4` æåãåªååå¼ä¸æ¿æ¢ï¼ä¸åä»»ä½è¡¨è¾¾å¼æ±å¼ãï¼`{{...}}` åå°çæ¯åé¢éï¼ä¸å¯è½åæ CSSï¼ - `buildContent` æ¯**设计æéæ**çï¼å®æ¿ä¸å°è¿è¡æçè¡æ°æ®ï¼æ´è°ä¸ä¸æè¡ææ ·å¼ã äºæ¯æ¹æãæéèå¼äº¤ç»æ°æ®ä¾§ãã䏿¡éé宿µï¼èæ¬èµ°çå® `grapesjs.init` â `getWrapper().append()` â `getProjectData()` â `loadProjectData()`ï¼å³è®¾è®¡å¨ççå®åºååè·¯å¾ï¼ï¼ | éé | GrapesJS è§£ææ | `getHtml()` | é¡¹ç®æ°æ®å¾è¿ | è½æè¡å | |---|---|---|---|---| | å 容éå `style="{{è·¯å¾}}"` | åªä¿çéæ CSS è¿èç¹ `style` 对象ï¼**ç»å®ä¸²è¢«æ´æ¡ä¸¢å¼** | 丢失 | çå¨ attributesï¼ä½ç»å¸/HTML é已没æ | å¦ | | `attributes.style="{{è·¯å¾}}"` | åæ ·çå¨ attributesï¼ä¸è¿ `style` å¯¹è±¡ï¼ | **丢失** | ä¿ç | æ¯ï¼ä½ç»å¸çä¸å°ï¼ | | **`hidden="{{è·¯å¾}}"`** | æ®é屿§ï¼åæ ·çå¨ attributes | **ä¿ç** | ä¿ç | **æ¯** | **éç¨ `hidden`**ãåå 两æ¡å®æµæå®çè¡ä¸ºéæ»ï¼ - Chromium é `td[hidden]` 宿µ `display: none`ãé«åº¦ 0ï¼ä¸ `style="display:none"` çæ ââ èæå¡ç«¯ PDF ç¨çå°±æ¯åä¸ä¸ª Chromiumï¼`PdfRenderService` â Playwrightï¼ï¼æä»¥æµè§å¨éçç»è®ºç´æ¥éç¨äºæå°äº§ç©ï¼ - 两侧渲æå¨**é½è·³è¿ãæ±å¼å为空串ãç屿§**ï¼å端 `render.ts:234`ãå端 `HtmlRenderer.java:214`ï¼ï¼äºæ¯ãé¦è¡ç»ç©ºä¸² â ä¸è¾åºè¯¥å±æ§ â å¯è§ï¼å ¶ä½è¡ç»ä¸ä¸ªéç©ºå¼ â è¾åº â éèãæ¯ä¸¤ä¾§éåä¸è´çç¡®å®æ§è¡ä¸ºã ä¸¤ä¸ªå®æµå¾å°ç硬约æï¼å¿ é¡»åè¿å¥çº¦ï¼ - **`{{item.group.children.length}}` åä¸å°å¼**ï¼è·¯å¾å¨æ°ç»ä¸ç»§ç»å屿§ä¼æå ç´ pluckï¼`.length` 读ä¸å°ã**è·¨è¡æ°å¿ é¡»ç±æ°æ®ä¾§æ¾å¼ç»åº**ï¼`group.span`ï¼ã - **ç»å®çåæ®µå¿ 须卿¯ä¸é¡¹ä¸é½åå¨**ï¼åªææ¯ç©ºä¸²ï¼ãåæ®µç¼ºå¸ä¼æ¸²æä¸ºç©ºå¹¶è®°ä¸æ¡ãå¨å½åæ°æ®ä¸åä¸å°å¼ãçé®é¢ï¼`hidden` è¥åå° `undefined`ï¼å±æ§ä¼è¢«è·³è¿ â 该éèçè¡åèé²åºæ¥ã ## åãæ¹æ¡ ### 4.1 æ°æ®ä¾§å¥çº¦ å¨**ç°æ `inspectionItems` çæ¯ä¸é¡¹**ä¸å¢å ï¼æ°ç»éä»ç¶åªæç宿£éªé¡¹ï¼**ä¸å¡åæç»å¤´è¡ä»¥å¤çä»»ä½ä¸è¥¿**ï¼ç»å¤´æ¬èº«å°±æ¯åæ®éçä¸é¡¹ï¼ï¼ | åæ®µ | ç±»å | å«ä¹ | |---|---|---| | `item.requirement` | String | æ¬è¡ãæ£æµè¦æ±ãææ¬ï¼**åç»ç¶é¡¹ = `formula_text` å ¬å¼**ï¼å ¶ä½ = åæ®ä¸çæ åè¦æ± | | `item.group.childName` | String | ãå项ãåææ¬ï¼**ç»å å项 = èªå·±çåå**ï¼ç»å éç¹è¡ï¼å«å¤æ ·åå¤åºæ¥çéç¹è¡ï¼ä¸ç¬ç«é¡¹ = 空串 | | `item.group.span` | Integer | **ãæ£éªé¡¹ç®ãå**ç `rowspan`ï¼**ç»å 第ä¸è¡ = 该ç»çæ»è¡æ°**ï¼éç¹è¡ + å项è¡ï¼ï¼åç»å ¶ä½è¡ä¸ç¬ç«é¡¹ = 1 | | `item.group.hidden` | String | **ç»å 第ä¸è¡ = 空串**ï¼è¿ä¸æ ¼å¯è§ï¼å¹¶åä¸åå¹¶ï¼ï¼**åç»å ¶ä½è¡ = `hidden`**ï¼è®©ä½ç»ä¸é¢çåå¹¶æ ¼ï¼ï¼ç¬ç«é¡¹ = 空串 | > **åå¹¶å£å¾ç»ä¸æä¸æ¡è§åï¼ç»å 第ä¸è¡è `span = è¯¥ç»æ»è¡æ°`ï¼åç»å ¶ä½è¡ä¸å¾ `span = 1` + `hidden = hidden`ã** > è¿æ ·å¤æ ·åçç»å¤´ï¼ä¸ä¸ªææ å¤è¡ï¼ä¹èªå¨è½å¨è§åé ââ åªæç¬¬ä¸è¡å¯è§å¹¶åå¹¶ï¼å¤åºæ¥çéç¹è¡è®©ä½ï¼ > æ¢ä¸ä¼å¤åºä¸æ ¼ç©ºç½ï¼ä¹ä¸ä¼æåæ¤æªã > **åªåå¹¶ãæ£éªé¡¹ç®ã䏿 ¼ï¼ä¸åå¹¶ãæ£æµè¦æ±ã**ï¼docx åççå¼éåå¹¶çåªæé¡¹ç®åï¼æ åè¦æ±ä»æ¯éè¡ç > ï¼å®æµ `20ç®ä¸` / `0-5` é£ä¸è¡å°±æ¯ç¬ç«çæ åè¦æ±ï¼ãè¥ææ£æµè¦æ±ä¹å¹¶è¿ç»å¤´æ ¼ï¼ç»å å项åèªçæ åè¦æ± > å°±æ å¤å¯æ¾ãä¼è¢«ç¿»æãç»å¤´çå ¬å¼åªå ç»å¤´éç¹è¡èªå·±é£ä¸æ ¼ã 䏤侧忥ï¼å¦åå端é¢è§ä¸å端åºä»¶ä¸ä¸è´ï¼ï¼ - å端 `engine/context/ReportContext.java`ï¼`InspectionItem` å `requirement` ä¸åµå¥ `ItemGroup group`ï¼`itemMap()` æ¾å¼æ¾å ¥ï¼è¯¥æ¹æ³éåæ®µæä¸¾ï¼ä¸é åå°ï¼`group` 为 null æ¶ä¸æ¾ï¼é¿å 污æå»ç» fixture çä½ç¨åï¼ã - å端 `components/quality/engine/context.ts`ï¼`ReportContextItem` å ååçå¯é `requirement` ä¸ `group`ã `hidden` æ¯**å符串è䏿¯å¸å°**ï¼è¿ä¸ªå±æ§é ãå¨ä¸å¨ãèµ·ä½ç¨ï¼`hidden="false"` ç §æ ·éèï¼æä»¥åªè½ç¨ã空串 = ä¸è¾åºãè¿ä¸ä¸ªå¯å¤å®çå½¢æãåååå¾ç´ç½ï¼å¼å°±æ¯è¯¥å±æ§çå¼ã ### 4.2 åç»è¯å«ä¸æ¥åé¡ºåº - **è°æ¯ç»å¤´**ï¼æé¡¹ææ è¢«æ¬åéå«çè¡ç¨ `parent_id` æè®¤ï¼å®å°±æ¯ç»å¤´ãä¸ä¾èµ `item_type`ï¼ä¹ä¸ä¾èµè¡åº ââ è¡åºæ¯é ä¸ä½çï¼ç¶é¡¹é£è¡ä¸å项ä¸ç¸é»ï¼ã`item_type = 2`ï¼åç»é¡¹ï¼åªç¨æ¥å¤æè¿ä¸ªç¶ææ ãå¤ä¸å¤æ ¼å½ç»å¤´ãï¼ä¸åä¸å½å±å¤æã - **æ¥å顺åº**ï¼é¡¶å±æ¡ç®ï¼ç¬ç«é¡¹ä¸ç»ï¼æææ ç `sort_order` ååºï¼`sort_order` ç¼ºå¤±çææåï¼åå¼ä¿æè¡åºï¼ç¨³å®æåºï¼ï¼ç»å å项æèªå·±ç `sort_order`ã宿µè¿ç»æ°æ®æåºæ¥æ¯ å¤è§(10) â è²åº¦(20) â æ°´åç»(30) â é ¸å«éç»(40) â æ£ä¸éå«éç»(50) â å¯åº¦(60) â æå ç(70)ï¼ä¸ docx å `sort_order` çæå¾ä¸è´ã - è¿æ¯**å¯ä¸ä¸å¤å¯¹æ¢æè¡ä¸ºçæ¹å¨**ï¼æ¤åæ¥åè¡åº = è¡è¡¨é¡ºåºã`sort_order` æ¬å°±æ¯è¿ä»½ä¸»æ°æ®é表æãæ¾ç¤ºé¡ºåºãçåæ®µï¼ä¸æ¬åéç¬ç«é¡¹ä¸¤è æ°å¥½ä¸è´ï¼å®é å½±åé¢å¾å°ãè¥ä¸å¡è¦æ±ä¸¥æ ¼æè¡åºï¼æ¹åä¸è¡å³å¯ï¼æåºé®æ¢æè¡åºï¼ã - **ç»å¤´èªå·±é£ä¸è¡**ï¼ç»å¤´ææ é叏卿¬åé乿è¡ï¼æ°´åå°±æ¯ç¬¬ 1 è¡ï¼ï¼å®ä½ä¸ºåå¹¶æ ¼çéç¹è¡è¾åºã - **ç»å¤´ææ 卿¬åéæ²¡æè¡**ï¼æ£éªæ¨¡æ¿åªå¾äºå项æ¶ä¼è¿æ ·ï¼ââ **ä»ç¶ä¸ºå®è¡¥ä¸è¡**ï¼å¦åç»åä¸å ¬å¼æ å¤å¯æ¾ãæ¥åä¸è¿ç»å°±æ£æäºéæ¡æç»ãè¡¥åºæ¥çè¿ä¸è¡ï¼å®æµå¼ä¸åä½ä¸ºç©ºã`requirement` åå ¬å¼ã`span` èæ´ç»è¡æ°ï¼è§ §4.1 çåå¹¶å£å¾ï¼ï¼å¹¶å¨ååºç `synthesizedGroupNames` é说æä¸ºåªå 个ç»å¤´è¡¥äºè¡ã为ä»ä¹ã - æ©ææ¹æ¡æ¾æè¿ç§æ åµå¤ä¸ºãç»ä¸æç«ãæç¬ç«é¡¹å¤çãï¼é£æ ·ä¼**åæ ·å¤ç°æ¬æ¬¡è¦ä¿®ç bug**ï¼ç»åä¸å ¬å¼å次丢失ï¼ã宿µç¡®è®¤è¿æ¡è·¯å¾å¯è¾¾ï¼`mes_qc_template_indicator` å³å®æ¬åæåªäºè¡ï¼æ¨¡æ¿å¯è½åªå¾å项ï¼ï¼æä»¥æãè¡¥è¡ãå®ç°ã - **ç»å¤´ææ 卿¬åéæ ¹æ¬æ²¡æå项**ï¼å³å®æ¯ä¸ä¸ªåç»é¡¹ï¼ä½æ²¡æä»»ä½è¡ç¨ `parent_id` æåå®ï¼ââ æç¬ç«æ£éªé¡¹ååºãç»åä¸åå¹¶ï¼å¹¶æå®çååæ¶è¿ `childlessGroupNames` 说æã ### 4.3 渲æç»æ æ°å¢ç»ä»¶ `GroupedQualityTable`ãåç»æ£éªé¡¹è¡¨ãï¼ä¸æ¹é `QualityTable`ï¼åè è¦å ç»ç¸å ³ props å¹¶å¨ `buildContent` é忝ï¼å±æ§é¢æ¿å¤æåº¦ä¸åï¼ä¸æ°ç»ä»¶è½ä¿è¯æ¢æç»å¸é¶å½±åï¼ã 7 åï¼å ¶ä¸ç¬¬ 2 æ ¼ï¼æ£éªé¡¹ç®ï¼å¨ç»å¤´è¡çºµååå¹¶ï¼ | å | ç»å® | ç»å¤´è¡ | ç»å åé¡¹è¡ | ç¬ç«é¡¹ | |---|---|---|---|---| | 1 åºå· | `{{index}}` | è¡å· | è¡å· | è¡å· | | 2 æ£éªé¡¹ç® | `{{item.itemName}}` + `rowspan="{{item.group.span}}"` + `hidden="{{item.group.hidden}}"` | ç»åï¼åå¹¶ï¼ | è¯¥æ ¼éè | èªå·±çåå | | 3 å项 | `{{item.group.childName}}` | 空 | èªå·±çåå | 空 | | 4 æ£æµè¦æ± | `{{item.requirement}}` | **å ¬å¼** | èªå·±çæ åè¦æ± | èªå·±çæ åè¦æ± | | 5 宿µå¼ | `{{item.actualValue}}` | ç»å¤´èªå·±çå¼ | èªå·±ç宿µå¼ | èªå·±ç宿µå¼ | | 6 åä½ | `{{item.unit}}` | åä¸ | åä¸ | åä¸ | | 7 å¤å® | `{{item.resultText}}` | æ å¤å®è§å | èªå·±çå¤å® | èªå·±çå¤å® | ç»æè¦ç¹ï¼ 1. å¤å±ä»æ¯**åå±** `repeat`ï¼è·¯å¾ `inspectionItems`ï¼é»è®¤å¼ï¼å±æ§é¢æ¿å¯æ¹ï¼ã 2. å两åä¹å¤ç §æ¬ `QualityTable` çéæåå æ ¼æ ·å¼ï¼`components` è¿å HTML å符串ï¼ä¸å ¶ä½ 11 个ç»ä»¶åæ¬¾åæ³ï¼ã 3. `rowspan` / `hidden` å¿ é¡»åå¨**屿§**ä¸ï¼ä¸è½åè¿è¡å `style`ï¼è¡å æ ·å¼ä¼è¢« GrapesJS è§£ææèç¹ `style` 对象ï¼è `renderStyle` ä¸è·ç»å®ï¼ã 4. `GroupedQualityTable` ä¸ `QualityTable` **äºè ç¨å ¶ä¸**ï¼åè ç¨äºæåç»çåæ®ï¼åè ç¨äºçº¯å¹³çæ£éªé¡¹è¡¨ã ## äºãè½å°æ¸ å **å端 `yudao-module-mes`** - `MesQcIndicatorDO`ï¼è¡¥æ å° `parent_id` / `item_type` / `formula_text` / `unit_text` / `sort_order`ã - `MesQcReportItemRespDTO`ï¼å `requirement` ä¸ `groupChildName` / `groupSpan` / `groupHidden`ï¼è·¨æ¨¡å API åæ´ â é `mvn compile -pl yudao-module-qcreport -am -q`ï¼ã - `MesQcReportApiImpl.buildItems`ï¼æ¹ç¨ `parent_id` å¤å½å±ï¼ä¸åç¨ `result_type IS NULL`ï¼ä¹ä¸å丢ç¶é¡¹ï¼ï¼æ `sort_order` æåºï¼æç»ä¿¡æ¯ä¸åå°ç»å¤´çéç¹è¡ä¸æ¯ä¸ªå项ã - `MesQcReportRespDTO`ï¼å `groupItemNames` ææä¸¤ä¸ªå说ä¸ä»¶äºçåæ®µ ââ `childlessGroupNames`ï¼æ¯åç»é¡¹ã使¬å没æå±äºå®çå项ï¼ä¸ `synthesizedGroupNames`ï¼æ¬å没æèªå·±çè¡ã被补äºä¸è¡æ¥æ¾ç»åä¸å ¬å¼ï¼ï¼å¦æ `missingIndicatorCount`ï¼æç»å¼ç¨çææ å·²è¢«å é¤ï¼ã **å端 `yudao-module-qcreport`** - `engine/context/ReportContext.java`ï¼`InspectionItem` å `requirement` ä¸ `group`ï¼`itemMap()` 带ä¸ã - `MesQcReportContextMapper`ï¼æ MES ç `requirement` / `group*` éè¿æ¥åä¸ä¸æï¼ç»å¤´éç¹è¡ä¸åæ¥ãå°æªå½å ¥å®æµå¼ãï¼å®æå®ä¹æ²¡æå®æµå¼ï¼ï¼`appendSkipWarnings` ææ¡éç¶é¡¹ä¸å被丢å¼èè°æ´ã **å端 `mom-pro2-before`** - `engine/context.ts` ç `ReportContextItem` å `requirement` / `group`ã - æ°å¢ `inspection/grouped-quality-table.ts` 并注åï¼`aiHint` 忏 ãæåç»æ¶ç¨å®ï¼æ åç»ç¨ QualityTableãã - `.qc-conformance/assemble-ts.ts`ï¼`definitions.length === 12`ï¼ç¬¬ 100 è¡ï¼ã`catalog.length === 12`ï¼ç¬¬ 255 è¡ï¼ãåæ®µéåæè¨é½è¦é第 13 个ç»ä»¶æ´æ°ã **æµè¯ä¸å¯¹æ** - å端åéå¿ é¡»å ¨ç»¿ï¼`FrontendConformanceTest` éåæ¯å¯¹çææäº§ç©åªæ¯ HTML ä¸è§åç»æï¼æ°å¢ç `group` ä¸åä¸ä»»ä½ fixture 表达å¼ï¼æ ä¸å¿ éçæï¼ã宿µï¼`yudao-module-qcreport` 141 é¡¹å ¨ç»¿ã - å端æ¢é `.qc-conformance/verify-grouped-table.ts`ï¼ç¨çå®å¼æè·ãç»å¤´ + å项 + ç¬ç«é¡¹ãæ··æçä¸ä¸æï¼æè¨åå¹¶æ ¼ãéèæ ¼ãå对é½ä¸ãæ å¤å®è§åãä¸åå½±åã**å·²è½å°å¹¶å ¨ç»¿**ï¼å对é½å¦æä¸æ¬¡æµè§å¨å®æµï¼è§è¿åº¦ææ¡£ï¼ã - åç»ç»å¸ fixture è¿ `FrontendConformanceTest` çåä¾§é忝坹 ââ **æ¬è½®æªå**ï¼åªåå端æ¢éãçç±ï¼è¯¥ fixture è¦æ±å端å 产åºãææ HTMLãåç± Java é忝坹ï¼èåç»ç»æçå ³é®é£é©ï¼`rowspan` 让ä½å忝å¦ä¸²ä½ï¼åå³äºæµè§å¨è¡¨æ ¼å¸å±ï¼é忝坹åºå®ä¸ä½è¿ä¸æ¡ï¼æ¢é + æµè§å¨å®æµææ¯ææè¯æ®ã ## å ãå·²å®çå£å¾ï¼åãå¾ ç¡®è®¤ãï¼ 1. **è¿ç¨åæ°è¡çå¤å®å£å¾** ââ å·²å®ç°ãæ å¤å®è§åãï¼æ¢æ²¡æè§æ ¼ä¸ä¸éã乿²¡æå¤å®è§åçè¡æ ä¸ºãæ å¤å®è§åãï¼ä¸è®¡å ¥åæ ¼ç忝ãä¸å䏿¥åç»è®ºï¼è¿åªæ¯æ¾ç¤ºä¸ç»è®¡å£å¾ï¼**䏿°å¢ä¹ä¸å 餿£éªé¡¹**ï¼`total` 仿¯çå®é¡¹æ°ã 2. **ç»å¤´ä¸¤åç宽度** ââ ç»åä¸å ¬å¼åå ä¸åï¼**æ£æµè¦æ±ååæ¶æ¿æ ãç»å¤´æ¾å ¬å¼ãä¸ãå ¶å®è¡æ¾æ åè¦æ±ã**ï¼å®æµæ¬åæææ åè¦æ±ä¸ºç©ºï¼ä¸ä¼äºç¸è¦çï¼ãä¸å¦å¼ä¸åï¼é¿å çº¯å¹³åæ®åºç°æ´å空ç½ã 3. **AI å¯¼å ¥** ââ æ¨¡åè¦è½å¨ `QualityTable` ä¸ `GroupedQualityTable` ä¹é´é对ï¼ä¾èµ `aiHint` ä¸æç¤ºè¯è§åï¼è¯å«åç¡®çæ æ³å éªä¿è¯ï¼éçå®è°ç¨è§å¯åå¦å®æ±æ¥ã ## ä¸ãæç¡®ä¸å - 䏿¹ `HtmlRenderer` / `BASE_CSS`ï¼P5 å·²éªè¯é¶æ¹å¨å¯è¡ï¼ã - 䏿¹ Schema å¥çº¦ 1.1ã䏿°å¢é¡¶å±ä¸ä¸ææ°ç»ã - ä¸ä¸ºãåå¹¶åå æ ¼ãå¼å ¥ `tbody` åµå¥ï¼P4 å¯è¡ä½å±éçº§å ¼å®¹è·¯å¾ï¼ä¸æ±¡æç»è®¡ï¼ä¸éç¨ï¼ã - ä¸å¨å±æ§é¢æ¿å¼å ¥ç»æå propsï¼æ¢æè½å䏿¯æï¼ä¸æ å¿ è¦ï¼ã - 䏿ç»å å项ååä¸åµå¥ï¼å½åæ°æ®åªæä¸¤å±ï¼çè¦æ´æ·±çå±çº§ï¼`parent_id` å·²è½è¡¨è¾¾ï¼ä½æ¸²æç»æè¦éæ°è®¾è®¡ï¼ã docs/qc_report_indicator_hierarchy_frontend_integration.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,196 @@ # è´¨æ£ææ å±çº§ç»´æ¤ - å端èè°æ¹æ¡ > åæ´æ¨¡åï¼`yudao-module-mes`ï¼è´¨æ£ææ ä¸»æ°æ®ï¼ > åæ´ç®çï¼è®©ãåç»é¡¹ / æå±åç» / å ¬å¼ / æåºå·ãè¿å个å±çº§å段å¯ä»¥å¨**è´¨æ£ææ 页**ç»´æ¤ï¼ > ä¸ååªè½é SQL é æ°æ®ã > æ¥åä¾§çåæ°é»è¾**ä¸è¡æªæ¹** ââ çé¢é åºæ¥çåç»ä¸ SQL é åºæ¥çåç»ï¼å¨æ¥åéå®å ¨åå½¢ã ## æ¶åé¡µé¢ - è´¨æ£ææ å表页ï¼èåï¼è´¨é管ç â è´¨æ£ææ ï¼ ## ä¸å¡æµç¨ä¸æ°æ®å¸¦å ¥ 1. å¨è´¨æ£ææ 页æ°å»ºãåç»é¡¹ãâ è¯¥ææ æä¸º**é¡¶çº§ææ **ï¼`æå±åç»` åºå®ä¸ºé¡¶çº§ã`å ¬å¼` å¿ å¡«ã 2. æ°å»ºãå½å ¥é¡¹ãâ å¨ `æå±åç»` ééæ©ç¬¬ 1 æ¥å»ºçåç»é¡¹ â è¯¥ææ æä¸ºè¿ä¸ªåç»çå项ã 3. æ£æµæ¹æ¡ â è´¨æ£å â åºæ¥åï¼æ¥åä¾§æ `parent_id` æåç»é¡¹ä¸å®çå项è¿åæä¸¤å±è¡¨å¤´ï¼ ç»å纵ååå¹¶ãå ¬å¼è¿ãæ£æµè¦æ±ãåã**è¿ä¸æ¥çå±ç¤ºææä¸æ°æ®æ¥èª SQL æ¶å®å ¨ä¸è´ã** ## API | æ¹æ³ | è·¯å¾ | 说æ | |------|------|------| | GET | `/mes/qc/indicator/page` | å页æ¥è¯¢ï¼æé `mes:qc-indicator:query`ï¼ | | GET | `/mes/qc/indicator/group-list` | **æ°å¢**ï¼åç»é¡¹ç²¾ç®åè¡¨ï¼æé `mes:qc-indicator:query`ï¼ | | POST | `/mes/qc/indicator/create` | æ°å¢ï¼æé `mes:qc-indicator:create`ï¼ | | PUT | `/mes/qc/indicator/update` | ä¿®æ¹ï¼æé `mes:qc-indicator:update`ï¼ | | DELETE | `/mes/qc/indicator/delete` | å é¤ï¼æé `mes:qc-indicator:delete`ï¼ | | GET | `/mes/qc/indicator/export-excel` | 导åºï¼æé `mes:qc-indicator:export`ï¼ | æ¥å£åç¼ä¸º `/admin-api`ï¼ä¸æ¢æçº¦å®ä¸è´ï¼ä¸æè·¯å¾åçç¥è¯¥åç¼ã ### GET /mes/qc/indicator/group-list ä¸ä¾ãæå±åç»ã䏿ã**åªè¿ååç»é¡¹**ï¼`item_type = 2`ï¼ï¼ä¸è¿åå½å ¥é¡¹ã æ 请æ±åæ°ãååºä¸ºææ 对象æ°ç»ï¼å段å䏿¹ååºåæ®µè¡¨ï¼æ¤å¤åªä¿è¯ `id` / `name` æå¼ï¼ã ### 请æ±åæ°ï¼create / updateï¼ `update` ä¸ `create` å ±ç¨åä¸ä¸ªè¯·æ±ä½ãæ°å¢å段æ â ã | åæ° | ç±»å | å¿ å¡« | 说æ | |------|------|------|------| | id | Long | update å¿ å¡« | ç¼å· | | code | String | æ¯ | æ£æµé¡¹ç¼ç | | name | String | æ¯ | æ£æµé¡¹åç§° | | type | Integer | æ¯ | æ£æµé¡¹ç±»åï¼åå ¸ `mes_indicator_type`ï¼ | | tool | String | å¦ | æ£æµå·¥å · | | â itemType | Integer | å¦ | æ¡ç®ç±»åï¼`1`=å½å ¥é¡¹ï¼é»è®¤ï¼ï¼`2`=åç»é¡¹ | | â parentId | Long | å¦ | ç¶ææ ç¼å·ï¼`0` æç空 = é¡¶çº§ææ | | â formulaText | String | åç»é¡¹å¿ å¡« | å ¬å¼å±ç¤ºææ¬ï¼æ¥åéä½ä¸ºè¯¥åç»çãæ£æµè¦æ±ã | | â sortOrder | Integer | å¦ | å级æåºå·ï¼ç空åèªå¨æå°å级æ«å°¾ | | resultType | Integer | å½å ¥é¡¹å¿ å¡« | ç»æå¼ç±»åï¼åå ¸ `mes_qc_result_type`ï¼ã**åç»é¡¹ä¸å¡«** | | resultSpecification | String | æ¡ä»¶å¿ å¡« | ç»æå¼å±æ§ã`resultType` 为ãæä»¶ãæãåå ¸ãæ¶å¿ å¡« | | remark | String | å¦ | 夿³¨ | > `resultType` åå 带 `@NotNull`ï¼æ¬è½®**ææäº**ï¼åç»é¡¹æç°ç½å£å¾å°±æ¯ `NULL`ï¼ç°ç½ 3 个åç»è¡ > ç `result_type` å ¨ä¸º NULLï¼ãæ¹ä¸ºæå¡ç«¯ææ¡ç®ç±»åæ¡ä»¶æ ¡éªï¼**åªæå½å ¥é¡¹å¿ å¡«**ã ### ååºå段æ°å¢ï¼page / export-excelï¼ | åæ®µ | ç±»å | 说æ | |------|------|------| | itemType | Integer | æ¡ç®ç±»å | | parentId | Long | ç¶ææ ç¼å·ï¼`0` 表示顶级 | | parentName | String | **æå±åç»åç§°**ï¼ç±å端åå¡«ï¼é¡¶çº§ææ 为 `null` | | formulaText | String | å ¬å¼ | | sortOrder | Integer | å级æåºå· | `parentName` ç±æ§å¶å¨åå¡«ï¼å弿¹å¼æ¯ï¼æ¶éæ¬é¡µææé `0` ç `parentId` â 䏿¬¡æ§æ id æ¹éæ¥ææ â 建 `id â name` æ å°åå¡«ãå æ¤**ç¶ææ å³ä½¿ä¸å¨å½å页ãçè³ä¸å¨çéç»æå ï¼åç§°ä¹è½æ£ç¡®è§£æ**ã > **`/get`ï¼è¯¦æ ï¼æ¥å£ä¸åå¡« `parentName`**ï¼ä¸ `/page`ã`/export-excel` ä¸ä¸è´ã > ç¼è¾å¼¹çªçæ°æ®æ¥æºæ¯å表è¡ï¼å表è¡å·²å¸¦ `parentName`ï¼ï¼ä¸åå½±åï¼è¥åç»æé¡µé¢ç´æ¥è° `/get` å±ç¤º > æå±åç»ï¼éè¦å¦è¡å¤çã ### ååºç¤ºä¾ ```json { "code": 0, "data": { "list": [ { "id": 21, "code": "SEED-BT-WATER", "name": "æ°´å", "type": 4, "itemType": 2, "parentId": 0, "parentName": null, "formulaText": "w = (m1 - m0) / m à 100%", "sortOrder": 30, "resultType": null, "resultSpecification": null }, { "id": 22, "code": "SEED-BT-WATER-M", "name": "è¯æ ·è´¨é m", "type": 4, "itemType": 1, "parentId": 21, "parentName": "æ°´å", "formulaText": null, "sortOrder": 1, "resultType": 1, "resultSpecification": null } ], "total": 34 } } ``` ## åæ®µå±ç¤ºè§å | åæ®µ | å±ç¤ºä½ç½® | 说æ | |------|----------|------| | æ¡ç®ç±»å | å表åãæ°å¢/ä¿®æ¹è¡¨å | åè¡¨ç¨æ ç¾å±ç¤ºï¼`åç»é¡¹` / `å½å ¥é¡¹` | | æå±åç» | å表åãæ°å¢/ä¿®æ¹è¡¨å | å表å `parentName`ï¼ä¸º `null` æ¶æ¾ç¤º `-`ï¼è¡¨ç¤ºé¡¶çº§ææ ï¼ | | å ¬å¼ | æ°å¢/ä¿®æ¹è¡¨å | **ä» åç»é¡¹å¯è§ï¼ä¸å¿ å¡«** | | æåºå· | æ°å¢/ä¿®æ¹è¡¨å | æ°åè¾å ¥ï¼æå° `0`ï¼çç©ºç±æå¡ç«¯è¡¥é½ | | æ¡ç®ç±»å | æç´¢è¡¨å | 䏿ï¼å½å ¥é¡¹ / åç»é¡¹ | **ãæå±åç»ã䏿¯æ å½¢å±ç¤º**ï¼åç»é¡¹åªè½æ¯é¡¶çº§ï¼æä»¥åé项就æ¯ä¸å±å¹³éºçåç»é¡¹å表 ï¼å¤å±å¥ä¸ä¸ªãé¡¶çº§ææ ãæ ¹èç¹ï¼ã䏿ä¾å¤çº§æ ã ## ä¸å¡è§å说æ ### å±çº§è§åï¼åªå 许两å±ï¼ | åºæ¯ | è§å | |------|------| | åç»é¡¹ | å¿ é¡»æ¯**é¡¶çº§ææ **ï¼`æå±åç»` 项å¨è¡¨åä¸**ä¸åºç°**ï¼è½åºåºå® `parentId = 0` | | å½å ¥é¡¹ | 坿å°ä»»æåç»é¡¹ä¸ï¼ä¹å¯ä½ä¸ºé¡¶çº§ææ ï¼`æå±åç»` çç©ºï¼ | | 为ä»ä¹åªæä¸¤å± | è§åæ¬èº«æ¨åºï¼ç¶å¿ é¡»æ¯**åç»é¡¹**ï¼èåç»é¡¹**åªè½é¡¶çº§** â 第ä¸å±æ æ³æé | **ãæå±åç»ãå¨è¡¨åä¸çåéåªæåç»é¡¹**ï¼æ°å»ºå½å ¥é¡¹æ¶ä¸å¯è½éå°èªå·±ï¼ä¹ä¸å¯è½éå°å¦ä¸ä¸ªå½å ¥é¡¹ã ### æ ¡éªè§åä¸æç¤ºææ¡ æå¡ç«¯æä¸å¡ä¼å 级**åªè¿å第ä¸å¤ä¸æ»¡è¶³çæ¡ä»¶**ï¼é¿å 䏿¬¡æå¤æ¡è®©ç¨æ·æä¸ä½éç¹ã | é误ç | è§¦åæ¡ä»¶ | æç¤ºææ¡è¦ç¹ | |--------|----------|--------------| | `1040601007` | åç»é¡¹å´éäºæå±åç» | å«åç»å + æéç¶ææ å + æ¹æ³ï¼ææ¡ç®ç±»åæ¹ä¸ºå½å ¥é¡¹åéåç»ï¼ | | `1040601006` | ç¶ææ æ¯èªå·± | ãç¶ææ ä¸è½æ¯èªå·±ã | | `1040601004` | ç¶ææ ä¸åå¨ï¼å·²è¢«å é¤ï¼ | å«ç¶ææ ç¼å· + æ¹æ³ï¼éæ°éæ©ææ¸ ç©ºï¼ | | `1040601005` | ç¶ææ 䏿¯åç»é¡¹ | å«ç¶ææ å + å®å½åçæ¡ç®ç±»å䏿å + æ¹æ³ï¼æ¹éåç»é¡¹ / å æå®çæ¡ç®ç±»åæ¹ä¸ºåç»é¡¹ï¼ | | `1040601009` | å½å ¥é¡¹æªå¡«ç»æå¼ç±»å | ãå½å ¥é¡¹çç»æå¼ç±»åä¸è½ä¸ºç©ºã | | `1040601003` | ç»æå¼ç±»å为æä»¶/åå ¸ä½ç»æå¼å±æ§ä¸ºç©º | ãç»æå¼å±æ§ä¸è½ä¸ºç©ºã | | `1040601008` | **æ¹æå°åç»ä¸**æ¶ï¼è¯¥ææ ä¸å·²æå项 | 嫿æ å + åé¡¹æ¡æ° + ã请å å é¤æç§»èµ°è¿ N 个å项ã | | `1040601008` | **æ¹ä¸ºå½å ¥é¡¹**æ¶ï¼è¯¥ææ ä¸å·²æå项 | åä¸é误ç ï¼ææ¡æ¹ä¸ºãå¦åè¿äºå项ä¼å¤±å»æå±åç»ã | å两æ¡ç¨åä¸ä¸ªé误ç ã**ææ¡åèªè´´ååºæ¯**ï¼ä¸ä¸ªæ¯ãä¸è½åæå°å«çåç»ä¸ãï¼ä¸ä¸ªæ¯ ãä¸è½åè¿æ¥åæå项ãã ### è½åºæ¶çèªå¨å¤ç | åæ®µ | å¤ç | |------|------| | `parentId` | ç空补 `0` | | `itemType` | ç空补 `1`ï¼å½å ¥é¡¹ï¼ | | `resultType` / `resultSpecification` | **åç»é¡¹å¼ºå¶æ¸ 空**ãå½å ¥é¡¹æ¹åç»é¡¹æ¶ï¼æ§çç»æå¼ä¼è¢«çæ£åå `NULL` | | `sortOrder` | ç空èªå¨æå°å级æ«å°¾ï¼é¡¶çº§æ¥é¿ `10`ï¼å项æ¥é¿ `1` | > ãæ¹åç»é¡¹åç»æå¼è¢«æ¸ 空ãä¾èµ DO ä¸ç `updateStrategy = ALWAYS`ãMyBatis-Plus é»è®¤ç > `NOT_NULL` çç¥ä¼æ `null` åæ®µä» UPDATE è¯å¥éåé¤ï¼å¯¼è´æ§ç»æå¼æ®ç ââ è¿æ¯æ¬è½®ä¿®æç > ä¸ä¸ªçå®åå ¥ç¼ºé·ï¼å端æ éæç¥ã ### å é¤ **å é¤åç»é¡¹ä¼çº§èå é¤å®ä¸é¢çææå项ã** | æä½ | ç»æ | |------|------| | å é¤åç»é¡¹ | å®çå ¨é¨å项ä¸å¹¶å é¤ | | å é¤å½å ¥é¡¹ | åªå å®èªå·± | æ¥åä¾§å¯¹ææ è¢«å å·²æå åºï¼ææ 已被å é¤çæç»è¡åªè®¡å ¥ `missingIndicatorCount` è¿ `warnings`ï¼ ä¸ä¼å¯¼è´åºä»¶å¤±è´¥ã ## 注æäºé¡¹ - **ãæ¡ç®ç±»åãä¸å¨å¯¼åº Excel é**ã`æå±åç»`ã`å ¬å¼`ã`æåºå·` ä¸åå·²è¿ Excelï¼ æ¡ç®ç±»å没æå¯¹åºåï¼å¯ç»åãæå±åç»ãæ¯å¦æå¼å¤æï¼ãè¿æ¯æ¬è½®æç¡®æ¥åçå£å¾ã - **`åä½` åæ®µä¸å¯¹å端æ´é²**ï¼ä¹ä¸å¨æ°å¢/ä¿®æ¹è¡¨åéãåå ï¼è¯¥åå¨å ¨é¨å端代ç éæ²¡æä»»ä½è¯»åæ¹ï¼ æ¥åéçãåä½ãåèªæ£éªæ¹æ¡è¡ç计éåä½ï¼æ¾ä¸ä¸ªæ²¡äººè¯»çåæ®µè¿è¡¨ååªä¼è¯¯å¯¼å½å ¥äººã - **忢æ¡ç®ç±»å伿¸ 空å½å ¥é¡¹ä¸å±å段**ï¼è¡¨ååå°ãåç»é¡¹ãæ¶ï¼æå±åç»ä¼è¢«é置为顶级ã ç»æå¼ç±»åä¸ç»æå¼å±æ§ä¼è¢«æ¸ 空ãè¿æ¯å¿ è¦ç ââ å¦å被éèçåæ®µä»ä¼é表åæäº¤ï¼ æ®çç `parentId` ä¼è¢«å端å±çº§æ ¡éªé¡¶åæ¥ã - **ç¼è¾åç»é¡¹æ¶ï¼ãæå±åç»ãåæ®µä¸å¯è§ããå ¬å¼ãå¿ å¡«çæ**ã - **`sortOrder` å 许éå¤**ï¼æå¡ç«¯ä¸æ ¡éªå级æåºå·çå¯ä¸æ§ï¼ç¸åæåºå·çç¸å¯¹é¡ºåºä¸ä¿è¯ã - **`group-list` è¿åçæ¯å ¨é¨åç»é¡¹**ï¼ä¸æä»»ä½ç»´åº¦è¿æ»¤ï¼å¦æåç»é¡¹æ°éå¢é¿å°å¾å¤§ï¼ éè¦èèæ¹é 为å页ææç´¢å¼éæ©ã docs/qc_report_instance_frontend_integration.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,290 @@ # æºè½è´¨æ£æ¥å â åºä»¶æ¥å£ å端èè°æ¹æ¡ > å端模åï¼`yudao-module-qcreport`ãæ¥å£åç¼ï¼`/admin-api/qc-report/instance` > æ¬è½®èå´ï¼**HTML åºä»¶ + PDF åºä»¶**ãPDF ç±åç«¯è° Chromium æå°ï¼æ¥å£åªè¿å彿¡£ç»æä¸ç¾åå°åï¼ > æ¥åå®ä¾è¡¨**ä¸åæä»¶å°å**ï¼PDF ç»ä¸æå¨éä»¶åºï¼`system_storage_attachment`ï¼éã ## æ¶åé¡µé¢ | é¡µé¢ | è·¯ç± | 说æ | ç¶æ | |---|---|---|---| | æ¥åå®ä¾å表页 | `/qc/report-instance` | å页æ¥çå·²åºä»¶çæ¥åï¼ææ¨¡æ¿/ç¼å·/ä¸å¡åæ®/ç¶æ/æ¶é´çé | **å·²è½å°**ï¼`views/mes/qc/report/instance/index.vue`ï¼ | | æ¥åé¢è§ / 详æ å¼¹çª | åä¸ | å±ç¤ºæ¸²æåºç HTMLï¼æ°æ®å¿«ç §é¡µç¾ï¼**ãå¯¼åº PDFãæé®è° `pdf` æ¥å£** | **å·²è½å°**ï¼`.../instance/modules/detail.vue`ï¼ | | 模æ¿è®¾è®¡å¨ï¼æ¢æï¼ | `/qc/report-template` | ãé¢è§ãæé®éè¦è°ç¨æ¬ææ¡£ç `preview` æ¥å£ | ä»å¾ æ¥å ¥ï¼æ¬è½®åªå äºãAI å¯¼å ¥ãæé®ï¼ | | MES è´¨æ£åï¼æ¢æï¼åç»ï¼ | â | ãåºæ¥åãå ¥å£ï¼æè´¨æ£åæ°æ®å½ä¸ååè° `generate` | **åç»è½®æ¬¡**ï¼æ¬è½®ç±è°ç¨æ¹èªè¡ç»ç»æ°æ®ï¼ | èåå·²å ¥åºï¼ãæ¥åå®ä¾ãæå¨ãè´¨é管çãï¼`parent_id=5500`ï¼ä¸ï¼`component` å¡« `mes/qc/report/instance/index`ï¼åç«¯å¨æè·¯ç±è§£æå°åå `.vue`ã ## ä¸å¡æµç¨ä¸æ°æ®å¸¦å ¥ 1. **è®¾è®¡å¨ â é¢è§**ï¼è®¾è®¡å¨æå½åç»å¸ä¿åæçæ¬åï¼ç¨ãå½åç»å¸å¯¹åºçæ¨¡æ¿ + çæ¬ãå ä¸ä»½ç¤ºä¾æ°æ®è°ç¨ `preview`ï¼ æ¿å° HTML ç´æ¥å±ç¤ºã**é¢è§ä¸æ¶èæ¥åç¼å·**ï¼å¯ä»¥åå¤è°ç¨ï¼**é¢è§æ¥åèç¨¿çæ¬**ï¼è§ãä¸å¡è§åãï¼ã 2. **ä¸å¡ç³»ç» â åºä»¶**ï¼è°ç¨æ¹ï¼æ¬è½®æ¯æå·¥æé ï¼åç»æ¯ MES è´¨æ£åï¼åå¤ãæ¥åä¸ä¸æãâ è° `generate` â å端渲æãè½åºãå»ç»æ°æ®å¿«ç § â è¿åå®ä¾ç¼å·ä¸æ¥åç¼å·ã 3. **å表 â 详æ **ï¼å表æ¿å° `id` â è° `get` å `renderHtml` å±ç¤ºï¼éè¦çã彿¶ç¨çä»ä¹æ°æ®ãåè° `snapshot`ã 4. **详æ â éæ°çæ**ï¼è° `regenerate` ç¨**å»ç»çå¿«ç §**鿏²ï¼è¦çäº§ç© HTMLãæ°æ®ä¸ç¼å·é½ä¸åã **已彿¡£ç PDF ä¼è¢«ä¸å¹¶ä½åº**ï¼è§ãä¸å¡è§åãï¼ã 5. **详æ â å¯¼åº PDF**ï¼ç¹ãå¯¼åº PDFãè° `pdf` æ¥å£ â å端æå°å¹¶å½æ¡£ â æ¥å£è¿åæ¬æ¬¡çä¸è½½å°åï¼ä¸´æ¶ç¾åï¼ã 页é¢è¦é¿ææ¾ç¤ºãè¿ä»½æ¥åæ PDFã请è°**éä»¶å表æ¥å£**ï¼è§ãPDF 彿¡£æä¹æ¥ãï¼ï¼ä¸è¦ç¼åæ¥å£è¿åçç¾åå°åã **å¸¦å ¥å ³ç³»è¦ç¹** - `templateId` æ¯ä¸»çº¿ï¼ä»æ¨¡æ¿åè¡¨å¸¦å ¥è®¾è®¡å¨ãåå¸¦å ¥åºä»¶ã - `version` å¯ä»¥ä¸ä¼ ï¼ä¸ä¼ æ¶å端å**模æ¿å½åççæ¬**ï¼`currentVersion`ï¼ï¼ä¸å®è¦ç¨æä¸ªç¹å®çæ¬æéè¦æ¾å¼ä¼ ã **设计å¨é¢è§å»ºè®®å§ç»æ¾å¼ä¼ çæ¬**ââè§ãä¸å¡è§åãé对 `currentVersion` ç说æã - `reportNo` å¯ä»¥ä¸ä¼ ï¼ä¸ä¼ æ¶ç±å端æ `QR + æ¥æ + 4 使µæ°´` èªå¨çæã - `businessId` / `businessType` åªåçç䏿¥è¯¢è¿æ»¤ï¼åç«¯ä¸æ ¡éªãä¸è§£æå®æåçåæ®ã ## API | æ¹æ³ | è·¯å¾ | 说æ | æéç | |---|---|---|---| | POST | `/qc-report/instance/preview` | åªæ¸²æï¼**ä¸è½åºãä¸çæã䏿¶èæ¥åç¼å·** | `qc-report:instance:query` | | POST | `/qc-report/instance/generate` | 渲æ + è½åº + å»ç»æ°æ®å¿«ç § | `qc-report:instance:create` | | GET | `/qc-report/instance/get?id=` | 详æ ï¼å ä¿¡æ¯ + 渲æäº§ç© HTMLï¼**ä¸å«**æ°æ®å¿«ç §ï¼ | `qc-report:instance:query` | | GET | `/qc-report/instance/snapshot?id=` | åå»ç»çæ°æ®å¿«ç § | `qc-report:instance:query` | | GET | `/qc-report/instance/page` | å页 | `qc-report:instance:query` | | POST | `/qc-report/instance/regenerate?id=` | ç¨å»ç»å¿«ç § + å䏿¨¡æ¿çæ¬é渲ï¼è¦çäº§ç© HTML | `qc-report:instance:create` | | POST | `/qc-report/instance/pdf?id=` | **æå°å®ä¾å·²åçäº§ç© HTML æ PDF 并彿¡£**ï¼å¯éå¤è°ç¨ | `qc-report:instance:export` | | DELETE | `/qc-report/instance/delete?id=` | å é¤å®ä¾ï¼**已彿¡£ç PDF ä¸å¹¶æ¸ ç**ï¼ | `qc-report:instance:delete` | > â å端å®ä¾é¡µè½å°æ¶ï¼`qc-report:instance:*` çèå䏿鮿é**å·²åæ¥è¡¥å ¥ `system_menu`** > ï¼èåãæ¥åå®ä¾ã+ æ¥è¯¢/åºä»¶/导åºPDF/å é¤åæ¡æé® + `qc-report:template:ai-import` 䏿¡ï¼ã > æªæè¿äºè¡ä¹åï¼éè¶ ç®¡è§è²è°è¿äºæ¥å£ä¼ 403ï¼è¶ 管ç±ä»£ç ç´è¿å ¨é¨èåï¼èè°ä»ä¸åå½±åã ### åºä»¶ / é¢è§è¯·æ±åæ°ï¼`preview` ä¸ `generate` åä¸å¥ï¼ | åæ° | ç±»å | å¿ å¡« | 说æ | |---|---|---|---| | `templateId` | Long | **æ¯** | 模æ¿ç¼å· | | `version` | String | å¦ | 模æ¿çæ¬å·ï¼å¦ `v1.0`ãä¸ä¼ åå该模æ¿ç `currentVersion` | | `reportNo` | String | å¦ | æ¥åç¼å·ãä¼ äºå°±ç¨å®ï¼æå·ä¼æ¥éï¼ï¼ä¸ä¼ ç±å端èªå¨çæ | | `businessId` | String | å¦ | ä¸å¡åæ®ç¼å·ï¼ä» çç䏿¥è¯¢ç¨ | | `businessType` | String | å¦ | ä¸å¡åæ®ç±»åï¼å¦ `mes_qc_oqc`ï¼ä» çç䏿¥è¯¢ç¨ | | `context` | Object | **æ¯** | æ¥åæ°æ®ï¼ç»æè§ä¸ | **`context` ç»æ**ï¼ä¸è®¾è®¡å¨éçæ°æ®å¥çº¦åæï¼ | åæ®µ | ç±»å | å¿ å¡« | 说æ | |---|---|---|---| | `report` | Object | å¦ | æ¥åçº§åæ®µãæ¾å¼ä¼ `null` ä¼è¢«åç«¯å æç©ºåæ®µéï¼ä¸ä¼æ¥é | | `report.reportNo` | String | å¦ | æ¥åç¼å·ã**é¡¶å± `reportNo` ä¼å äºå®** | | `report.reportName` | String | å¦ | æ¥ååç§° | | `report.sampleNo` | String | å¦ | æ ·åç¼å· | | `report.productCode` | String | å¦ | 产åç¼ç | | `report.productName` | String | å¦ | 产ååç§° | | `report.spec` | String | å¦ | è§æ ¼åå· | | `report.batchNo` | String | å¦ | æ¹æ¬¡å· | | `report.workOrderNo` | String | å¦ | å·¥åå· | | `report.inspectType` | String | å¦ | æ£éªç±»å | | `report.inspector` | String | å¦ | æ£éªå | | `report.inspectDate` | String | å¦ | æ£éªæ¥æ | | `report.department` | String | å¦ | é¨é¨ | | `report.customerName` | String | å¦ | 客æ·åç§° | | `report.supplierName` | String | å¦ | ä¾åºååç§° | | `report.result` / `report.resultText` / `report.conclusion` / `report.passRate` | String | å¦ | **å¤å®ç»æï¼ç±å端ç®åºå¹¶è¦ç**ï¼è°ç¨æ¹ä¸ç¨ä¼ | | `report.total` / `report.passCount` / `report.failCount` | Number | å¦ | **åä¸ï¼ç±å端ç®åº** | | `inspectionItems` | Array | **æ¯** | æ£éªé¡¹å表ï¼**ä¸è½ä¸ºç©º**ï¼ç©ºæ°ç»ä¼æ¥éï¼è§ãé误ç ãï¼ | | `inspectionItems[].index` | Number | å¦ | åºå·ï¼æ¸²æ `{{index}}` ç¨ | | `inspectionItems[].itemCode` | String | å¦ | æ£éªé¡¹ç¼ç | | `inspectionItems[].itemName` | String | å¦ | æ£éªé¡¹åç§° | | `inspectionItems[].standardValue` | String | å¦ | æ åå¼ï¼ææ¬ï¼å¦ `10±0.5`ï¼ | | `inspectionItems[].actualValue` | String | å¦ | 宿µå¼ | | `inspectionItems[].unit` | String | å¦ | åä½ | | `inspectionItems[].upperLimit` / `lowerLimit` | Number | å¦ | è§æ ¼ä¸ä¸éã**ä¸ä¼ å°±æ¯æ²¡æè§æ ¼åºé´**ï¼å¤å®ä¼è½å°è§åææ°æ®èªå¸¦ç»æä¸ | | `inspectionItems[].result` / `resultText` | String | å¦ | å¤å®ç»æï¼**ç±å端ç®åºå¹¶è¦ç** | | `inspectionItems[].remark` | String | å¦ | 夿³¨ | **请æ±ç¤ºä¾** ```json { "templateId": 5, "version": "v1.0", "businessId": "OQC20260918001", "businessType": "mes_qc_oqc", "context": { "report": { "reportName": "åºè´§æ£éªæ¥å", "productName": "æ³å °", "inspector": "å¼ ä¸" }, "inspectionItems": [ { "index": 1, "itemName": "å¤è§", "standardValue": "æ åç", "actualValue": "åæ ¼" }, { "index": 2, "itemName": "é¿åº¦", "standardValue": "10±0.5", "actualValue": 10.2, "upperLimit": 10.5, "lowerLimit": 9.5 } ] } } ``` ### ååºå段 **`preview` ååº** | åæ®µ | ç±»å | 说æ | |---|---|---| | `reportNo` | String | æ¬æ¬¡æ¸²æç¨çç¼å·ã**æ¥èªå ¥å**ï¼æ²¡ä¼ å°±æ¯ç©º ââ é¢è§ä¸çæç¼å· | | `html` | String | 宿´ HTML ææ¡£ï¼å¯ç´æ¥é¢è§/æå° | | `errors` | Array\<String\> | æ°æ®ç¼ºå£æ¸ åï¼è§ä¸ | **`generate` ååº** | åæ®µ | ç±»å | 说æ | |---|---|---| | `id` | Long | å®ä¾ç¼å·ï¼æ¿å®å»æ¥è¯¦æ | | `reportNo` | String | æç»çæçæ¥åç¼å·ï¼èªå¨çææå ¥åæå®ï¼ | | `templateVersion` | String | æ¬æ¬¡ä½¿ç¨ç模æ¿çæ¬å· | | `status` | Number | æ¥åç¶æï¼`0` çæä¸ / `1` çææå / `2` çæå¤±è´¥ãå½ååªä¼è¿å `1` | | `errors` | Array\<String\> | æ°æ®ç¼ºå£æ¸ å | **`regenerate` ååº** | åæ®µ | ç±»å | 说æ | |---|---|---| | `id` | Long | å®ä¾ç¼å· | | `reportNo` | String | æ¥åç¼å·ï¼ä¸çææ¶ç¸åï¼ | | `errors` | Array\<String\> | æ°æ®ç¼ºå£æ¸ å | **`pdf` ååº** | åæ®µ | ç±»å | 说æ | |---|---|---| | `id` | Long | å®ä¾ç¼å· | | `reportNo` | String | æ¥åç¼å· | | `fileName` | String | PDF æä»¶åï¼è§åæ¯ `<æ¥åç¼å·>.pdf`ï¼å¦ `QR20260918-0001.pdf`ï¼ | | `byteSize` | Long | PDF åèæ° | | `blobId` | Long | æä»¶ç¼å·ï¼`system_storage_blob` 主é®ï¼ | | `attachmentId` | Long | éä»¶å ³èç¼å·ï¼`system_storage_attachment` 主é®ï¼ | | `previewURL` | String | é¢è§å°åã**临æ¶ç¾åï¼ä¼è¿æ**ï¼å«åèµ·æ¥é¿æç¨ | | `downloadURL` | String | ä¸è½½å°åã**临æ¶ç¾åï¼ä¼è¿æ**ï¼ä¸è½½å¨ä½è¯·æ¿å³æ¶è¿åçè¿ä¸ªå¼ | | `durationMs` | Long | æ¬æ¬¡åºä»¶æ»èæ¶ï¼æ¯«ç§ï¼ï¼å«æéçå¹¶ååé¢ãå¿ è¦æ¶é坿µè§å¨ççå¾ | | `pdfDurationMs` | Long | æå°èæ¶ï¼æ¯«ç§ï¼ï¼ä» å«ç页é¢èµæºä¸æå°ãä¸ `durationMs` ä¹å·®å°±æ¯çå¾ ææ¬ | | `browserStatus` | String | `READY`ï¼å¤ç¨äºå·²å¨è¿è¡çæµè§å¨ï¼/ `RESTARTED`ï¼æ¬æ¬¡éå¯äºæµè§å¨ï¼ã**ä» ä¾åè䏿æ¥**ï¼å端ä¸éè¦åæ¯å¤ç | > `pdf` æ¯**æ¾å¼è§¦å**çï¼ç¨æ·ç¹ãå¯¼åº PDFãæçæãå®ä¸ä¼é `generate` / `regenerate` èªå¨è·ï¼ > å 为æ¯å¯¼ä¸æ¬¡é½è¦æèµ· Chromium æå°ï¼ç§çº§ï¼ï¼åºä»¶æ¥å£ä¸è¯¥èè¿ä¸ªå¼éã **`get` ååº**ï¼`id`ã`reportNo`ã`templateId`ã`templateVersion`ã`businessId`ã`businessType`ã`renderHtml`ã`status`ã`creator`ã`createTime`ã`updateTime`ã **注æä¸å« `dataSnapshot`**ï¼ä½ç§¯å¤§ä¸å表/详æ ç¨ä¸å°ï¼ï¼è¦å®è¯·è° `snapshot`ã **`snapshot` ååº**ï¼`{ "report": {...}, "inspectionItems": [...] }`ï¼å³åºä»¶å½æ¶å»ç»çé£ä»½æ°æ®ï¼å«ç®å¥½ç PASS/FAIL ä¸åæ ¼çï¼ã **`page` 请æ±åæ°**ï¼å ¶ä½ä¸ºéç¨å页忰 `pageNo` / `pageSize`ï¼ | åæ° | ç±»å | 说æ | |---|---|---| | `templateId` | Long | 模æ¿ç¼å·ï¼ç²¾ç¡®å¹é | | `reportNo` | String | æ¥åç¼å·ï¼**模ç³å¹é ** | | `businessType` | String | ä¸å¡åæ®ç±»åï¼ç²¾ç¡®å¹é | | `businessId` | String | ä¸å¡åæ®ç¼å·ï¼ç²¾ç¡®å¹é | | `status` | Number | æ¥åç¶æï¼ç²¾ç¡®å¹é | | `createTime` | Array\<String\> | å建æ¶é´åºé´ï¼`["å¼å§","ç»æ"]`ï¼æ ¼å¼ `yyyy-MM-dd HH:mm:ss` | ### PDF 彿¡£æä¹æ¥ï¼ä¸æ°å¢ä¸ç¨æ¥è¯¢æ¥å£ï¼ æ¥åå®ä¾è¡¨é**没æä¹ä¸ä¼æ** `pdfUrl` / `pdfPath` è¿ç±»å段ãPDF ç»ä¸éè¿ system 模åçéä»¶åºéæ¥ï¼ èµ°çæ¯å ¨é¡¹ç®éç¨çéä»¶æ¥å£ï¼ERP / CRM 页é¢å款姿å¿ï¼ï¼ | æ¹æ³ | è·¯å¾ | 说æ | |---|---|---| | GET | `/system/storage-attachment/list?recordType=qc_report_instance&recordId=<å®ä¾ç¼å·>` | æ¥è¿ä»½æ¥å彿¡£ç PDF | | åæ° | å¼ | 说æ | |---|---|---| | `recordType` | åºå® `qc_report_instance` | æ¥åå®ä¾çè®°å½ç±»åï¼åæ»è¿ä¸ªå¼ | | `recordId` | æ¥åå®ä¾ç¼å·ï¼`instance.id`ï¼ | | ååºæ¯ä¸ä¸ªæ°ç»ï¼æ¯é¡¹å« `url` / `previewURL` / `downloadURL` / `name` / `id`ï¼éä»¶ç¼å·ï¼çã **æ£å¸¸æ 嵿°ç»éæå¤ä¸æ¡**ï¼å次导åºä¼æ¿æ¢ææ§çï¼ä¸ä¼è¶å¯¼è¶å¤ï¼ã å端å±ç¤ºãPDFãæé®çé»è¾å»ºè®®ï¼ 1. è¿è¯¦æ 页æ¶è°ä¸æ¬¡éä»¶å表æ¥å£ï¼ææ°æ® â æ¾ç¤ºãä¸è½½ PDFãï¼æ²¡æ â æ¾ç¤ºãå¯¼åº PDFãã 2. ç¹ãå¯¼åº PDFãè° `pdf` æ¥å£ï¼æååç¨è¿åç `downloadURL` ç´æ¥ä¸è½½ï¼æéæ°æ¥ä¸æ¬¡éä»¶åè¡¨å·æ°ç¶æï¼ã 3. **ä¸è¦æ `downloadURL` åè¿åç«¯ç¶æé¿æä½¿ç¨**ï¼å®æ¯å¸¦ç¾åç临æ¶å°åï¼è¿æåä¼ 401/403ã ## åæ®µå±ç¤ºè§å | åæ®µ | å±ç¤ºä½ç½® | 说æ | |---|---|---| | `reportNo` | å表åã详æ 头é¨ãHTML å | å表建议å ãå¤å¶ç¼å·ãï¼**宿¯ä¸å¡å¯ä¸é®ï¼ä¸è½éå¤** | | `templateVersion` | å表åã详æ | 建议æ¾ç¤ºæã模æ¿å v1.0ãï¼å岿¥åè¦è½çåºç¨çæ¯åªä¸ç | | `businessType` / `businessId` | å表åã详æ | 建议ç¨åå ¸æ `businessType` 转æä¸æï¼å¦ `mes_qc_oqc` â åºè´§æ£éªåï¼ | | `status` | å表å | ç¨åå ¸ `qc_report_instance_status` æ¸²ææ ç¾ï¼çæä¸/çææå/çæå¤±è´¥ï¼ | | `renderHtml` | 详æ ãé¢è§ | ç¨ iframe ææ´å HTML 容卿¸²æï¼**ä¸è¦æ HTML å½çº¯ææ¬å±ç¤º** | | PDF æ¯å¦åå¨ | 详æ 页ãä¸è½½ PDF / å¯¼åº PDFãæé®ãå表å | æ°æ®æ¥æºæ¯éä»¶å表æ¥å£ï¼`recordType=qc_report_instance`ï¼ï¼ä¸æ¯å®ä¾è¡¨ã`regenerate` ä¹åè¦å·æ°è¿ä¸ªç¶æ | | `creator` / `createTime` | å表åã详æ | â | | `errors` | åºä»¶/é¢è§/éæ°çæå | **ç¨å¯æå 颿¿ææç¤ºæ¡å±ç¤º**ï¼ä¸è¦éé»ä¸¢å¼ï¼è§ãä¸å¡è§åã | ## ä¸å¡è§å说æ | åºæ¯ | è§å | |---|---| | **æ¥åç¼å·è°çæ** | åç«¯ãæ ¼å¼ `QR + yyyyMMdd + '-' + 4 使µæ°´`ï¼å¦ `QR20260918-0001`ï¼ï¼**æå¤©ç¬ç«ï¼è·¨å¤©ä» 0001 鿥**ãä¼ å ¥ `reportNo` å¯è¦ç | | **æå®ç¼å·æå·** | æ¥éå¹¶**æææ¯åªä¸ªç¼å·**ï¼ä¸æ¯ç¬¼ç»ãç¼å·å·²åå¨ãï¼ãè¦ä¹æ¢ç¼å·ï¼è¦ä¹ç空让åç«¯çæ | | **é¢è§ä¸æ¶èåºå·** | `preview` åªæ¸²æï¼ä¸è½åºãä¸åæµæ°´å·ãå¯ä»¥åå¤ç¹é¢è§ï¼ä¸ä¼æå½å¤©çç¼å·è·³å· | | **å æçç¼å·ä¸ä¼è¢«éç¨** | å 餿¥åæ¯è½¯å é¤ï¼ç¼å·ä»ç¶å ä½ï¼å½å¤©çæµæ°´**åªå¢ä¸å**ãå æ `â¦-0001` ä¹åä¸ä¸ä»½æ¯ `â¦-0002`ï¼**ä¸ä¼åå° 0001**ãå端ä¸è¦å设ãç¼å·è¿ç»ã | | **ç¨åªä¸ªçæ¬ï¼åºä»¶ï¼** | `generate` **è¦æ±çæ¬å·²åå¸**ï¼è稿ä¼è¢«æï¼`1_070_101_003`ï¼ãåºä»¶ä¼æçæ¬å·å»ç»è¿å®ä¾ãèå®ä¾å¹¶ä¸åå 容ï¼ç¨è稿åºä»¶çäºç»å岿¥ååä¸é¢ãçæ¬å·å¯¹å¾ä¸ãå 容已ç»åäºãçé· | | **ç¨åªä¸ªçæ¬ï¼é¢è§ï¼** | `preview` **æ¥åèç¨¿çæ¬ï¼ä¹æ¥åå·²åç¨çæ¬**ï¼åªè¦çæ¬åå¨ä¸æç»å¸å 容å³å¯ã设计å¨ç主æµç¨å°±æ¯ãä¿åè稿 â ç¹é¢è§ãï¼è稿ä¼åå¨é¢è§è¿é没æåæï¼ä¸è½åºãä¸å»ç»æ°æ®ã䏿¶èç¼å·ï¼ | | **`currentVersion` 为空æ¶** | ä¸ä¼ `version` 䏿¨¡æ¿ `currentVersion` 为空 â `1_070_101_007`ã**æ¤æ¶ `preview` 乿¿ä¸å°çæ¬**ï¼è®¾è®¡å¨é¢è§å¿ é¡»æ¾å¼ä¼ `version`ï¼å¦åä¿åçè稿 `v1.1`ï¼ | | **æ°æ®å¿«ç §å»ç»** | åºä»¶æ¶æãå¤å®åçæ°æ®ãæ´ä»½å»ç»è¿å®ä¾ãä¸å¡ç³»ç»éçæ°æ®åæ¥æä¹åï¼**é½ä¸å½±åè¿ä»½å岿¥å** | | **éæ°çæå¨çæ¯ä»ä¹** | åªé渲**æç产ç©**ï¼ç¨çæ¯**å®ä¾éå»ç»çå¿«ç §**ä¸**åä¸ä¸ªæ¨¡æ¿çæ¬**ãæ°æ®ãæ¥åç¼å·é½ä¸å | | **éæ°çæä¸è¦æ±çæ¬ä»å·²åå¸** | çæ¬åæ¥è¢«åç¨ï¼å岿¥åä¹ä¸è½å æ¤æä¸å¼ï¼æä»¥ `regenerate` å 许ç¨å·²åç¨ççæ¬ | | **å¤å®ç»æç±å端ç®** | `result` / `resultText` / `conclusion` / `passRate` / `total` / `passCount` / `failCount` ä¸å¾ä»¥**å端ç®åºç**为åï¼è°ç¨æ¹ä¼ äºä¹ä¼è¢«è¦ç | | **`result` ä¸ `resultText` åå·¥** | `result` æ¯æºå¨å¼ï¼`PASS` / `FAIL` / 空å符串ï¼ï¼`resultText` æ¯ç»äººçç䏿ï¼åæ ¼ / ä¸åæ ¼ / å¾ å¤å®ï¼ã**ç»å¸ä¸æ¾ç¤ºçæ¯ `resultText`**ï¼æä»¥äº§ç© HTML éæä¸å° `PASS` åæ ·ï¼å«æ®æ¤ä»¥ä¸ºæ²¡å¤å® | | **æ æ³å¤å®ç项** | æ¢æ²¡æè§æ ¼ä¸ä¸éã乿²¡æå½ä¸ä»»ä½è§åçæ£éªé¡¹ â `result` 为空ã`resultText` 为 `å¾ å¤å®`ï¼**ä¸è®¡å ¥ `passCount`/`failCount` ä½è®¡å ¥ `total`**ï¼æä»¥å¯è½åºç° `total=3 pass=1 fail=1`ï¼åæ ¼çæ `pass/total` ç®ï¼ãåæ¶å®ä¼åºç°å¨ `errors` éï¼å¦ã第 1 项ãå¤è§ã没æè§æ ¼ä¸ä¸é乿²¡æå¤å®è§åï¼æ æ³å¤å®ãï¼ | | **æ°æ®ç¼ºå£æ¸ å** | 模æ¿ç»å®äºä½æ°æ®é没æçåæ®µã以å被å®å ¨çç¥æ¦ä¸ç模æ¿å±æ§ï¼é½ä¼åºç°å¨ `errors` éã**å®åªå¨ preview/generate/regenerate çååºéåºç°ï¼ä¸è½åº** ââ äºååçå岿¥åæ¶**çä¸å°**彿¶æåªäºå段缺å¼ãéè¦çç请å¨åºä»¶æ¶èªè¡è®°å½ | | **PDF æçæ¯åªä»½ HTML** | æçæ¯**å®ä¾éå·²ç»åççé£ä»½ HTML**ï¼ä¸éæ°æ¸²æãæä»¥ã详æ 页çå°çãä¸ãæåºæ¥çãå¿ ç¶åºèªåä¸ä»½åèï¼**ä¸ä¼åºç°é¡µé¢ä¸å¥½å¥½çãæåºæ¥åå½¢** | | **鿏²å³ä½åºå·²å½æ¡£ PDF** | è° `regenerate` ä¹åï¼ä¹å导åºç PDF **ä¼è¢«ä½åº**ï¼éä»¶å表ä¼åæç©ºï¼ãåå ï¼äº§ç© HTML æ¢äºï¼åççæ§ PDF å°±æ¯ã页颿¯æ°æçãä¸è½½å°çæ¯æ§æä»¶ãçéé»ä¸ä¸è´ã**åç«¯å¨ `regenerate` æååè¦æãä¸è½½ PDFãæé®æ¢å¤æãå¯¼åº PDFã** | | **PDF 纸é¢ä¸é¡µé¢é¢è§ä¸è´** | A4/A3/A5/Letterãæ¨ªç«åã页边è·é½åèªæ¨¡æ¿ççº¸å¼ é ç½®ï¼ä¸ HTML é `@page` ç¨çæ¯åä¸ä»½æ°åï¼ä¸åå¨ãé¢è§ 210mmãPDF æ 210.5mmãè¿ç±»æ¼ç§» | | **PDF æ¯"æ¾å¼çæ"ç** | åªæè° `pdf` æ¥å£æä¼æ PDFã`generate` / `regenerate` é½ä¸ä¼é¡ºå¸¦çæ PDF | | **éå¤å¯¼åºæ¯å®å ¨ç** | å坼䏿¬¡ä¼æ¿æ¢ææ§ç彿¡£ï¼é件仿¯ 1 æ¡ï¼ï¼ä¸ä¼è¶å¯¼è¶å¤ãä¹ä¸ä¼çå¤å¿æä»¶ã**å¯ä»¥æ¾å¿è®©ç¨æ·éå¤ç¹** | | **åºä»¶å¾æ ¢æ¶å åè¯ç¨æ·** | 馿¬¡å¯¼åºææµè§å¨åå´©è¿è¦é坿¶éè¦å ç§ï¼æ¥å£å¨ `durationMs` éå¦å®è¿åãåç«¯å»ºè®®ç¹æé®å转åç´å°ååºï¼ä¸è¦èªå·±è®¾å®æ¶å¨å¤è¶ æ¶ | | **å¹¶åä¸é** | æå¡ç«¯åæ¶è¿è¡ç PDF æå°æä¸éï¼é»è®¤ 2ï¼ï¼æéè¶ è¿ 30 ç§ä¼è¿å `1_070_103_007`ãæ¹éå¯¼åºæ¶ä¸è¦å¹¶åçæï¼ä¸²è¡æå°å¹¶å | | **å份æ¥åæä»¶å¤é¡µ** | é¿æ¥åï¼æ£éªé¡¹å¤ï¼ä¼èªå¨å页ï¼`printBackground` å·²å¼ï¼å¤å®åºè²ä¼æåºæ¥ï¼è¡¨å¤´è·¨é¡µéå¤ | | **PDF åè½å¯è¢«è¿ç»´å ³æ** | é¨ç½²ç¯å¢æ²¡æµè§å¨æ¶ä¼å ³æï¼`yudao.qcreport.pdf.enabled=false`ï¼ï¼æ¤æ¶ `pdf` æ¥å£è¿å `1_070_103_008`ãå端åºåæ ·æç¤ºï¼ä¸è¦å½æç³»ç»å¼å¸¸ | | **å é¤å®ä¾** | 软å é¤ãå æå `get` è¿å `null`ï¼ä¸æ¥éï¼ï¼å端è¦å 空ã**已彿¡£ç PDF ä¼ä¸å¹¶æ¸ ç** | | **模æ¿åç¨** | åç¨ç模æ¿**ä¸è½åºæ°ä»¶**ï¼å·²æå岿¥åä¸åå½±å | ## 注æäºé¡¹ - **å表/详æ ä¸è¦å±ç¤º `dataSnapshot` åæ®µ**ï¼`get` ä¸ `page` é½ä¸è¿åå®ï¼è¿æ¯ææçï¼ä½ç§¯å¤§ï¼ã - **`get` 对已å é¤çå®ä¾è¿å `data:null`**ï¼ä¸æ¯æ¥éãæææ `id` å详æ çå ¥å£é½è¦å¤ç©ºã - **HTML 渲æè¦æ³¨ææ ·å¼é离**ï¼äº§ç©æ¯å®æ´ HTML ææ¡£ï¼èªå¸¦ `@page` 䏿 ·å¼ï¼ï¼ ç´æ¥å¡è¿é¡µé¢ä¼æ±¡æå ¨å±æ ·å¼ï¼å»ºè®®ç¨ `iframe` æ¿è½½ã - **é¢è§ååºéç `reportNo` å¯è½ä¸ºç©º**ï¼è¿æ¯æ£å¸¸çï¼é¢è§ä¸çæç¼å·ï¼ï¼ä¸è¦æ¿å®å½åºä»¶ç»æã - **`inspectionItems` ä¸è½ä¼ 空æ°ç»**ï¼æ²¡ææ£éªé¡¹çãæ¥åãæ²¡ææä¹ï¼å端ä¼ç´æ¥æ¥éã - **`businessId` ä¸ä¼æ¯æ°åç±»å**ï¼è½ç¶è¡¨éæ¯ `varchar`ï¼ä½è°ç¨æ¹ç»çå¯è½æ¯å·¥åå·ãæ¹æ¬¡å·ä¸ç±»çå符串ï¼åç«¯å«æ Long å¤çã - **`errors` å¯è½å¾é¿**ï¼å°¤å ¶æ¨¡æ¿åå好æ¶ã建议æå å±ç¤º + æ¾ç¤ºæ¡æ°ã - **æé**ï¼æ¥å£æéç æ¯ `qc-report:instance:query` / `:create` / `:delete` / `:export`ã å·²å ¥åºå° `system_menu`ï¼è§ãAPIãèä¸ç说æï¼ã`create` æ¯**é¢ç½®**çï¼æ¬è½®é¡µé¢æ²¡æåºä»¶å ¥å£ï¼ 宿¯ç»åç» `MesQcReportApi` è½®åå¤çï¼ä¸æ¯éæ¼ã - **ãAI å¯¼å ¥ãæé®ç¨ç¬ç«æéç `qc-report:template:ai-import`**ï¼è½æ¹æ¨¡æ¿ä¸çäºè½ç¨å¤§æ¨¡åè¯å«æä»¶ ï¼æ¯æ¬¡è°ç¨é½äº§çè´¹ç¨ï¼ï¼ä¸¤è å¿ é¡»å弿æï¼ä¸è½åå¹¶å°æ¨¡æ¿çç¼è¾æééã - **é¢è§å¿ é¡» `sandbox=""`**ï¼ç©ºå±æ§ï¼ä¸æ¯çç¥è¯¥å±æ§ï¼ï¼ç©ºå¼ = å®å ¨ç¦ç¨èæ¬ã表åä¸åæºï¼åªè®© CSS çæã äº§ç© HTML æ¯æ¸²æç»æï¼ä¸éè¦ä¹ä¸åºè¯¥è®©å®æ§è¡ä»»ä½ä¸è¥¿ã - **éå¤å¯¼åºä¸å æä»¶å·²å®æµ**ï¼åä¸å®ä¾è¿ç¹ 3 次ï¼åºä¸å§ç»åªæ 1 æ¡ææéä»¶ ï¼å两æ¡éä»¶è¡ä¸ blob è¡å为 `deleted=1`ï¼ï¼ç£çç®å½éä¹åªçå½åè¿ä¸ä»½ PDFã - **导åºç PDF 䏿ä¾èµæå¡å¨çåä½**ï¼å¼åæºï¼Windowsï¼èªå¸¦å¾®è½¯é 黿²¡é®é¢ï¼ ç产 Linux æå¡å¨è¥æ²¡è£ CJK åä½ä¼æææ¹æ¡ãè¿å±äºé¨ç½²åç½®ï¼å端ä¸éè¦å¤çï¼ä½**éªæ¶æ¶è¦å¨ç®æ ç¯å¢ä¸çä¸ç¼ä¸æ**ã - **`pdf` æ¥å£çè¿åå°åæ¯ç¾åç临æ¶å°å**ï¼å¸¦è¿ææ¶é´ä¸ä½¿ç¨æ¬¡æ°éå¶ï¼ **ä¸è¦**æå®åè¿æ°æ®åºãä¹ä¸è¦æ¾è¿å表æ¥å£ç¼åï¼éè¦æ¶éæ°æ¥éä»¶å表æéæ°å¯¼åºã - **éä»¶å表éç `name` æ¯ PDF æä»¶å**ï¼`QR20260918-0001.pdf`ï¼ï¼ä¸è½½æ¶ç¨å®å½åå³å¯ã ## é误ç | é误ç | æ¥æ | 触ååºæ¯ | |---|---|---| | `1_070_100_000` | 模æ¿ä¸åå¨ | `templateId` æ¾ä¸å° | | `1_070_100_002` | 该模æ¿å·²åç¨ï¼ä¸è½çææ¥å | æ¨¡æ¿ `status=1` | | `1_070_101_000` | 模æ¿çæ¬ä¸åå¨ | `version` ä¼ äºä½æ¥ä¸å° | | `1_070_101_003` | 该模æ¿çæ¬æªåå¸ | `generate` ç `version` æåè稿ï¼**`preview` 䏿¥è¿ä¸ªé**ï¼ | | `1_070_101_007` | 该模æ¿è¿æ²¡æå·²åå¸ççæ¬ï¼æ æ³çææ¥åï¼è¯·å å¨è®¾è®¡å¨ä¸åå¸ä¸ä¸ªçæ¬ | ä¸ä¼ `version` 䏿¨¡æ¿ `currentVersion` 为空ï¼`preview` åæ ·ä¼æ¥ï¼æ¤æ¶é¡»æ¾å¼ä¼ `version`ï¼ | | `1_070_101_008` | 该模æ¿çæ¬æ²¡æç»å¸å å®¹ï¼æ æ³çææ¥åï¼è¯·å å¨è®¾è®¡å¨ä¸è®¾è®¡å¹¶ä¿å模æ¿å 容 | çæ¬æè®°å½ä½ç»å¸æ¯ç©ºç | | `1_070_102_000` | æ¥åå®ä¾ä¸åå¨ | `id` æ¾ä¸å°ï¼`get` é¤å¤ï¼å®è¿å nullï¼ | | `1_070_102_001` | æ¥åç¼å·ãXã已被å ç¨ï¼è¯·æ¢ä¸ä¸ªç¼å·ï¼æç空ç±ç³»ç»æ QR+æ¥æ+æµæ°´ èªå¨çæ | æå®äºéå¤ç¼å·ã**å«å·²è¢«è½¯å é¤çç¼å·**ï¼å æçå·ä¸åæ¶ï¼ | | `1_070_102_001` | èªå¨çææ¥åç¼å·æ¶è¿ç» 5 次ä¸å·²ææ¥åå²çªï¼è¯·ç¨åéè¯ï¼ä¹å¯å¨è¯·æ±éæ¾å¼æå®ä¸ä¸ªæªè¢«å ç¨çæ¥åç¼å· | ä» å¨å䏿¶å»æå¤ä¸ªåºä»¶è¯·æ±æ¢åä¸ä¸ªæµæ°´å·æ¶åºç°ï¼æ£å¸¸ä½¿ç¨ä¸åºçå° | | `1_070_102_002` | æ¥åå®ä¾ç¼ºå°æ°æ®å¿«ç §ï¼æ æ³éæ°çæ | å¿«ç §ä¸ºç©ºçèæ°æ® | | `1_070_102_003` | æ¥åå®ä¾æ²¡ææ¸²æäº§ç© HTMLï¼æ æ³çæ PDFï¼è¯·å 对该æ¥åæ§è¡ãéæ°çæã | `pdf` æ¶å®ä¾ç `render_html` 为空ï¼èæ°æ®ï¼ã**æç¤ºéå·²ç»åºå¨ä½ï¼å ãéæ°çæã** | | `1_070_103_004` | PDF æ¸²ææµè§å¨å¯å¨å¤±è´¥ï¼è¯·æ£æ¥ Chromium æ¯å¦å·²å®è£ | æå¡å¨æ²¡è£ Chrome / é ç½®ç `executable-path` ä¸å¯¹ãæ¥æéä¼å¸¦ä¸å½åé ç½®çæµè§å¨æ¥æºä¸å®é åå | | `1_070_103_005` | PDF çæå¤±è´¥ | æå°è¿ç¨ä¸æµè§å¨æ¥éï¼å«ç页é¢èµæºè¶ æ¶ï¼ | | `1_070_103_006` | PDF å·²çæï¼ä½å½æ¡£å°éä»¶åºå¤±è´¥ | æå°æåãåæ¡£å¤±è´¥ã**注æå®ä¸çäºãPDF 没çæã**ï¼å端çæç¤ºææ¡ä¸è¦åæãçæå¤±è´¥ã | | `1_070_103_007` | å½å PDF 渲æä»»å¡å·²è¾¾ä¸éï¼è¯·ç¨åéè¯ | å¹¶åæéè¶ è¿ 30 ç§ãæ¥æé带å½åå¹¶åä¸éå¼ | | `1_070_103_008` | PDF åºä»¶åè½æªå¯ç¨ï¼è¯·è系管çå | è¿ç»´æ `yudao.qcreport.pdf.enabled` å ³æäº `false` | | `1_070_103_000` | æ£éªé¡¹åè¡¨ä¸ºç©ºï¼æ²¡æå¯å¤å®çå 容ï¼è¯·è³å°ä¼ å ¥ä¸ä¸ªæ£éªé¡¹ï¼inspectionItemsï¼ååºä»¶ | `inspectionItems` 为空 | docs/qc_report_template_version_frontend_integration.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,124 @@ # æ¥å模æ¿çæ¬ç®¡ç - å端èè°æ¹æ¡ï¼ä¿åæ ¡éª + åç¨çº¦æï¼ ## æ¶åé¡µé¢ - æ¥å模æ¿è®¾è®¡å¨ï¼ä¿å / å¦åæ°çæ¬ï¼èµ° `create` ä¸ `update`ï¼ - æ¥å模æ¿è¯¦æ â çæ¬å表ï¼åå¸ / åæ» / åç¨ï¼ ## æ¬æ¬¡åæ´æ¦è¿° `qc_report_template_version` çåå ¥å£æ°å¢ä¸¤æ¡**æç»è§å**ï¼å对åºä¸ä¸ªæ°é误ç ã 两æ¡é½ä¸æ¯æ°å¢å段ãä¹ä¸æ¯æ°å¢æ¥å£ï¼èæ¯æã以åä¼éé»åä¸ / åä¸åæç¸ãçæ åµæ¹æ**å½åºæ¥é**ã å端éè¦åçæ¯ï¼æå端è¿åç `msg` åæ ·å±ç¤ºï¼ææ¡å·²å¯ç´æ¥ç»ç¨æ·çï¼ï¼å¹¶ä¿è¯è¿ä¸¤ç§å¤±è´¥**ä¸å¼¹éç¨ãç³»ç»å¼å¸¸ã**ã | æ¥å£ | æ¹æ³ | æ°å¢æç»è§å | é误ç | |---|---|---|---| | `/qc-report/template-version/create` | POST | ç»å¸å«ä¸å 许渲æçå 容 | `1_070_101_009` | | `/qc-report/template-version/update` | PUT | åä¸ | `1_070_101_009` | | `/qc-report/template-version/disable` | POST | ç®æ æ¯æ¨¡æ¿ç**å½åçæ¬** | `1_070_101_010` | > `create` ä¸ `update` å ±ç¨å䏿®µ Schema æ ¡éªï¼ä¸¤æ¡å ¥å£è¡ä¸ºä¸è´ï¼**åªè¦æäº¤ç Schema ä¸åæ ¼ï¼ä¸å¾ä¸è½åº**ã ## è§åä¸ï¼ç»å¸å 容å®å ¨æ ¡éªï¼`1_070_101_009`ï¼ ### æ¡çæ¯ä»ä¹ æ¥åäº§ç©æ¯æ¼å符串æ¼åºæ¥çï¼æ¨¡æ¿ç»å¸éæä¸å¤å 容ä¼**ä¸ç»è½¬ä¹**ç´æ¥è¿å ¥ HTMLï¼ 1. **èç¹çæ ç¾å**ï¼å¦ `tagName: "h2"`ï¼ï¼ 2. **屿§å**ï¼å±æ§**å¼**ä¼è½¬ä¹ï¼å±æ§**å**ä¸ä¼ï¼ï¼ 3. **æ´æ®µ CSS**ï¼`styles` åæå符串æ¶ä¼è¢«åæ ·å¡è¿ `<style>`ï¼ã 模æ¿ç»å¸åæ¯ç¨æ·å¯ç¼è¾æ°æ®ï¼æä»¥è¿ä¸å¤é½æ¯å¯è¢«åå ¥çæ³¨å ¥é¢ã ### æ ¡éªè§å | 维度 | è§å | |---|---| | æ ç¾å | å¿ é¡»è½å¨ç½ååå ï¼å¸¸è§æçä¸ææ¬æ ç¾ï¼`div/p/span/h1~h6/img/hr`ãè¡¨æ ¼ä¸æãåè¡¨ä¸æã常è§è¡å è¯ä¹æ ç¾ï¼ãèæ¬ãæ ·å¼ãå åµé¡µé¢ã表åç±»æ ç¾ä¸å¾æç» | | æ ç¾å | é**æ´ä½**å¹é ç½ååï¼`"img src=x onerror=alert(1)"` è¿ç§ãæ ç¾åéå¡å±æ§ãçåæ³ä¼è¢«æ | | 屿§å | åªå è®¸åæ¯ / æ°å / `-` `_` `:` `.`ï¼ä¸ä¸è½ä»¥æ°åå¼å¤´ï¼å«å¼å·æç©ºæ ¼ç屿§åä¼è¢«æï¼å®è½é¡¶åºä¸ä¸ªæ°å±æ§ï¼ | | 屿§å | 以 `on` å¼å¤´çä¸å¾è§ä¸ºäºä»¶å±æ§ï¼æç» | | `styles` | å¿ é¡»æ¯**æ ·å¼è§åæ°ç»**ï¼åæä¸æ®µ CSS ææ¬ä¼è¢«æï¼æ¹æ ·å¼èµ°è®¾è®¡å¨çæ ·å¼é¢æ¿ï¼ä¿ååèªç¶æ¯è§åæ°ç»ï¼ | | `selectors` / `mediaText` / æ ·å¼å±æ§åä¸å±æ§å¼ | åºç° `<` 峿ç»ï¼`</style>` è½æåéåæ ·å¼åï¼æåé¢çå 容åæç HTMLï¼ | ### ä¸ä¼è¯¯æçä¸ç§æ åµ è®¾è®¡å¨æ£å¸¸ä¿åçç»å¸**ä¸ä¼**被æ¦ï¼ä»¥ä¸ä¸ç±»å¨å®ç°é被æç¡®æé¤ï¼å端æ éç¹æ®å¤çï¼ - **`textnode` èç¹ä¸ç `tagName`**ï¼æ¸²æå¨æ ¹æ¬ä¸è¯»å®ï¼ - **GrapesJS æå¨èç¹ä¸ç `docEl` / `head` å æ°æ®**ï¼æ¸²æå¨ä¸è¯»ï¼æ ¡éªä¹åªæ²¿ `components` èµ°ï¼ - **åéç»å¸**ï¼å·²å¯¹è¿è¡åºä¸å ¨é¨çå®çæ¬å®æµéè¿ã ### æ¥éææ¡ 䏿¬¡æå¤è¿å **8 æ¡**é®é¢ï¼æ¯æ¡é½å¸¦**ä½ç½®**ä¸**è¿è§å 容**ï¼å½¢å¦ï¼ ``` 模æ¿ç»å¸éæä¸å 许åºç°çå 容ï¼å·²æç»ä¿åï¼ç»å¸ãæ ¹ > 第 1 个ç»ä»¶ãçæ ç¾æ¯ãscriptãï¼ æ¥å䏿¯æè¯¥æ ç¾ãå¯ç¨çåªæå¸¸è§æçæ ç¾ï¼div/p/span/h1~h6/img/hr ä¸è¡¨æ ¼ãå表ãè¡å ææ¬æ ç¾ï¼ï¼ èæ¬ãæ ·å¼ãå åµé¡µé¢ã表åç±»æ ç¾ä¸å¾ä¸å 许ã请å¨è®¾è®¡å¨ä¸å æè¿äºå 容åéæ°ä¿å ``` 夿¡é®é¢ä»¥ `ï¼` è¿æ¥ãæ¹å®åä¿å䏿¬¡å³å¯çå°ä¸ä¸æ¹ã ## è§åäºï¼ä¸è½åç¨ãå½åçæ¬ãï¼`1_070_101_010`ï¼ ### æ¡çæ¯ä»ä¹ 模æ¿è¡¨ä¸ç `current_version` æåãåºä»¶æ¶é»è®¤ç¨åªä¸ªçæ¬ãã åç¨æä¸ªçæ¬**ä¸ä¼**æ¸ ç©º `current_version`ï¼äºæ¯åç¨å½åçæ¬ä¼é åºä¸ä»½**èªç¸çç¾**çæ¨¡æ¿ï¼ `current_version` è¿æçå®ï¼å®å´å·²ç»ä¸å¯ç¨ãè¿ç§æ¨¡æ¿èªæ¤åºä¸äºæ¥åï¼ èä¸å¤±è´¥åçå¨**åºä»¶é£ä¸å»**ï¼æ¥ `1_070_101_003`ï¼ï¼ç¨æ·ä¸ä¼æ³å°èµ·å æ¯ä¹å飿¬¡åç¨ã å æ¤ `disable` ç´æ¥æç»ï¼**ä¸ä¼**é¡ºææ¿ç¨æ·æ¹æ `current_version`ââ ãæ¹åªä¸ªçæ¬ãæ¯ç¨æ·çå³å®ï¼ä¸è¯¥ç±ãåç¨ãè¿ä¸ªå¨ä½ææä»£å³ã ### æ¥éææ¡ ``` çæ¬ãv1.1ãæ¯æ¨¡æ¿ãæ¥ææ£éªæ¥å模æ¿ãå½åæ£å¨ä½¿ç¨ççæ¬ï¼ä¸è½åç¨ã 请å å°è¯¥æ¨¡æ¿ççæ¬å表é对å¦ä¸ä¸ªå·²åå¸çæ¬æ§è¡ãåæ»ãï¼æå½åçæ¬åèµ°ï¼ååç¨æ¬çæ¬ï¼ è¥è¦è®©æ´ä¸ªæ¨¡æ¿åæ¢ä½¿ç¨ï¼è¯·æ¹ä¸ºåç¨æ¨¡æ¿æ¬èº« ``` ææ¡éç»åºäº**两æ¡å¯æ§è¡åºè·¯**ï¼å端ä¸éè¦èªå·±ç»ç»å¼å¯¼è¯æ¯ã ### 被æçå¤å®å£å¾ - æ¯è¾å¯¹è±¡æ¯**模æ¿è¡¨ `current_version` çå符串**ä¸**è¯¥çæ¬è®°å½ç `version`**ï¼ - åªè¦ç¸çå°±æç»ï¼**ä¸ççæ¬æ¯å¦å·²åå¸**ï¼èç¨¿çæ¬åæ ·å¯è½è¢« `current_version` æå°ï¼ï¼ - åç¨**éå½åçæ¬**çå·²åå¸çæ¬ä¾ç¶å 许ï¼è¡ä¸ºä¸åã ## åæ®µå±ç¤ºè§å æ¬æ¬¡æ æ°å¢åæ®µï¼æ éè°æ´å表æè¡¨ååã | åºæ¯ | åç«¯è¡¨ç° | |---|---| | ä¿åæ¨¡æ¿æ¥ `1_070_101_009` | ç¨ `msg` å ¨ææç¤ºï¼å¯è½å«å¤æ¡ï¼ç¨ `ï¼` åéï¼ï¼**ä¸è¦**æªææãä¿å失败ã | | åç¨çæ¬æ¥ `1_070_101_010` | ç¨ `msg` å ¨ææç¤ºï¼æç¤ºææ¡å·²å«åç»æä½æ¥éª¤ï¼æ éå¦å æå¼ | | å ¶å®é误ç | è¡ä¸ºä¸å | ## ä¸å¡è§å说æ | åºæ¯ | è§å | |---|---| | æ°å»ºçæ¬ / æ´æ°çæ¬æ¶ Schema 为空 | å 许ï¼å å»ºçæ¬å设计ï¼ï¼ä¸è§¦åç»å¸æ ¡éª | | æ°å»ºçæ¬ / æ´æ°çæ¬æ¶ Schema é空ä½ç¼º `schemaVersion` | æç»ï¼`1_070_101_004` | | ç»å¸æé®é¢ | æç»ä¿åï¼`1_070_101_009`ï¼**ä¸è½åº** | | æ´æ°**å·²åå¸**çæ¬ | ä»ç¶æç»ï¼`1_070_101_002`ï¼æ¹å¨åªè½å¦åæ°è稿ï¼ï¼ä¸æ¬æ¬¡åæ´æ å ³ | | åç¨å½åçæ¬ | æç»ï¼`1_070_101_010` | | åç¨éå½åçæ¬ | æ£å¸¸åç¨ | | åç¨æ´ä¸ªæ¨¡æ¿ | èµ°ãåç¨æ¨¡æ¿ãå ¥å£ï¼ä¸åæ¬æ¬¡åæ´å½±å | ## 注æäºé¡¹ - **é误æç¤ºä¸è¦åæ**ï¼è¿ä¸¤æ¡é½æ¯**ç¨æ·å¨ç¼è¾å¨éçæä½åé¦**ï¼å¿ é¡»æå端 `msg` åæ ·å±ç¤ºã ç»å¸é®é¢ä¸æ¬¡æå¤ 8 æ¡ï¼å¤æ¡æ¶å»ºè®®æ `ï¼` æè¡å±ç¤ºï¼ä¾¿äºéæ¡å¤çã - **å¯¹æ ¡éªä¸çº¿åå°±å·²å ¥åºçèæ°æ®æ²¡æå åº**ï¼æ ¡éªåªå¨ä¿åæ¶æ§è¡ï¼åå²çæ¬ä¸ä¼è¢«åæº¯æ£æ¥ã è¥æçåéçæ¬æé®é¢ï¼éè¦å¦è¡æ ¸æ¥ï¼åºä»¶é¾è·¯ä¸åå½±åï¼ä»æåæ ·æ¸²æï¼ã - **`1_070_101_009` çå®ä½ä¿¡æ¯æ¯ã第 N 个ç»ä»¶ã**ï¼æè¯¥ç»ä»¶å¨å ¶ç¶èç¹ `components` æ°ç»ä¸çåºå·ï¼ä» 1 å¼å§ï¼ï¼ è·¯å¾å½¢å¦ `æ ¹ > 第 2 个ç»ä»¶ > 第 1 个ç»ä»¶`ï¼å±çº§è¿æ·±æ¶ä¸é´é¨å伿å 为 `â¦`ã - ç»å¸æ ¡éª**䏿¹å渲æç»æ**ï¼æ¸²æå¼æä¸å端 `engine/render.ts` çéåä¸è´æ§åå鿥åç `regenerate` å¤ç°æ§é½ä¸åå½±åï¼æ¬æ¬¡åªå¨**åå ¥ä¾§**å é¸é¨ã docs/sql/config_export_all_20260918.sql
¶Ô±ÈÐÂÎļþ ÎļþÌ«´ó docs/sql/qc_report_dict.sql
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,55 @@ -- ============================================================ -- æºè½è´¨æ£æ¥åè®¾è®¡å¹³å° åå ¸æ°æ® -- ç®æ åºï¼ruoyi-vue-proï¼local profile ç master æ°æ®æºï¼ -- å¹çï¼å å åæ -- 说æï¼æ¥åç±»åå¤ç¨ MES å·²æåå ¸ mes_qc_typeï¼ä¸å¦å»ºéå¤åå ¸ -- ============================================================ USE `ruoyi-vue-pro`; -- æ¸ ç DELETE FROM `system_dict_data` WHERE `id` BETWEEN 1065400 AND 1065499; DELETE FROM `system_dict_type` WHERE `id` BETWEEN 1065400 AND 1065499; -- ------------------------------------------------------------ -- åå ¸ç±»å -- ------------------------------------------------------------ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1065400, 'è´¨æ£æ¥å模æ¿ç¶æ', 'qc_report_template_status', 0, '1', NOW(), '1', NOW(), b'0'), (1065401, 'è´¨æ£æ¥å模æ¿çæ¬ç¶æ', 'qc_report_version_status', 0, '1', NOW(), '1', NOW(), b'0'), (1065402, 'è´¨æ£æ¥åå®ä¾ç¶æ', 'qc_report_instance_status', 0, '1', NOW(), '1', NOW(), b'0'), (1065403, 'è´¨æ£æ¥åçº¸å¼ å°ºå¯¸', 'qc_report_page_size', 0, '1', NOW(), '1', NOW(), b'0'), (1065404, 'è´¨æ£æ¥åçº¸å¼ æ¹å', 'qc_report_orientation', 0, '1', NOW(), '1', NOW(), b'0'), (1065405, 'è´¨æ£æ¥åéç¨è¡ä¸', 'qc_report_industry', 0, '1', NOW(), '1', NOW(), b'0'); -- ------------------------------------------------------------ -- åå ¸æ°æ® -- ------------------------------------------------------------ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES -- 模æ¿ç¶æï¼å¯¹åº QcReportEnums.TemplateStatusEnumï¼ (1065410, 1, 'å¯ç¨', '0', 'qc_report_template_status', 0, 'success', '', '1', NOW(), '1', NOW(), b'0'), (1065411, 2, 'åç¨', '1', 'qc_report_template_status', 0, 'info', '', '1', NOW(), '1', NOW(), b'0'), -- çæ¬ç¶æï¼å¯¹åº QcReportEnums.VersionStatusEnumï¼ (1065420, 1, 'è稿', '0', 'qc_report_version_status', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065421, 2, 'å·²åå¸', '1', 'qc_report_version_status', 0, 'success', '', '1', NOW(), '1', NOW(), b'0'), (1065422, 3, 'å·²åç¨', '2', 'qc_report_version_status', 0, 'info', '', '1', NOW(), '1', NOW(), b'0'), -- å®ä¾ç¶æï¼å¯¹åº QcReportEnums.InstanceStatusEnumï¼ (1065430, 1, 'çæä¸', '0', 'qc_report_instance_status', 0, 'processing', '', '1', NOW(), '1', NOW(), b'0'), (1065431, 2, 'çææå', '1', 'qc_report_instance_status', 0, 'success', '', '1', NOW(), '1', NOW(), b'0'), (1065432, 3, 'çæå¤±è´¥', '2', 'qc_report_instance_status', 0, 'error', '', '1', NOW(), '1', NOW(), b'0'), -- çº¸å¼ å°ºå¯¸ï¼å¯¹åº QcReportEnums.PageSizeEnumï¼å¼ä¸ºåç¬¦ä¸²ï¼ (1065440, 1, 'A3', 'A3', 'qc_report_page_size', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065441, 2, 'A4', 'A4', 'qc_report_page_size', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065442, 3, 'A5', 'A5', 'qc_report_page_size', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065443, 4, 'Letter', 'Letter', 'qc_report_page_size', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), -- çº¸å¼ æ¹åï¼å¯¹åº QcReportEnums.OrientationEnumï¼ (1065450, 1, '纵å', 'portrait', 'qc_report_orientation', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065451, 2, '横å', 'landscape', 'qc_report_orientation', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), -- éç¨è¡ä¸ (1065460, 1, 'éç¨', 'general', 'qc_report_industry', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065461, 2, 'çµåçµå¨', 'electronics', 'qc_report_industry', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065462, 3, 'äºéæºæ¢°', 'hardware', 'qc_report_industry', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065463, 4, '汽车é¶é¨ä»¶', 'auto', 'qc_report_industry', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065464, 5, 'å¡è¶å¶å', 'plastic', 'qc_report_industry', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065465, 6, 'é£å饮æ', 'food', 'qc_report_industry', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'), (1065466, 7, 'å»è¯å»ç', 'pharma', 'qc_report_industry', 0, 'default', '', '1', NOW(), '1', NOW(), b'0'); docs/sql/qc_report_menu.sql
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,34 @@ -- ============================================================ -- æºè½è´¨æ£æ¥åè®¾è®¡å¹³å° èå䏿é -- ç®æ åºï¼ruoyi-vue-proï¼local profile ç master æ°æ®æºï¼ -- å½å±ï¼è´¨é管çï¼èå id = 5500ï¼ä¸ -- å¹çï¼å å åæ -- ============================================================ USE `ruoyi-vue-pro`; -- æ¸ çï¼å«ä¹åå¯è½å·²åå¨çè®°å½ï¼1075406 æ¾ä½ä¸ºè®¾è®¡å¨çéèèåï¼å·²æ¹ä¸ºå端éæè·¯ç±ï¼ DELETE FROM `system_menu` WHERE `id` IN (1075400, 1075401, 1075402, 1075403, 1075404, 1075405, 1075406); -- åæ¥æ¸ æè®¾è®¡å¨éèèåéççè§è²ææï¼é¿å çä¸å¤å¿ææ DELETE FROM `system_role_menu` WHERE `menu_id` = 1075406; -- æ¥å模æ¿è®¾è®¡ï¼èåï¼ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`) VALUES (1075400, 'æ¥å模æ¿è®¾è®¡', '', 2, 12, 5500, 'report-template', 'ep:document', 'mes/qc/report/template/index', NULL, 0); -- æ¥åæ¨¡æ¿ æé®æé INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`) VALUES (1075401, 'æ¥åæ¨¡æ¿æ¥è¯¢', 'qc-report:template:query', 3, 1, 1075400, '', '', NULL, NULL, 0), (1075402, 'æ¥å模æ¿å建', 'qc-report:template:create', 3, 2, 1075400, '', '', NULL, NULL, 0), (1075403, 'æ¥åæ¨¡æ¿æ´æ°', 'qc-report:template:update', 3, 3, 1075400, '', '', NULL, NULL, 0), (1075404, 'æ¥å模æ¿å é¤', 'qc-report:template:delete', 3, 4, 1075400, '', '', NULL, NULL, 0), (1075405, 'æ¥å模æ¿åå¸', 'qc-report:template:publish', 3, 5, 1075400, '', '', NULL, NULL, 0); -- ææç»è¶ 级管çåè§è²ï¼è¥ä»£ç å·²å¯¹è¶ ç®¡è¿åå ¨é¨èåï¼æ¤æ¥ä¸ºåä½ä½æ å®³ï¼ INSERT INTO `system_role_menu` (`role_id`, `menu_id`, `creator`, `updater`, `deleted`) SELECT 1, m.`id`, 'admin', 'admin', b'0' FROM `system_menu` m WHERE m.`id` IN (1075400, 1075401, 1075402, 1075403, 1075404, 1075405) AND NOT EXISTS ( SELECT 1 FROM `system_role_menu` rm WHERE rm.`role_id` = 1 AND rm.`menu_id` = m.`id` AND rm.`deleted` = b'0' ); docs/sql/qc_report_platform_ddl.sql
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,140 @@ -- ============================================================ -- æºè½è´¨æ£æ¥åè®¾è®¡å¹³å° å»ºè¡¨èæ¬ -- 模åï¼yudao-module-qcreport -- 说æï¼é¡¹ç®ä¸ä½¿ç¨å¤ç§æ·ï¼æ ä¸å« tenant_id åæ®µ -- ============================================================ -- ---------------------------- -- æ¥åæ¨¡æ¿ -- ---------------------------- DROP TABLE IF EXISTS `qc_report_template`; CREATE TABLE `qc_report_template` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '模æ¿ç¼å·', `template_code` VARCHAR(64) NOT NULL COMMENT '模æ¿ç¼ç ', `template_name` VARCHAR(128) NOT NULL COMMENT '模æ¿åç§°', `industry` VARCHAR(32) DEFAULT NULL COMMENT 'æå±è¡ä¸', `report_type` VARCHAR(32) DEFAULT NULL COMMENT 'æ¥åç±»å', `page_size` VARCHAR(16) NOT NULL DEFAULT 'A4' COMMENT 'çº¸å¼ å°ºå¯¸ï¼A3/A4/A5/Letter', `orientation` VARCHAR(16) NOT NULL DEFAULT 'portrait' COMMENT 'çº¸å¼ æ¹åï¼portrait/landscape', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '模æ¿ç¶æï¼0å¯ç¨ 1åç¨', `current_version` VARCHAR(32) DEFAULT NULL COMMENT 'å½åçæ¬å·', `description` VARCHAR(512) DEFAULT NULL COMMENT 'æ¨¡æ¿æè¿°', `creator` VARCHAR(64) DEFAULT '' COMMENT 'å建è ', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', `updater` VARCHAR(64) DEFAULT '' COMMENT 'æ´æ°è ', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', PRIMARY KEY (`id`), UNIQUE KEY `uk_template_code` (`template_code`), KEY `idx_template_name` (`template_name`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'æºè½è´¨æ£æ¥å-æ¥å模æ¿'; -- ---------------------------- -- 模æ¿çæ¬ï¼åå¸åä¸å¯è¦çï¼ -- ---------------------------- DROP TABLE IF EXISTS `qc_report_template_version`; CREATE TABLE `qc_report_template_version` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'çæ¬ç¼å·', `template_id` BIGINT NOT NULL COMMENT '模æ¿ç¼å·', `version` VARCHAR(32) NOT NULL COMMENT 'çæ¬å·ï¼å¦ v1.0', `schema_json` LONGTEXT COMMENT 'æ¨¡æ¿ Schemaï¼GrapesJS JSON + ç»ä»¶ + æ°æ®æº + ç»å® + è§å + æ ·å¼ï¼', `status` TINYINT NOT NULL DEFAULT 0 COMMENT 'çæ¬ç¶æï¼0è稿 1å·²åå¸ 2å·²åç¨', `description` VARCHAR(512) DEFAULT NULL COMMENT 'çæ¬è¯´æ', `creator` VARCHAR(64) DEFAULT '' COMMENT 'å建è ', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', `updater` VARCHAR(64) DEFAULT '' COMMENT 'æ´æ°è ', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', PRIMARY KEY (`id`), UNIQUE KEY `uk_template_version` (`template_id`, `version`), KEY `idx_template_id` (`template_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'æºè½è´¨æ£æ¥å-模æ¿çæ¬'; -- ---------------------------- -- æ¥åå®ä¾ï¼å¿ é¡»ä¿åæ°æ®å¿«ç §ï¼ -- ---------------------------- DROP TABLE IF EXISTS `qc_report_instance`; CREATE TABLE `qc_report_instance` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'å®ä¾ç¼å·', `report_no` VARCHAR(64) NOT NULL COMMENT 'æ¥åç¼å·ï¼ä¸å¡å¯ä¸ï¼', `template_id` BIGINT NOT NULL COMMENT '模æ¿ç¼å·', `template_version` VARCHAR(32) NOT NULL COMMENT 'çææ¶ä½¿ç¨ç模æ¿çæ¬å·', `business_id` VARCHAR(64) DEFAULT NULL COMMENT 'ä¸å¡åæ®ç¼å·', `business_type` VARCHAR(64) DEFAULT NULL COMMENT 'ä¸å¡åæ®ç±»åï¼å¦ mes_qc_iqc', `data_snapshot` LONGTEXT COMMENT 'æ°æ®å¿«ç §ï¼æ¸²ææ¶ç宿´æ°æ®ä¸ä¸æ', `render_html` LONGTEXT COMMENT '渲æäº§ç© HTML', `status` TINYINT NOT NULL DEFAULT 0 COMMENT 'æ¥åç¶æï¼0çæä¸ 1çææå 2çæå¤±è´¥', `creator` VARCHAR(64) DEFAULT '' COMMENT 'å建è ', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', `updater` VARCHAR(64) DEFAULT '' COMMENT 'æ´æ°è ', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', PRIMARY KEY (`id`), UNIQUE KEY `uk_report_no` (`report_no`), KEY `idx_template_id` (`template_id`), KEY `idx_business` (`business_type`, `business_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'æºè½è´¨æ£æ¥å-æ¥åå®ä¾'; -- ---------------------------- -- æ°æ®æºå®ä¹ -- ---------------------------- DROP TABLE IF EXISTS `qc_report_data_source`; CREATE TABLE `qc_report_data_source` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ç¼å·', `template_id` BIGINT NOT NULL COMMENT '模æ¿ç¼å·', `source_key` VARCHAR(64) NOT NULL COMMENT 'æ°æ®æºæ è¯ï¼å¦ inspectionItems', `source_type` VARCHAR(32) NOT NULL COMMENT 'æ°æ®æºç±»åï¼BUILTIN/SQL/API', `config` LONGTEXT COMMENT 'æ°æ®æºé ç½® JSON', `creator` VARCHAR(64) DEFAULT '' COMMENT 'å建è ', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', `updater` VARCHAR(64) DEFAULT '' COMMENT 'æ´æ°è ', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', PRIMARY KEY (`id`), KEY `idx_template_id` (`template_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'æºè½è´¨æ£æ¥å-æ°æ®æº'; -- ---------------------------- -- å¤å®è§å -- ---------------------------- DROP TABLE IF EXISTS `qc_report_rule`; CREATE TABLE `qc_report_rule` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ç¼å·', `template_id` BIGINT NOT NULL COMMENT '模æ¿ç¼å·', `rule_name` VARCHAR(128) NOT NULL COMMENT 'è§ååç§°', `target_path` VARCHAR(256) NOT NULL COMMENT 'ä½ç¨ç®æ è·¯å¾ï¼å¦ inspectionItems[].actualValue', `expression` VARCHAR(1024) NOT NULL COMMENT 'å¤å®è¡¨è¾¾å¼ï¼å¦ actualValue >= lowerLimit AND actualValue <= upperLimit', `pass_value` VARCHAR(64) DEFAULT 'PASS' COMMENT 'åæ ¼æ¶è¾åºå¼', `fail_value` VARCHAR(64) DEFAULT 'FAIL' COMMENT 'ä¸åæ ¼æ¶è¾åºå¼', `creator` VARCHAR(64) DEFAULT '' COMMENT 'å建è ', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', `updater` VARCHAR(64) DEFAULT '' COMMENT 'æ´æ°è ', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', PRIMARY KEY (`id`), KEY `idx_template_id` (`template_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'æºè½è´¨æ£æ¥å-å¤å®è§å'; -- ---------------------------- -- 渲æè®°å½ï¼ææ¥ç¨ï¼èæ¶ãæµè§å¨ç¶æãå¼å¸¸æ ï¼ -- ---------------------------- DROP TABLE IF EXISTS `qc_report_render_record`; CREATE TABLE `qc_report_render_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ç¼å·', `report_id` BIGINT DEFAULT NULL COMMENT 'æ¥åå®ä¾ç¼å·', `template_id` BIGINT DEFAULT NULL COMMENT '模æ¿ç¼å·', `business_id` VARCHAR(64) DEFAULT NULL COMMENT 'ä¸å¡åæ®ç¼å·', `render_start_time` DATETIME DEFAULT NULL COMMENT '渲æå¼å§æ¶é´', `render_end_time` DATETIME DEFAULT NULL COMMENT '渲æç»ææ¶é´', `render_duration` BIGINT DEFAULT NULL COMMENT '渲æèæ¶ï¼æ¯«ç§ï¼', `pdf_duration` BIGINT DEFAULT NULL COMMENT 'PDF çæèæ¶ï¼æ¯«ç§ï¼', `browser_status` VARCHAR(64) DEFAULT NULL COMMENT 'æµè§å¨ç¶æ', `error_stack` LONGTEXT COMMENT 'å¼å¸¸å æ ', `creator` VARCHAR(64) DEFAULT '' COMMENT 'å建è ', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', `updater` VARCHAR(64) DEFAULT '' COMMENT 'æ´æ°è ', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', PRIMARY KEY (`id`), KEY `idx_report_id` (`report_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'æºè½è´¨æ£æ¥å-渲æè®°å½'; docs/ÖÇÄÜÖʼ챨¸æÆ½Ì¨-¿ª·¢½ø¶È.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,3382 @@ # æºè½è´¨æ£æ¥åè®¾è®¡å¹³å° â å¼åè¿åº¦ > 便®ææ¡£ï¼`docs/æºè½è´¨æ£æ¥å设计平å°ââClaude Code Agent ä¸ä¸å¼åæç¤ºè¯.md` > æ¹æ¡è®¾è®¡ï¼`docs/æºè½è´¨æ£æ¥åå¹³å°-æ¹æ¡è®¾è®¡.md` > å»ºè¡¨èæ¬ï¼`docs/sql/qc_report_platform_ddl.sql` --- ## 2026-09-17ï¼ç¬¬ 1 å¤©ï¼ ### ä¸ã仿¥å®æé¡¹ | # | äºé¡¹ | ç¶æ | äº§åº | |---|---|---|---| | 1 | **Phase 0 é¡¹ç®æ«æ**ï¼å端 + åç«¯å¹¶è¡æ«æï¼ | â 宿 | å端模åå¸å±/è´¨æ£å/PDFè½å/æä»¶åå¨/æ¡æ¶å·¥å ·ãå端 Vben5.7+AntDV4.2.6/è·¯ç±èå驱å¨/GrapesJS 缺失 çç»è®º | | 2 | **æ¹æ¡è®¾è®¡ææ¡£** | â 宿 | `docs/æºè½è´¨æ£æ¥åå¹³å°-æ¹æ¡è®¾è®¡.md`ï¼21 ä¸ªç« èï¼ææ¯æ ãå¯å¤ç¨è½åãæ°æ®åºè®¾è®¡ãç»ä»¶æ¶æãGrapesJS éæãSchemaãç»å®/è§å/渲æ/Playwright/åé¡µæ¹æ¡ãAPI èæ¡ãPhase 1~5 计åãé£é©æ¸ åãå¾ ç¡®è®¤é®é¢ï¼ | | 3 | **å ³é®å³ç确认**ï¼ç¨æ·ææ¿ï¼ | â 宿 | è§ä¸æ¹ãäºãå ³é®å³çã | | 4 | **å端模å骨æ¶** | â 宿 | æ°å»º `yudao-module-qcreport` 模åï¼pom + 宿´å ç®å½éª¨æ¶ï¼ | | 5 | **æ¨¡åæ³¨å** | â 宿 | æ ¹ `pom.xml` ç `<modules>`ã`yudao-server/pom.xml` ç `<dependencies>` åå 䏿¡ | | 6 | **é误ç å®ä¹** | â 宿 | `enums/ErrorCodeConstants.java`ï¼ä½¿ç¨ **1-070-000-000** 段ï¼å·²æ ¸å¯¹ 1-001~1-013/020/022/030/040/050/051/060 å被å ç¨ï¼ | | 7 | **æ ¸å¿ DO** | â 宿 | `QcReportTemplateDO`ã`QcReportTemplateVersionDO`ã`QcReportInstanceDO` | | 8 | **æ¨¡æ¿ Schema 模å** | â 宿 | `ReportTemplateSchema`ï¼page/grapes/components/dataSources/bindings/rules/stylesï¼ | | 9 | **æä¸¾éå** | â 宿 | `QcReportEnums`ï¼TemplateStatus / VersionStatus / InstanceStatus / PageSize / Orientation / CheckResult | | 10 | **å»ºè¡¨èæ¬** | â 宿 | `docs/sql/qc_report_platform_ddl.sql`ï¼6 å¼ è¡¨ï¼æ tenant_idï¼ | ### äºãå ³é®å³çï¼å·²è·ç¨æ·ç¡®è®¤ï¼ | è®®é¢ | å³ç | 说æ | |---|---|---| | **åç«¯ä¿®æ¹ææ** | â **å·²ææ** | å è®¸ä¿®æ¹ `mom-pro2-before`ï¼æ°å¢é¡µé¢/ç»ä»¶/API æä»¶ï¼ä¸éææ¢æä»£ç ï¼ | | **PDF æ¹æ¡** | â **ç¨ Playwright + Chromium** | ç¨æ·ç¡®è®¤è¿è¡/é¨ç½²ç¯å¢**è½èç½**ï¼å¯ä¸è½½ Chromium | | **仿¥èå´** | â **å端模åéª¨æ¶ + æ¨¡æ¿ CRUD** | 渲æé¾è·¯ä¸å端设计å¨åç»è¿è¡ | | **模åå½å** | â `yudao-module-qcreport` | 表å `qc_report_*`ï¼æ¥å£åç¼ `/qc-report/*`ï¼è´´å项ç®ã模å_åãå½åä¹ æ¯ï¼ | | å端模åå½å± | ç¬ç«æ°æ¨¡å | ä¸è¡ä¸ä¸å¡è§£è¦ï¼ææ¡£ §35 è¦æ±æ ¸å¿ç¼è¾å¨ä¸ä¸è¡ä¸å¼ºè¦åï¼ | ### ä¸ãæ«æå¾å°çå ³é®äºå®ï¼åç»å¼åå¿ é¡»éµå®ï¼ 1. **UI æ å¯ä¸**ï¼åç«¯æ¯ **Ant Design Vue 4.2.6 + Vben Admin 5.7.0 ååºç¨ç**ï¼**æ Element Plus**ãæ°é¡µé¢å¿ é¡»ç¨ AntDVï¼ç¦æ¢å¼å ¥ Element Plusã 2. **èå/è·¯ç±ç±å端驱å¨**ï¼é¡µé¢æ¾å° `src/views/**/index.vue`ï¼å端 `system_menu.component` åç¸å¯¹è·¯å¾ï¼å¦ `mes/qc/report/template/index`ï¼å³å¯åºç°èåï¼**æ éæå·¥æ³¨åè·¯ç±**ã 3. **è´¨æ£ä¸å¡æ¨¡åå·²åå¨ä¸å®å¤**ï¼MES `qc` åï¼ï¼`mes_qc_iqc / ipqc / oqc / rqc` + `*_line`ã`mes_qc_template / _indicator / _item`ã`mes_qc_indicator_result(_detail)`ã`mes_qc_ncr`ã**æ¥åå¹³å°å¿ é¡»ç»å®å¤ç¨ï¼ç¦æ¢éå»ºè´¨æ£æ¨¡åã** 4. **项ç®é¶ PDF çæè½å**ï¼ä» æ PDFBoxï¼CRM/ERP åªè¯»è§£æï¼ãFastExcelï¼Excel 导åºï¼ãPlaywright/Chromium/FreeMarker/openhtmltopdf **å ¨é¨ä¸åå¨**ï¼éæ°å¢ã 5. **ç§¯æ¨æ¥è¡¨æ¨¡å被注éç¦ç¨**ï¼æ ¹ `pom.xml` ä¸ `yudao-server/pom.xml` ä¸ `yudao-module-report` 为注éç¶æï¼æ³¨é说æãç§¯æ¨æ¥è¡¨æä¸æ¯æ Spring Boot 4ãï¼ââæ æ°æ¨¡å**ä¸å¤ç¨è¯¥åå**ï¼æ¹ç¨ `yudao-module-qcreport`ã 6. **åç«¯å·²è£ å¯ç´æ¥å¤ç¨çåº**ï¼é¿å éå¤é è½®åï¼ï¼ - `qrcode 1.5.4` + `jsbarcode 3.12.3`ï¼å·²å°è£ `packages/effects/common-ui/src/components/barcode/barcode.vue`ï¼å« `getImageBase64()`ï¼ - `vue3-print-nb 0.1.4`ï¼æå°ï¼èä¾ `views/bpm/processInstance/detail/modules/process-print.vue`ï¼ - `vue3-signature 0.4.4`ï¼çµåç¾åï¼HRM å·²ç¨ï¼ - `vuedraggable 4.1.0` + `sortablejs 1.15.7`ï¼ææ½ï¼ - `tinymce 7.9.3` / `@tiptap/*`ï¼å¯ææ¬ï¼ã`echarts 6.1.0`ï¼å¾è¡¨ï¼ - `bpmn-js 18.16.1`ï¼é¡¹ç®å å¯ä¸ãç»å¸+屿§é¢æ¿ãå ä¾ï¼å¯åèå¸å±ç»ç»ï¼ 7. **GrapesJS 确认æªå®è£ **ï¼Phase 1 éæ°å¢ä¾èµï¼ä½ç§¯/æå»ºå½±åéè¯ä¼°ï¼ã 8. **JSON ååå¨èå¼**ï¼`@TableName(value=..., autoResultMap = true)` + `@TableField(typeHandler = Jackson3TypeHandler.class)`ï¼èä¾ `BpmFormDO.java:53`ï¼ã 9. **æ¡æ¶éç¨ä»¶**ï¼`CommonResult` / `PageResult` / `PageParam` / `BaseDO`ï¼creator/updater/createTime/updateTime/deleted èªå¨å¡«å ï¼/ `ServiceException`+`ServiceExceptionUtil.exception(...)` / `BeanUtils` / `BaseMapperX` / `LambdaQueryWrapperX`ã 10. **æ å¤ç§æ·**ï¼`yudao.tenant.enable: false`ï¼æ°è¡¨**ä¸å tenant_id**ï¼ä¸é¡¹ç®è§åä¸è´ï¼ã 11. **æµè¯åºç±»**ï¼JUnit5 + `BaseDbUnitTest`ï¼H2ï¼/ `BaseMockitoUnitTest` + PODAMã 12. **æå»ºå½ä»¤**ï¼å端 `mvn compile -pl <模å> -am -q` / å ¨é `mvn compile -q`ï¼å端 `pnpm dev`(5666) / `pnpm build` / `pnpm typecheck`ã ### åã仿¥æ°å¢/ä¿®æ¹æä»¶æ¸ å **æ°å¢ï¼å端ï¼** ``` yudao-module-qcreport/pom.xml yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/ âââ enums/ErrorCodeConstants.java # 1-070 段é误ç âââ enums/QcReportEnums.java # 6 ç»æä¸¾ âââ dal/dataobject/template/QcReportTemplateDO.java âââ dal/dataobject/version/QcReportTemplateVersionDO.java âââ dal/dataobject/version/ReportTemplateSchema.java âââ dal/dataobject/instance/QcReportInstanceDO.java ï¼å¦å·²å»ºå¥½ controller/service/dal.mysql/engine{bounding,rule,render}/config ç空ç®å½éª¨æ¶ï¼ ``` **ä¿®æ¹ï¼åç«¯å ¬å ±æä»¶ï¼å½±åé¢å·²è¯ä¼°ï¼** | æä»¶ | æ¹å¨ | å½±åé¢ | |---|---|---| | `pom.xml` | `<modules>` å¢å `yudao-module-qcreport` | ä» æ°å¢ä¸ä¸ªå模åï¼ä¸å½±åæ¢ææ¨¡åæå»º | | `yudao-server/pom.xml` | `<dependencies>` å¢å `yudao-module-qcreport` | å¯å¨æ¨¡åå¤èåä¸ä¸ª jar | **æ°å¢ï¼ææ¡£/SQLï¼** ``` docs/æºè½è´¨æ£æ¥åå¹³å°-æ¹æ¡è®¾è®¡.md docs/æºè½è´¨æ£æ¥åå¹³å°-å¼åè¿åº¦.md # æ¬æä»¶ docs/sql/qc_report_platform_ddl.sql # 6 å¼ è¡¨å»ºè¡¨èæ¬ ``` ### äºãè¿è¡ä¸ / æªå®æé¡¹ | äºé¡¹ | ç¶æ | ä¸ä¸æ¥ | |---|---|---| | æ°æ¨¡åç¼è¯éªè¯ | â **å·²éè¿** | `mvn compile -pl yudao-module-qcreport -am -q` **exit code 0**ï¼`target/classes/.../qcreport/{dal,enums}` 已产åºãæ éä¿®å¤ | | æ¨¡æ¿ CRUDï¼Controller/Service/Mapper/VOï¼ | æªå¼å§ | Phase 1 é¦è¦ä»»å¡ | | çæ¬ CRUD + åå¸/åæ» | ⬠æªå¼å§ | | | èå/æé SQLï¼`system_menu` + æéç ï¼ | æªå¼å§ | éæå½åæ¿æ´» profile æåçç®æ åºåå ¥ | | DDL è½åºæ§è¡ | ⬠æªå¼å§ | 注æï¼**ä¸å¾å¨è¿è¡åº `ruoyi-vue-pro` éææ§è¡**ï¼æç®æ åºæ§è¡ | | å端 Designer + Quality Components | ⬠æªå¼å§ | å·²è·ææ | | GrapesJS ä¾èµå¼å ¥ | ⬠æªå¼å§ | éè¯ä¼°ä½ç§¯ä¸ Vite æå»ºå½±å | | æ°æ®ç»å®å¼æ / è§å弿 | ⬠æªå¼å§ | **ç¦æ¢ç¨ eval**ï¼é¡»èªå»º AST è§£æç½ååæ±å¼ | | HTML 渲æ + Playwright PDF | ⬠æªå¼å§ | éå `com.microsoft.playwright:playwright` å° `yudao-dependencies` | | æ¥åå®ä¾ + data_snapshot | æªå¼å§ | | ### å ã踩è¿çå / 注æäºé¡¹ 1. **éè¯¯ç æ®µå·å²çª**ï¼æ°å¢æ¨¡ååå¿ é¡»å grep å ¨é¡¹ç® `ErrorCodeConstants` ç¡®è®¤æ®µå·æªè¢«å ç¨ï¼æ¬é¡¹ç®å·²ç¨ 1-001~1-013ã1-020ã1-022ã1-030ã1-040ã1-050ã1-051ã1-060ï¼ï¼æ¬æ¬¡éç¨ **1-070**ã 2. **`yudao-module-report` åå被å **ï¼ç§¯æ¨æ¥è¡¨ï¼æ³¨éç¶æï¼ï¼æ°æ¨¡åé¿å¼è¯¥åï¼é¿å æ¥åå¯ç¨ç§¯æ¨æ¥è¡¨æ¶å²çªã 3. **å端æ¯ãååºç¨çã**ï¼`@vben/web-antd-standalone`ï¼ï¼ä¸å¡ä»£ç å¨**æ ¹ `src/`**ï¼ä¸æ¯ `apps/web-antd/src`ãæ«ææ¶å®¹æçéã 4. **å端æ Element Plus**ï¼æ¹æ¡ææ¡£æå° Vben + AntDVï¼å®é 确认**æ Element Plus**ï¼ä¸è¦æ"æä¸å¡æ¨¡åå·²ç»ä¸ç¨ Element Plus"å»éåã 5. **`docs/` ä¸å·²æå¤§éæ¢ææ¹æ¡ææ¡£**ï¼æ°ææ¡£å½åé带模ååç¼ï¼æ¬æ¬¡ç¨ãæºè½è´¨æ£æ¥åå¹³å°-ãï¼ä»¥å æ··æ·ã 6. **DDL ä¸ç `schema` æ¯ MySQL ä¿çå**ï¼å»ºè¡¨èæ¬å·²ç¨åå¼å·å 裹ï¼MyBatis-Plus æ å°æ¶å段å `schema` ä¹é注æï¼å½åç± `Jackson3TypeHandler` æ å°ä¸º `ReportTemplateSchema` å¯¹è±¡ï¼æ£å¸¸ï¼ã 7. **Playwright 馿¬¡ä½¿ç¨éä¸è½½ Chromium**ï¼çº¦ 150MBï¼ï¼ç¨æ·å·²ç¡®è®¤ç¯å¢å¯èç½ï¼å®ç°æ¶éå¤çãæªå®è£ æ¶ç»åºå¯è¯»æç¤ºãï¼å·²å®ä¹é误ç `RENDER_PDF_BROWSER_LAUNCH_FAILED`ï¼ã ### ä¸ãææ¥ï¼2026-09-18ï¼å¾ 忏 å 1. **ç¡®è®¤æ°æ¨¡åç¼è¯ç»æ**ï¼ä¿®å¤ç¼è¯é误ï¼`mvn compile -pl yudao-module-qcreport -am -q`ï¼ã 2. **æ§è¡ DDL è½åº**ï¼ç®æ åº = **`ruoyi-vue-pro`**ï¼2026-09-17 æå®æµï¼`application.yaml:6` active=localï¼`application-local.yaml:68` master æ°æ®æº = `jdbc:mysql://localhost:3306/ruoyi-vue-pro`ï¼root/123456ï¼ã â 该é ç½®æ¼ç§»é¢ç¹ï¼åå²ä¸åºç°è¿ mom-xgdl / mom-thzyï¼ï¼**卿åå¡å¿ éæ° grep 䏿¬¡ datasource**ï¼å¦é确认 MySQL MCP è¿çæ¯å¦åä¸å®ä¾ï¼ä¸æ¯åç¨ `docker exec mysql8 mysql -uroot -p123456 -e "..."`ï¼å¹¶ç¨ `information_schema` å读éªè¯ã 3. **å®ææ¨¡æ¿ CRUD**ï¼Phase 1 å端é¨åï¼ï¼ - `QcReportTemplateMapper` + `QcReportTemplateService/Impl` + `QcReportTemplateController` - VOï¼`QcReportTemplateSaveReqVO` / `PageReqVO` / `RespVO` - æ¥è¯¢æ¡ä»¶ï¼æ¨¡æ¿åç§°ãç¼ç ãè¡ä¸ãæ¥åç±»åãç¶æãå建人ãå建æ¶é´ - æ¥å£ï¼create / update / delete / get / page / copy 4. **çæ¬ CRUD**ï¼`QcReportTemplateVersionMapper/Service/Controller`ï¼create / list / publish / rollbackï¼åå¸åä¸å¯è¦çï¼ã 5. **èå䏿é**ï¼`system_menu` èåè®°å½ + æéç `qc-report:template:*`ã`qc-report:instance:*`ï¼æç®æ åºåå ¥ã 6. **å端起æ¥**ï¼å·²è·ææï¼ï¼è¯ä¼°å¹¶å¼å ¥ GrapesJSï¼æ `src/views/mes/qc/report/template/` å表页ï¼ç §æ `src/views/mes/qc/defect/` ç»æï¼+ `src/api/mes/qc/report/` API æä»¶ã 7. ï¼è¥æ¶é´å 许ï¼å¯å¨ **Quality Components core**ï¼types/registry/factory/serializer/validatorï¼ã ### å «ãé£é©æéï¼ææ¥å¼å·¥åå çï¼ | # | é£é© | ç级 | |---|---|---| | 1 | Playwright Chromium ä¸è½½åç½ç»éå¶ï¼å·²ç¡®è®¤å¯èç½ï¼ä½é宿µï¼ | ð¡ | | 2 | GrapesJS æ°å¢ä¾èµå¯¹ Vite æå»ºä½ç§¯çå½±å | ð¡ | | 3 | è§åå¼æè¡¨è¾¾å¼å®å ¨ï¼**ä¸¥ç¦ eval / ScriptEngine**ï¼ | | | 4 | æ¨¡æ¿ HTML å±ç¨æ·å¯ç¼è¾å 容 â å¿ é¡»æå¡ç«¯ Sanitizationï¼XSSï¼ | ð¡ | | 5 | æ°è¡¨ DDL å¿ é¡»è½å°**ç®æ åº**ï¼ç¦æ¢æ±¡æè¿è¡åº | ð¡ | | 6 | 100+ 页 PDF / 1000+ è¡è¡¨æ ¼çæ§è½ä¸å å | ð¢ | --- ## 2026-09-18ï¼ç¬¬ 2 å¤©ï¼ ### ä¸ã仿¥å®æé¡¹ | # | äºé¡¹ | ç¶æ | äº§åº | |---|---|---|---| | 1 | **DDL / åå ¸ / èå è½åº** | â 宿 | `qc_report_platform_ddl.sql`ï¼6 å¼ è¡¨ï¼ã`qc_report_dict.sql`ã`qc_report_menu.sql` å·²æ§è¡å°è¿è¡åº `ruoyi-vue-pro` | | 2 | **æ¨¡æ¿ CRUD å端** | â 宿 | Controller/Service/Mapper/VO å ¨å¥ï¼å« **copyï¼å¤å¶æ¨¡æ¿ï¼**ï¼æ¥å£ `/qc-report/template/{create,update,delete,get,page,copy}` | | 3 | **çæ¬ CRUD + åå¸** | â 宿 | `/qc-report/template-version/{create,update,get,list-by-template,publish}`ï¼**å·²åå¸çæ¬ä¸å¯è¦ç**ï¼æ¹å¨åªè½å¦åæ°è稿 | | 4 | **èå䏿éç ** | â 宿 | èåãè´¨é管ç â æ¥å模æ¿ã+ æéç `qc-report:template:*` | | 5 | **å端å¼å ¥ GrapesJS + å表页** | â 宿 | `grapesjs ^0.23.6`ï¼ç» `catalog:` 声æï¼ï¼å表页 `template/index.vue` + `modules/form.vue` + `data.ts` | | 6 | **模æ¿è®¾è®¡å¨é¡µé¢ï¼Phase 1 å端ï¼** | â 宿 | `template/designer/`ï¼`index.vue` / `use-designer.ts` / `blocks.ts` / `constants.ts`ï¼+ éæè·¯ç± `router/routes/modules/qcreport.ts` | | 7 | **设计å¨ç«¯å°ç«¯èè°éªè¯** | â 宿 | è§ä¸æ¹ãäºã设计å¨èè°ï¼å®ä½å¹¶ä¿®å¤ç 12 个é®é¢ã | ### äºã设计å¨èè°ï¼å®ä½å¹¶ä¿®å¤ç 12 个é®é¢ Phase 1 çè®¾è®¡å¨æ¯æ GrapesJS åµè¿ AntDV 页é¢ï¼ç¬ç«è¸©äº 12 个åï¼å已修å¤å¹¶éæ¡å¨æµè§å¨å®æµï¼ | # | ç°è±¡ | æ ¹å | ä¿®å¤ | |---|---|---|---| | 1 | 页é¢ç½å±ï¼æ§å¶å°æ¥ `does not provide an export named 'useTabs'` | `useTabs` ä» `@vben/common-ui` å¯¼å ¥ï¼å®é å¨ **`@vben/hooks`** | æ¹å¯¼å ¥æ¥æºï¼`designer/index.vue`ï¼ | | 2 | è°æ´çº¸å¼ 设置åï¼ç»å¸ä¸æªä¿åçå å®¹è¢«æ¸ ç©º | ãåºç¨å°ç»å¸ãå¤ç¨äº `loadSchema()`ï¼ä¼æä¸æ¬¡ä¿åçæ°æ®éæ°çåç»å¸ | æ°å¢**éç ´åå¼** `applyPageSize()`ï¼åªéæå°ºå¯¸ä¸åè线ï¼ä¸å¨ç»å¸å 容 | | 3 | ç»å¸ç©ºç½ï¼iframe 尺寸 0Ã0 | `Devices.remove(id)` + `add(id)` + `setDevice(id)` å device å弿²¡åï¼Backbone ä¸è§¦å `change:device`ï¼`Canvas.updateDevice` 䏿§è¡ | è®¾å¤ id ç¼ç 尺寸ï¼`qc-report-page-1123x794`ï¼ï¼ä¿è¯åå¼çåå | | 4 | ä¿å䏿¥çæ ·å¼æå¨ `@media (max-width: 794px)` ä¸ï¼æ¢çº¸å¼ å³å¤±æ | `Device` ç `widthMedia` 为 `null` æ¶ä¼åéæ `width`ï¼äº§çæè®¾å¤å®½çåªä½æ¥è¯¢ | 注åè®¾å¤æ¶æ¾å¼ `widthMedia: ''` | | 5 | èæ¨¡æ¿æå¼æ¯ç©ºç½ç¼è¾å¨ï¼ä¸æ¯åæå å®¹ï¼ | æ§æ°æ® `grapes` åªæ `{assets, styles}`ã**没æ `pages`**ï¼`loadProjectData` è½½ä¸åºé¡µé¢ | `loadSchema` å¢å `Array.isArray(pages) && pages.length > 0` å®å« | | 6 | çº¸å¼ çä¸è§ï¼æ·±è²èæ¯ï¼ | GrapesJS é»è®¤**æ·±è²ä¸»é¢**ï¼ä¸ `--gjs-left-width:15%`/`--gjs-canvas-top:40px` ä»å¨çæï¼`panels:{defaults:[]}` å¹¶ä¸è½å»æå®ä»¬ | å¨é¡µé¢æ ¹èç¹è¦ç `--gjs-*` åéä¸ºæµ è²ä¸»é¢ + ç½®é¶ `--gjs-left-width`/`--gjs-canvas-top` | | 7 | çº¸å¼ ç½åºæ¶ææ¶æ | `canvas.frameStyle` **åªä½ç¨äºç¬¬ä¸ä¸ªç»å¸å¸§**ï¼`loadProjectData` æ°å»ºç帧没ç½åº | `applyFrameGuide()` 卿¯æ¬¡ `canvas:frame:load` ä¸éå»ºåæ¾å¼å body ç½åº + é¡µè¾¹è· | | 8 | ç»å¸æ¯å¯è§åºå¤§æ¶è¢«è£æï¼ä¸**æ æ³æ»å¨** | `.gjs-cv-canvas` æ¯ `overflow:hidden` çç»å¯¹å®ä½èå°ï¼å¸§ä¹æ¯ç»å¯¹å®ä½ | è¦ç为 `overflow:auto` æ»å¨å®¹å¨ï¼å¸§å®¹å¨/帧å è£ `position:static`ã`margin:24px auto` åå½ææ¡£æµ | | 9 | **ç¹å»å·¦ä¾§ç»ä»¶åæ ä»»ä½ååº** | `BlockView.handleClick` å¨ä¸æ»¡è¶³ `block.onClick \|\| config.appendOnClick` æ¶ç´æ¥ return | `blockManager.appendOnClick: true`ï¼ç¹å»å³è¿½å ï¼ææ½åæ¶å¯ç¨ï¼ | | 10 | **å·¦å³ä¸¤ä¾§ãç»ä»¶ / 屿§ã颿¿æ»ä¸å¨ï¼åºé¨è¢«è£** | Ant `Spin` å¤å äºä¸å± `.ant-spin-container`ï¼å®é»è®¤æ¯**å 容é«åº¦**èéææ»¡ç¶çº§ï¼æä¸¤æ æå° 785px > å¯è§åº 735pxï¼`overflow:auto` æ°¸ä¸è§¦å | `:deep(.ant-spin-container){ height:100% }` æé«åº¦é¾æ¥å `flex-1` ç¶çº§ | | 11 | ç»å¸ iframe ç `<style>` éåºç°åé¢é `[object Object]` | `canvas.frameStyle` ä¼è¢«**åæ ·æ¼æ¥**æ CSS ææ¬ï¼ä¼ 对象å³åºé | æ¹ä¸ºä¼ CSS å符串 | | 12 | æå¼å·²å é¤çæ¨¡æ¿ / 忢已å é¤ççæ¬æ¶**æ´é¡µå´©æº**ï¼`Cannot read properties of null`ï¼ | `getTemplate`/`getVersion` 对已软å é¤çè®°å½è¿å `data:null`ï¼å端æªå | 两å¤åå 空å¼å®å«ï¼æç¤ºãä¸åå¨æå·²è¢«å é¤ãå¹¶è¿åå表 / å·æ°çæ¬å表 | ### ä¸ãèè°éªè¯ç»è®ºï¼æµè§å¨å®æµï¼ | éªè¯é¡¹ | ç»æ | |---|---| | ç¹å»ç»ä»¶å追å å°ç»å¸ | â 追å åº `data-quality-type` 为 `Heading` / `Table` çç»ä»¶ï¼è¡¨æ ¼ 12 个åå æ ¼æ£å¸¸çæ | | ç»å¸æ»å¨ | â `scrollHeight 842 > clientHeight 725`ï¼å¯æ»å¨ | | 左侧ç»ä»¶é¢æ¿æ»å¨ | â `scrollHeight 785 > clientHeight 735`ï¼å®æµ `scrollTop` å¯è¾¾ 50.7 | | å³ä¾§å±æ§é¢æ¿æ»å¨ | â éä¸ç»ä»¶å¹¶å±å¼æ ·å¼ååºå `scrollHeight 1458 > clientHeight 735`ï¼å®æµå¯æ»å°åº | | ä¿åèç¨¿ï¼æ°å»ºçæ¬ï¼ | â æ°æ¨¡æ¿ä¿ååº `v1.0(è稿)`ï¼è½åº `schema_json` 4527 å符 | | ä¿åå 容æ£ç¡®æ§ | â `schemaVersion=1.0`ã`page=A4纵å10mm`ã`grapes.pages` 1 个ã`components` è®°å½ `ReportHeader`+`Table` ä¸å¸¦ `index`ï¼**æ `mediaText`ãæ `[object Object]`** | | éæ°æå¼è¿å | â çæ¬ä¸ææ¾ç¤ºãv1.0ï¼è稿ï¼ãï¼ç»å¸è¿ååº `ReportHeader`+`Table` ä¸é¡µè¾¹è· | | åç«¯ç±»åæ£æ¥ | â `pnpm typecheck` 卿¬æ¨¡åæ°æ¹æä»¶ä¸ **0 error**ï¼ä»åºå ¶ä½ 316 æ¡ä¸ºæ¢æé®é¢ï¼éä¸å¨ `views/bi/warehouse`ã`views/wls`ã`views/crm` çï¼éæ¬æ¬¡å¼å ¥ï¼ | ### åã仿¥æ°å¢/ä¿®æ¹æä»¶æ¸ å **æ°å¢ï¼å端 `yudao-module-qcreport`ï¼** ``` controller/admin/template/QcReportTemplateController.java controller/admin/template/vo/{QcReportTemplatePageReqVO,RespVO,SaveReqVO}.java controller/admin/version/QcReportTemplateVersionController.java controller/admin/version/vo/{PageReqVO,RespVO,SaveReqVO,UpdateReqVO}.java dal/mysql/{template/QcReportTemplateMapper,version/QcReportTemplateVersionMapper}.java service/template/{QcReportTemplateService,QcReportTemplateServiceImpl}.java service/version/{QcReportTemplateVersionService,QcReportTemplateVersionServiceImpl}.java ``` **æ°å¢ï¼å端 `mom-pro2-before`ï¼** ``` src/api/mes/qc/report/template/index.ts src/api/mes/qc/report/version/index.ts src/router/routes/modules/qcreport.ts # 设计å¨éæè·¯ç±ï¼hideInMenuï¼å¸¦ id è·³è½¬ï¼ src/views/mes/qc/report/template/index.vue # 模æ¿åè¡¨é¡µï¼æ°å¢ã设计ãå ¥å£ï¼ src/views/mes/qc/report/template/modules/form.vue src/views/mes/qc/report/template/data.ts # å表/表å schema src/views/mes/qc/report/template/designer/index.vue # 设计å¨é¡µé¢ï¼é¡¶æ /çº¸å¼ è®¾ç½®/䏿 å¸å±ï¼ src/views/mes/qc/report/template/designer/use-designer.ts # GrapesJS å°è£ ï¼ç»å¸ â Schema åå转æ¢ï¼ src/views/mes/qc/report/template/designer/blocks.ts # åºç¡ç»ä»¶åå®ä¹ src/views/mes/qc/report/template/designer/constants.ts # çº¸å¼ å°ºå¯¸/é»è®¤é¡µ/æ¢ç® ``` **æ°å¢ï¼SQLï¼**ï¼`docs/sql/qc_report_dict.sql`ã`docs/sql/qc_report_menu.sql` ### äºãè¿è¡ä¸ / æªå®æé¡¹ | äºé¡¹ | ç¶æ | ä¸ä¸æ¥ | |---|---|---| | GrapesJS 设计å¨ï¼Phase 1ï¼ | â 宿并宿µ | â | | **Quality Components æ³¨åæºå¶** | â 宿 | è§ææ«ãPhase 2 宿½è®°å½ã | | è´¨éç»ä»¶æ¬ä½ | â 宿 | 12 个ç»ä»¶ï¼åºç¡ 6 + æ¥å 3 + æ£éª 2 + ç»æ 1ï¼ | | QualityPropertyPanelï¼ä¸å¡å±æ§é¢æ¿ï¼ | â 宿 | åè®®åæ®µç» traits è½å°å±æ§é¢æ¿ï¼å«ãç»å®æ°æ®ãæ§ä»¶ | | æ°æ®ç»å®å¼æ | â 宿 | è·¯å¾è§£æ + åå¼ + 缺å£ä¸æ¥ | | è§å弿 | â 宿 | èªå»º AST ç½ååæ±å¼ï¼**æ eval / ScriptEngine** | | HTML 渲æå¼æ | â 宿 | è§ææ«ãPhase 2 宿½è®°å½ã | | Playwright / Chromium PDF | ⬠æªå¼å§ | Phase 3ï¼**ç¦æ¢ç¨æªå¾æ¹å¼çæ PDF**ï¼éå å®ãå端渲æ HTML â å端æå°ãè¿æ¯ãå端 Java é忏²æã | | æ¥åå®ä¾ + `data_snapshot` | ⬠æªå¼å§ | Phase 4ï¼çææ¶å¿«ç §ï¼ä¿è¯å岿¥åä¸éä¸»æ°æ®åå | | èªå¨åæµè¯ / E2E | ⬠æªå¼å§ | Phase 5 | ### å ã踩è¿çå / 注æäºé¡¹ï¼è¡¥å ï¼ 1. **GrapesJS ç `frameStyle` æ¯ CSS åç¬¦ä¸²ä¸æ¯å¯¹è±¡**ï¼ä¼ 对象ä¼è¢«æ¼æ `[object Object]` åè¿ç»å¸ã 2. **GrapesJS é»è®¤æ·±è²ä¸»é¢**ï¼å³ä½¿ `panels:{defaults:[]}` 廿äºé»è®¤é¢æ¿ï¼`--gjs-left-width`/`--gjs-canvas-top` ä»ä¼å½±å `.gjs-cv-canvas` ç宽é«è®¡ç®ï¼å¿ é¡»ä¸å¹¶è¦çã 3. **`Devices` ç `setDevice` å¨ id ä¸åæ¶ä¸ä¼è§¦å `change:device`**ï¼Backbone è¯ä¹ï¼ï¼è¦éæç»å¸å°±å¾è®© device å¼ççååã 4. **`Device.widthMedia` 为 `null` ä¼åéæ `width`**ï¼ä»èææ ·å¼åè¿ `@media (max-width: Npx)`ï¼æ¨¡æ¿ç±»ç»å¸åºæ¾å¼ä¼ `''`ã 5. **Ant `Spin` ä¼æå ¥ `.ant-spin-container` ææ `height:100%` é¾**ãä»»ä½ãSpin å 裹 + å é¨ flex ææ»¡ + å é¨å overflow:autoãçå¸å±é½è¦è¡¥ `:deep(.ant-spin-container){height:100%}`ï¼å¦åå 鍿»å¨æ¡æ°¸è¿ä¸åºç°ãåºé¨è¢«éé»è£æã 6. **`loadProjectData` éè¦å¸¦ `pages` çé¡¹ç®æ°æ®**ï¼`{assets,styles}` è¿ç§ä¼è¢«è½½æç©ºç¼è¾å¨ï¼è½½å ¥åå¿ é¡»å¤ç©ºã 7. å端对**已软å é¤**çè®°å½ï¼`get` æ¥å£è¿å `data:null`ï¼ä¸æ¯æ¥éï¼ï¼åç«¯æææ id å详æ çå ¥å£é½è¦å 空ã ### ä¸ãç¯å¢ç¶æä¸éçæé 1. **å端ä¸å¨è¿è¡**ï¼2026-09-18 12:18 ä¹å `48080` å·²æ çå¬ï¼æ¥å¿åå¨ 12:18:37ï¼æ å¼å¸¸æ ï¼æ¯è¢«åæ¢èéå´©æºï¼ã彿¥æå䏿¬¡ãä¿åãå æ¤è¿å **502 Bad Gateway**ï¼å端已æç»ä¸é误æç¤ºï¼ééé»å¤±è´¥ï¼ã å端ç±ç¨æ·å¨ IntelliJ ä¸è¿è¡ï¼`-Dmaven.multiModuleProjectDirectory=.../mom-pro2-after-test`ï¼ï¼**å¯å¨/åæ¢äº¤ç±ç¨æ·æå¨å¤ç**ã 2. **èè°éçæ°æ®**ï¼å½æ¥ä¸ºéªè¯ä¿åé¾è·¯ï¼å¨**æ¨¡æ¿ 3ï¼QC_IQC_DEMO_COPYï¼**ä¸äº§çäºä¸æ¡ `v1.0` èç¨¿çæ¬ï¼`qc_report_template_version.id=6`ï¼ââè¯¥æ¨¡æ¿æ¤åæ çæ¬ï¼å¦ä¸éè¦å¯ç´æ¥å é¤ã 3. æ°æ®åºï¼`application.yaml` `active: local` â `application-local.yaml` æå `jdbc:mysql://localhost:3306/ruoyi-vue-pro`ã**该é ç½®åå²ä¸æ¼ç§»è¿å¤æ¬¡ï¼å¨æåå¡å¿ éæ°ç¡®è®¤ã** ### å «ãææ¥ï¼2026-09-19ï¼å¾ 忏 å > è¯¥æ¸ åå·²å½åºæ§è¡å®æ¯ï¼å®æ½ä¸éªè¯è®°å½è§ä¸ä¸èãPhase 2 宿½è®°å½ãã 1. **Phase 2 èµ·æ¥ï¼Quality Components æ³¨åæºå¶** - `engine/component` ä¸ç `types / registry / factory / serializer / validator`ï¼ç¬ç«æ³¨åï¼ä¸è¿ GrapesJS å æ ¸ï¼ - 设计å¨ç左侧ãè´¨éç»ä»¶ãåç±»æ¹ä¸ºä» registry 卿çæï¼è䏿¯åæ»å¨ `blocks.ts` 2. **è´¨éç»ä»¶æ¬ä½**ï¼QualityTable / SampleInfo / ReportHeader / InspectionItem / Result äºç±»ï¼æ¯ç±»å¸¦ `qualityType` + 屿§ schemaã 3. **QualityPropertyPanel**ï¼éä¸è´¨éç»ä»¶æ¶ï¼å³ä¾§é¤ GrapesJS æ ·å¼é¢æ¿å¤ï¼å渲æä¸å¡å±æ§è¡¨åã 4. **æ°æ®ç»å®å¼æ**ï¼ç»å®è·¯å¾è§£æ + åå¼ï¼ï¼ä¸º Phase 3 渲æååå¤ã 5. **è§å弿**ï¼å å®è¡¨è¾¾å¼è¯æ³ä¸ AST æ±å¼å¨éª¨æ¶ï¼**ä¸¥ç¦ `eval` / `ScriptEngine`**ã 6. æ¶å°¾ï¼ç¡®è®¤æ¯å¦æ¸ çæ¨¡æ¿ 3 çèè°éççæ¬ã --- ## Phase 2 宿½è®°å½ï¼2026-09-18 ç»ï¼ > ç®æ ï¼è®©ãè´¨éç»ä»¶ãæä¸ºä¸å¥**ç¬ç«äº GrapesJS çæ³¨åæºå¶**ï¼å¹¶æé > `æ¨¡æ¿ Schema + æ¥åä¸ä¸æ â HTML` çæ¸²æé¾è·¯ï¼è¾¾æ Phase 2 éªæ¶æ å > **ã`inspectionItems` èªå¨çæè¡¨æ ¼å¹¶ç®åº PASS/FAILã**ã ### ä¸ã交ä»ç© **åè®®å±ï¼`src/components/quality/core/`ï¼** â ç»ä»¶å®ç°éä¸åºç°ä»»ä½ GrapesJS æ¦å¿µ | æä»¶ | èè´£ | |---|---| | `types.ts` | `QualityComponentDefinition` åè®®ï¼propertySchema / dataSchema / buildContent / validate | | `registry.ts` | å¹ç注åè¡¨ï¼æ type ç´¢å¼ | | `factory.ts` | åè®® â GrapesJS 产ç©ï¼ç»ä»¶åãç»å¸å 容ãtraitsï¼**å¯ä¸çæ¡¥æ¥å±**ï¼ | | `serializer.ts` | ä¸å¡å±æ§ â `data-qc-*` 屿§å¾è¿ | | `validator.ts` | 屿§æ¶æä¸æ ¡éª | | `attrs.ts` | `data-quality-type` / `data-qc-*` / `data-qc-repeat(-row)` / `data-qc-id` 常é | | `html.ts` / `icons.ts` | `cssStyle` / `escapeHtml` / å è SVG 徿 | | `lookup.ts` | æèç¹ä¸ç `data-quality-type` 忥å®ä¹ | | `canvas-sync.ts` | ç»å¸æå â ä¸å¡å±æ§åå忥ï¼è½½å ¥åé建 traits | | `trait-binding.ts` | ãç»å®æ°æ®ãèªå®ä¹ trait æ§ä»¶ï¼ä¸ææé + æå·¥è¾å ¥ + `tip` æç¤ºï¼ | **ç»ä»¶æ¬ä½ï¼12 个ï¼** - åºç¡ï¼`Heading` / `Text` / `Image` / `Divider` / `Spacer` / `Table` - æ¥åï¼`ReportHeader` / `ReportFooter` / `SampleInfo` - æ£éªï¼`QualityTable` / `InspectionItem` - ç»æï¼`ReportResult` æ°å¢ä¸ä¸ªç»ä»¶ï¼å¨ `index.ts` ç `BUILT_IN_DEFINITIONS` éå ä¸è¡ï¼**䏿¹è®¾è®¡å¨é¡µé¢ã䏿¹ GrapesJS éæä»£ç **ã **弿å±ï¼`src/components/quality/engine/`ï¼** | æä»¶ | èè´£ | |---|---| | `path.ts` | ç»å®è·¯å¾è§£æï¼`a.b.c`ï¼ä¸æ°å¼è½¬æ¢ | | `binding.ts` | `{{path}}` ææ¬è§£æï¼ç¼ºå¼èµ°åè°ä¸æ¥è䏿¯éé»åæ | | `context.ts` | æ¥åä¸ä¸ææ¨¡å + `PASS`/`FAIL`/`å¾ å¤å®` 常é | | `rule-engine.ts` | **èªå»ºè¯æ³/AST è§£æä¸ç½ååæ±å¼ï¼æ `eval`ãæ `ScriptEngine`** | | `report-evaluator.ts` | é项å¤å®ï¼item è§å â è§æ ¼ä¸ä¸é â æ°æ®èªå¸¦ç»æï¼ä¸æ¥åæ±æ» | | `page.ts` | A3/A4/A5/Letter çº¸å¼ ä¸é¡µè¾¹è·æ¢ç®ï¼mmï¼ | | `render.ts` | GrapesJS é¡¹ç®æ°æ® â HTML ææ¡£ï¼å« `@page` 䏿 ·å¼éæ©å¨æ¹åï¼ | 设计å¨ä¾§çæ¥çº¿ï¼å·¦ä¾§ç»ä»¶é¢æ¿æ¹ç± registry 卿çæï¼`registerBindingTraitType()` 注åç»å®æ§ä»¶ï¼ `loadSchema` è½½å ¥åè°ç¨ `restoreQualityTraits()` éå»ºå±æ§é¢æ¿å段ã ### äºãPhase 2 éªæ¶ï¼æµè§å¨å®æµç»æ ç¨**å端çå®ä¿åç Schema**ï¼æ¨¡æ¿ 3 è稿 `qc_report_template_version.id=6`ï¼é ä¸ä»½ 3 è¡æ£éªæ°æ®ï¼ å¨é¡µé¢å ç´æ¥è°ç¨ `renderReport({ schema, context })`ï¼ | éªè¯é¡¹ | ç»æ | |---|---| | æ£éªé¡¹è¡¨æ ¼æ `inspectionItems` èªå¨çæè¡ | â `tbody` å±å¼ 3 è¡ï¼å¤è§ / é¿åº¦ / ç´å¾ï¼ï¼å ¨ææ¡£ 3 表 / 10 `tr` / 42 `td` / 6 `th` | | PASS/FAIL å¤å® | â æè§æ ¼ä¸ä¸éæ¶ 1 åæ ¼ / 1 ä¸åæ ¼ï¼`result=FAIL`ã`passRate=50.00%` | | å ¨é¨åæ ¼ | â `result=PASS`ã`passRate=100.00%`ãç»è®ºãåæ ¼ã | | çº¸å¼ ä¸é¡µè¾¹è· | â äº§åº `@page { size: 210mm 297mm; margin: 10mm 10mm 10mm 10mm; }` | | ç»å®å ¨é¨è§£æ | â äº§ç©æ æ®ç `{{}}` | | ç»ä»¶ id ä¸åæ³æ¼æ DOM `id` | â æ¹å为 `data-qc-id`ï¼éå¤è¡ä¸ä¼æ id | | æ°æ®ç¼ºå£æ¾å¼æ´é² | â å¦ã第 1 项ãå¤è§ã没æè§æ ¼ä¸ä¸é乿²¡æå¤å®è§åï¼æ æ³å¤å®ãè¿ `errors`ï¼ä¸éé» | | ãç»å®æ°æ®ãæ§ä»¶ï¼text 模å¼ï¼ | â æéãå¤å®ç»è®ºãåå ¥ `{{report.conclusion}}`ï¼æå·¥è¾å ¥ãåæ ¼ãæ¶ä¸æåè½ãä¸ç»å®ã | | ãç»å®æ°æ®ãæ§ä»¶ï¼path 模å¼ï¼ | â æéãæ£éªé¡¹ç®å表ãåå ¥è£¸ `inspectionItems`ï¼ä¸å `{{}}`ï¼ | | è¡åºå· | â `{{index}}` 渲æåº 1ã2⦠| | åç«¯ç±»åæ£æ¥ | â `pnpm typecheck` 卿¬æ¨¡åæ°æ¹æä»¶ä¸ **0 error**ï¼ä»åºå ¶ä½ 316 æ¡ä¸ºæ¢æé®é¢ï¼éæ¬æ¬¡å¼å ¥ï¼ | ### ä¸ãæ¬é¶æ®µå®ä½å¹¶ä¿®å¤çé®é¢ | # | ç°è±¡ | æ ¹å | ä¿®å¤ | |---|---|---|---| | 1 | **ä¿ååé弿¨¡æ¿ï¼å³ä¾§ä¸å¡å±æ§é¢æ¿å ¨ç©º**ï¼ç»ä»¶åªå© Id/Title | traits **ä¸åä¸** GrapesJS é¡¹ç®æ°æ®åºååï¼ä¸æ¯ä¸ªç»ä»¶é½èªå¸¦ `id`/`title` 两个é»è®¤ traitï¼å¯¼è´ãå·²æ trait 就跳è¿é建ãçå¤ç©ºæä¸ºç | `restoreQualityTraits()` æ `data-quality-type` 仿³¨å表**æ æ¡ä»¶é建** traitsã屿§å¼ä»ä¼å 读 `data-qc-*`ï¼ä¸ä¼æç¨æ·æ¹è¿çå¼å²åé»è®¤ | | 2 | ç¹å»å·¦ä¾§ç»ä»¶åï¼æ°ç»ä»¶è¢«**å¡è¿å½åéä¸ç»ä»¶å é¨**ï¼å¹¶é¡¶æå®åæçææ¬ | `BlockView.handleClick` ä¼å `append` è¿ `editor.getSelected()` | æ¯ä¸ª block èªå®ä¹ `onClick`ï¼ç»ä¸è¿½å å° `editor.getWrapper()`ï¼ææ½ä»å¯ç²¾ç¡®è½ç¹ | | 3 | æ´å¼ è¡¨æ ¼æ¸²ææä¸å `div` | é¡¹ç®æ°æ®å¨ `tagName` çäºç»ä»¶ç±»åé»è®¤å¼æ¶ä¸å `tagName`ï¼`table/thead/tbody/row/cell` åªç `type` | `render.ts` å¢å `TYPE_TAGS` æ å°æ `type` æ¨æ ç¾ | | 4 | 渲æäº§ç©ä¸è®¾è®¡å¨æè§ä¸ä¸è´ | GrapesJS `__innerHTML` æ¯ `!cmps.length ? content : cmps.map(...)`ï¼**åç»ä»¶ä¼å äº content** | `renderChildren` åè§åï¼æåç»ä»¶å°±å¿½ç¥ `content` | | 5 | åºå·åæ°¸è¿ç©ºç½ | éå¤å®¹å¨æè¡å·æ³¨å ¥å°ä½ç¨å**é¡¶å±** `index`ï¼ç»ä»¶éå´åçæ¯ `{{item.index}}` | è¡¨è¾¾å¼æ¹ `{{index}}`ï¼å¹¶æãè¡åºå·ãç»è®°è¿ `dataSchema`ï¼ç»å®ä¸æéè½éå° | | 6 | **ãç»è®ºï¼åæ ¼ãä¸ãåæ ¼ç 0.00%ãåæ¶åºç°** | æ¥å级å¤å®åæ `failCount === 0 ? PASS : FAIL`ï¼å ¨é¨å¾ å¤å®æ¶ `failCount=0` è¢«å¤æåæ ¼ | åªæ `passCount === total` æç»åæ ¼ç»è®ºï¼åå¨å¾ å¤å®é¡¹æ¶ä¸ä¸ç»è®ºï¼æ²¡éªè¿ä¸è½è¯´åæ ¼ï¼æ²¡è¯æ®ä¹ä¸è½è¯´ä¸åæ ¼ï¼ | | 7 | ãç»å®æ°æ®ãæ§ä»¶ç»**è·¯å¾å**åæ®µå ä¸ `{{}}`ï¼æ¸²ææå°±æ¾ä¸å°æ°ç» | åè®®éæ²¡åºåãææ¬ç»å®ãä¸ãè·¯å¾ç»å®ã | `QualityFieldSchema` æ°å¢ `bindAs: 'path' \| 'text'`ï¼é»è®¤ `text`ï¼ï¼`QualityTable` çãæ£éªé¡¹æ°æ®æºã声æä¸º `path` | ### åãåç»æ©å±éæ±ï¼2026-09-18 ä¸ç¨æ·ç¡®è®¤å£å¾ï¼ ç¨æ·æåºåç»è¦å **AI + OCR è¯å«æä»¶èªå¨çææ¨¡æ¿**ã已确认两ç¹å£å¾ï¼ 1. **è¾å ¥æä»¶ä¸ç±»é½è¦**ï¼æ«æä»¶/ç §çãçµåç PDFãWord/Excel 模æ¿ã â è¾å ¥ä¾§å¿ é¡»åæ**坿æéé å¨**ï¼ç»ä¸äº§åºãä¸é´ææ¡£ç»æãå交 AIã ï¼PDF èµ° PDFBoxãWord/Excel èµ° POI / FastExcelï¼é¡¹ç®åå·²æï¼æ«æä»¶é OCR + çé¢åæï¼Java æ ç°ææ¹æ¡ï¼å¾åæ¥äºç«¯è¡¨æ ¼è¯å« APIãï¼ 2. **AI 产åºåªä½è稿**ï¼äººå·¥å¨è®¾è®¡å¨ç¡®è®¤åæä¿å为æ£å¼çæ¬ã **è¿ä¸¤ç¹å£å¾å¯¹æ¶æçå½±åï¼éè¦ï¼ï¼** å 为è稿ä¸éè¦ç«å»å¯æ¸²æï¼**ãè¯ä¹ â ç»å¸ JSONãçç¼è¯å¯ä»¥æ¾å¨å端**ï¼ AI åºè¯ä¹è稿 â ç¨æ·å¨è®¾è®¡å¨æå¼ â å端ç¨ç°æç TS ç»ä»¶å®ä¹ï¼`factory.buildContent`ï¼ç¼è¯æç»å¸ â ç¨æ·ç¡®è®¤ä¿åæ¶æè½ `grapes`ã**æå¡ç«¯å æ¤ä¸éè¦ç»ä»¶ç¥è¯**ï¼Java 渲æå¼æç»§ç»åªè®¤ `grapes`ãç»§ç»ç»ä»¶æ å ³ï¼ ä¸åå¨ã12 个ç»ä»¶å¨ TS/Java ååä¸ä»½ãçæ¼ç§»é£é©ã **å¿ é¡»åè¿å¥çº¦ç约æï¼** è¯ä¹å±æ¯**ææçåé**ââåªè¡¨è¾¾ãç¨æ³¨åç»ä»¶æåºæ¥çæå¹³ç»æãï¼ ä¸è¡¨è¾¾ä»»æ HTMLï¼ç¨æ·æå·¥æå ¥çè£¸è¡¨æ ¼ãèªå®ä¹ div ä¸å¨å ¶å ï¼ãAI åªè½ç¨å·²æ³¨åçç»ä»¶ä½ä¸ºç§¯æ¨ã è¿æ¯æææ¶çªçè¾¹çï¼ä¸æ¯å®ç°çæ¼ã **è¦åçäºï¼** - æ `ReportTemplateSchema.components` ä»ã`collectSchema` 顺æå¡«ã渲æä¸è¯»ãç**æ»æ°æ®è¡¥ææ£å¼å¥çº¦**ï¼ è¿ `dataSources` / `bindings` ä¸èµ·å®ä¹æ¸ æ¥ï¼å¹¶å»ç»è¿ `schemaVersion`ã - 该å¥çº¦åæ¶ä¹æ¯**ç» LLM çç§¯æ¨æ¸ å**ï¼è®¾è®¡å¨ç»ä»¶é¢æ¿ãå端ç¼è¯ãAI æç¤ºä¸å¤å¤ç¨åä¸ä»½æ¥æºï¼ã ### äºãçç»ä¸ä¸æ¥çæé 1. **Phase 3 æä¸ä¸ªå¾ ææ¿çæ¶æåå**ï¼æ¸²æå¼æç®åæ¯ **TS**ï¼è·å¨æµè§å¨éãå端 Playwright æå° PDF éè¦ HTMLââ (A) å端渲æ HTML â POST ç»å端ï¼å端å **Sanitization** åç¨ Playwright/Chromium æå°ï¼ (B) ææ¸²æå¼æç§»æ¤æ Javaï¼æå¡ç«¯å ¨é¾è·¯èªæã æ¹æ¡è®¾è®¡ææ¡£çé£é©æ¸ åè¦æ±ãæ¨¡æ¿ HTML å±ç¨æ·å¯ç¼è¾å 容 â **å¿ é¡»æå¡ç«¯ Sanitization**ãï¼ä¸¤æ¡è·¯é½è½æ»¡è¶³ï¼ä½æå ¥å·®å«å¾å¤§ã 2. **æ¨¡æ¿ 3ï¼QC_IQC_DEMO_COPYï¼çèç¨¿çæ¬ `id=6` æ¯èè°éçæ°æ®**ï¼é颿¯æµè¯ç»ä»¶ï¼ä¸å«ä¸å¤æ©æãç»ä»¶è¢«å¡è¿æ é¢ãçç迹ï¼ï¼å»ºè®®ç´æ¥å é¤ã 3. **MySQL MCP å½åæåæ²çå®ä¾**ï¼`product-inventory-management-jcck`ãMySQL 8.0.32ï¼ï¼**ä¸å«æ¬é¡¹ç®ä»»ä½åº**ã æ¬é¶æ®µæ ¸å¯¹ç宿°æ®èµ°çæ¯é¡µé¢å `requestClient` / å端æ¥å£ï¼ä¸æ¯ MCPã 4. å端**æ æµè¯æ¡æ¶**ï¼æ¬é¶æ®µçéªè¯æ¹å¼æ¯ãæµè§å¨å `await import('#/components/quality/index.ts')` ç´æ¥è°çº¯å½æ° + Playwright æä½çå®è®¾è®¡å¨ãã Phase 5 è¥è¦è¡¥èªå¨åæµè¯ï¼éè¦å å¼å ¥æµè¯è¿è¡å¨ã --- ## Phase 3 宿½è®°å½ï¼2026-09-18 ç»ï¼ > ç®æ ï¼è®©**æå¡ç«¯èªæåºä»¶é¾è·¯**ââ`Schema + ä¸ä¸æ â HTML â PDF`ï¼ > ä¸ä¾èµæµè§å¨ãä¸ä¾èµå端å¨è·ï¼æ¹éåºä»¶ä¸å®æ¶ä»»å¡æè½èµ°åä¸ä¸ªå ¥å£ã ### ä¸ãæ¶æååçç»è®ºï¼é Bï¼æå¼æç§»æ¤æ Javaï¼ ç¬¬ä¸ä»½è¿åº¦éççé®é¢ï¼Aï¼å端渲æ HTML åä¼ å端æå° / Bï¼ç§»æ¤æ Javaï¼å·²ææ¿èµ° **B**ã | 维度 | Aï¼å端渲æ + åä¼ ï¼ | Bï¼Java å¼æï¼ | |---|---|---| | æ§è½ | åºä»¶è¦ä¹å ç¨æ·æµè§å¨ï¼è¦ä¹èµ·æ 头æµè§å¨ï¼æ¹éåºä»¶è¦æéçæµè§å¨å®ä¾ | 纯å å计ç®ï¼æ æµè§å¨å¼éï¼PDF é¶æ®µæéè¦ Chromiumï¼ä¸å¯å¤ç¨å®ä¾ | | æ©å±æ§ | åç«¯ä¸æ¹æ¸²æé»è¾ï¼å岿¥åéåºç»æå°±å | 弿æ ç¶æãç»ä»¶æ å ³ï¼æ¥å ¶å®æ¨¡å / 宿¶ä»»å¡ / æ¶æ¯æ¶è´¹é½ä¸æ¹ä»£ç | | å¯ç»´æ¤æ§ | 渲æé»è¾æ£å¨æµè§å¨ç«¯ï¼ææ¥è¦å¼ DevTools | æå¡ç«¯å¯æç¹ãå¯åæµã坿æ¥å¿ï¼åºä»¶é®é¢ç´æ¥å®ä½ | | å¯ç¨æ§ | å端ä¸å¨çº¿å°±åºä¸äºä»¶ | ä¸å端解è¦ï¼æ¥å£/ä»»å¡é½è½åºä»¶ | **B çå¯ä¸çå®é£é©æ¯ã两å¥å¼ææ¼ç§»ã**ï¼é¢è§ç¨ TSãåºä»¶ç¨ Javaï¼ä¸ä¸è´å°±æ¯ãé¢è§ä¸åºä»¶ä¸ç¬¦ãï¼ã æä»¥æ¬é¶æ®µä¸æ¯å纯移æ¤ï¼èæ¯**è¿å¸¦åäºä¸å¥å¯¹ææºå¶**ââè§ç¬¬ä¸èï¼è¿æ¯ B è½æç«çåæï¼ä¸æ¯éå 项ã ### äºã交ä»ç© **Java 弿ï¼`yudao-module-qcreport/src/main/java/.../engine/`ï¼24 个æä»¶ï¼** | å | æä»¶ | èè´£ | |---|---|---| | `engine/` | `QualityReportEngine` | é¨é¢ï¼`render` / `rulesOf` / `pageOf` / `grapesOf` / `validateRules` | | | `Paths` `Numbers` `JsValues` `Bindings` | è·¯å¾åå¼ã`String(value)` è¯ä¹ã`{{path}}` ç»å®è§£æ | | | `QualityResult` | PASS/FAIL + `å¾ å¤å®` | | | `PageSetting` `PageMargin` `ResolvedPage` `PageSizes` | çº¸å¼ /é¡µè¾¹è·æ¢ç®ä¸ `@page` è¾åº | | `engine/context/` | `ReportContext` `ReportFields` `InspectionItem` | æ¥åä¸ä¸ææ¨¡åï¼å段åä¸ TS ä¾§ä¸è´ï¼ | | `engine/rule/` | `RuleEngine` `RuleParser` `RuleEvaluator` `RuleNode` `RuleToken` `TokenType` | **èªå»ºè¯æ³/AST + ç½åå彿°**ï¼æ `eval`ãæ `ScriptEngine` | | | `QualityRuleDefinition` `RuleScope` | è§åå®ä¹ä¸ä½ç¨å | | | `RuleSyntaxException` `RuleRuntimeException` | è¯æ³é误ï¼å¸¦ä½ç½®ï¼ä¸æ±å¼é误 | | `engine/report/` | `ReportEvaluator` `EvaluationOutcome` | é项å¤å®ä¸æ¥åæ±æ» | | `engine/render/` | `HtmlRenderer` `RenderNode` `RenderOutcome` | Schema â HTML ææ¡£ï¼å« `@page`ãæ ·å¼éæ©å¨æ¹åï¼ | 弿**ä¸ä¾èµ Springãä¸è½åºãä¸åæé**ââåªåãSchema + ä¸ä¸æãå HTMLï¼ è¿æ ·æ¹éçæã宿¶ä»»å¡ãå ¶å®æ¨¡åè°ç¨é½èµ°åä¸ä¸ªå ¥å£ã **å¯¹ææºå¶ï¼æ¬é¶æ®µçæ ¸å¿äº§åºä¹ä¸ï¼** | ä½ç½® | ä½ç¨ | |---|---| | `src/test/resources/qcreport/schema.json` `context.json` | åä¸ä»½è¾å ¥ fixture | | `src/test/resources/qcreport/expected-html.html` `expected-rules.json` | **åç«¯å¼æ**è·åºæ¥çææäº§ç©ï¼å»ç»ï¼ | | `mom-pro2-before/.qc-conformance/render-ts.ts` | ç¨åç«¯å¼æéç®ææäº§ç©çèæ¬ï¼æ¹å¨ä»»ä¸ä¾§å¼æåéè·ï¼ | | `FrontendConformanceTest` | åç«¯å¼æè·åä¸ fixtureï¼é忝坹 HTML / ç¼ºå£æ¸ å / 41 æ¡è¡¨è¾¾å¼ | | `ReportEvaluatorTest` | å¤å®è¯ä¹ä¸æ±å¼å¥å£®æ§ï¼19 个ç¨ä¾ï¼ | éç®ææäº§ç©çå½ä»¤ï¼å¨ `mom-pro2-before` ä¸ï¼ï¼ ```powershell ./node_modules/.bin/tsx .qc-conformance/render-ts.ts ../yudao-module-qcreport/src/test/resources/qcreport ../yudao-module-qcreport/src/test/resources/qcreport ``` ### ä¸ã对æè¦çäºä»ä¹ `FrontendConformanceTest` ä¸é¡¹æè¨ï¼**å ¨ç»¿**ï¼ | ç¨ä¾ | æ¯å¯¹å 容 | |---|---| | 渲æäº§ç©éåä¸è´ | HTML å ¨æï¼å« `@page`ã`data-qc-id` æ¹åãéå¤è¡å±å¼ã转ä¹ï¼ï¼ æ°æ®ç¼ºå£æ¸ å | | å¤å®ç»æä¸è´ | ç»è®º OK/ä¸åæ ¼ãtotal/passCount/failCount/passRateãé项 PASS/FAIL | | è§åæ±å¼ä¸è´ | **41 æ¡è¡¨è¾¾å¼**çç±»å䏿æ¬ï¼ææ¥éææ¡éåç¸å | 41 æ¡è¡¨è¾¾å¼è¦çï¼å符串/æ°å¼/å¸å°æ¯è¾ã`IN` / `NOT IN` / `BETWEEN` / `NOT BETWEEN`ã `AND` / `OR` / `!` / ä¸å è´å·ãç®æ¯ä¸é¤é¶ãææ¬æ¼æ¥ã`NULL` æ¯è¾ã 12 个ç½åå彿°çæ£å¸¸å¼ï¼å« `STDDEV` / `CP` / `CPK` çæµ®ç¹ææ¬ï¼ã 缺å / ç±»åä¸ç¬¦ / æªç¥å½æ° / è¯æ³æ®ç¼ºçæ¥éææ¡ã以å**çå¼å£å¾**ã ç§»æ¤è¿ç¨ä¸çæ£ä¼å¬äººçæ¯ **JS ä¸ Java çåå¼/æ ¼å¼åå·®å¼**ï¼éæ¡å¤çè¿ï¼ | é·é± | å¤ç | |---|---| | `String(60)` â `"60"`ï¼Java `String.valueOf(60.0D)` â `"60.0"` | `Numbers.toString` ææ´æ°å¼æµ®ç¹æ°æ¶ææ´æ° | | æ°ç» `String()`ï¼JS `"1,2"`ï¼Java `"[1, 2]"` | `JsValues.asText` 对 List éå ç´ æ¼æ¥ | | ç§å¦è®¡æ°æ³éå¼ä¸åï¼JS <-6 æ â¥21ï¼Java <-3 æ â¥7ï¼ | ç»ä¸èµ° `Numbers.toString` | | `toUpperCase()` å locale å½±åï¼åè³å ¶è¯ i â Ä°ï¼ | å¤å®åå¼å½ä¸åç¨ `Locale.ROOT` | | `Map.copyOf` ä¸ä¿è¯éå顺åºï¼ä¼è®©æ¥ééçå¯ç¨å½æ°éæºæ | ç¨ `Collections.unmodifiableMap` ä¿ä½ LinkedHashMap é¡ºåº | | `Map.of(...).get(null)` æ NPE | ç±»åæ å°å å¤ç©ºåæ¥è¡¨ | | **çå¼å£å¾**ï¼è½å½æ°åççåå¼ä¸å¾æ `!== 0` å¤ç | æä»¥ `"0"` æ¯**åå¼**ï¼èåç JS `Boolean("0")` æ¯çå¼ï¼ç©ºæ°ç»ä¹æ¯åå¼ãåç«¯ç §æ¬åç«¯ï¼æ¹å¨åå¿ é¡»å æ¹å端 | ### åãè¿è¶ åç«¯å¼æçä¸å¤å®å ¨å åº `HtmlRenderer` å¨å±æ§è¾åºåå äºä¸éé¸ï¼**è¿æ¯ææå离 TS ç**ï¼ - **äºä»¶å±æ§**ï¼`on*`ï¼ä¸å¾ä¸è¾åºââæ¨¡æ¿éå¡ `onclick` ä¸è¯¥åºç°å¨åºä»¶äº§ç©éï¼ - **å±é©åè®® URL**ï¼`javascript:` / `vbscript:` / `data:`ï¼`data:image/` é¤å¤ï¼ä¸å¾ä¸è¾åºã 被æ¦ä¸ç屿§**åè¿é®é¢æ¸ å**ï¼å¦ã模æ¿å±æ§ãonclickãæ¯äºä»¶å±æ§ï¼æ¥å产ç©ä¸è¾åºï¼å·²å¿½ç¥ãï¼ï¼ 让模æ¿ä½è çå¾è§ï¼è䏿¯ææå°ä¸ªå±æ§ãTS çæ²¡æè¿éé¸ï¼æä»¥è¿æ¡**ä¸å¨å¯¹æèå´å **ï¼é `ReportEvaluatorTest` ä¹å¤çåæµå®ã å¦é说æï¼**å ¥ç« HTML ç Sanitization ä»å½åºä»¶æ¥å£è´è´£**ï¼Phase 3 第 4 æ¥ï¼ï¼ 渲æå¼æçè¿éé¸åªç®¡ãç»ä»¶å±æ§ â 产ç©å±æ§ãè¿ä¸å±ï¼ä¸¤è ä¸éå ã ### äºãå¤å®è¯ä¹ï¼ä¸ä¸ªçå®ç¼ºé·è¢«æµè¯éä½äº Phase 2 ä¿®è¿ç缺é·ï¼ãç»è®ºåæ ¼ + åæ ¼ç 0%ãèªç¸çç¾ï¼ç°å¨æåå½ç¨ä¾å®çï¼ ä¸é¡¹éä¸¤é¡¹åæ ¼ãä¸é¡¹å¾ å¤å® â **ä¸ç»åæ ¼ç»è®º**ï¼ç»è®ºä¸ºãå¾ å¤å®ãï¼ï¼åæ ¼çç §ç® 66.67%ã `ReportEvaluatorTest` 妿è¦çï¼åä¾§è§æ ¼ãè§åä¼å äºä¸ä¸éãæ°æ®èªå¸¦ç»æå åºã è§åè·ä¸éæ¶ä¸ç»ç»è®ºãåç¨è§å忽ç¥ãæ¥å级è§åä¼å äºæ±æ»ã空æ¥åä¸ç»ç»è®ºã **å¤å®ä¸ä¿®æ¹ä¼ å ¥ä¸ä¸æ**ã彿°ç¼ºåæ¥å¯è¯»é误ï¼ä¸æ¯ `IndexOutOfBoundsException` ç©¿éæãç³»ç»å¼å¸¸ãï¼ã ### å ãéªæ¶ ``` Tests run: 29, Failures: 0, Errors: 0, Skipped: 0 ``` - `FrontendConformanceTest`ï¼3/3 éè¿ï¼HTML å ¨æéåä¸è´ã41 æ¡è¡¨è¾¾å¼ä¸è´ï¼ - `ReportEvaluatorTest`ï¼23/23 éè¿ï¼å¤å®è¯ä¹ + è§åæ±å¼å¥å£®æ§ + ä¿ååæ ¡éªï¼ - `ReportTemplateSchemaCompatibilityTest`ï¼3/3 éè¿ï¼èµ°çäº§åæ¬¾ `Jackson3TypeHandler` è¯»æ§ JSONï¼ - ç¼è¯ï¼`mvn compile -pl yudao-module-qcreport -am -q -o` éè¿ è¿è¡å½ä»¤ï¼ ```powershell mvn test -pl yudao-module-qcreport -am -q -o -Dtest='FrontendConformanceTest,ReportEvaluatorTest,ReportTemplateSchemaCompatibilityTest' -Dsurefire.failIfNoSpecifiedTests=false ``` ### ä¸ãè¿è¡ä¸ / æªå®æé¡¹ | # | äºé¡¹ | ç¶æ | |---|---|---| | 1 | å¼æç§»æ¤ï¼æ¸²æ + è§å + å¤å®ï¼ | â 宿并éè¿å¯¹æ | | 2 | å»ç» Schema è¯ä¹å±å¥çº¦ï¼å¥çº¦ 1.1ï¼ | â 宿ï¼è§ç¬¬ä¹è | | 2b | ä¿®å¤è®¾è®¡å¨ä¿åæ¸ ç©ºå¤å®è§åï¼æ°æ®ä¸¢å¤±ï¼ | â 宿ï¼è§ç¬¬ä¹è | | 2c | è§åç¼è¾çé¢ + ä¿ååæ ¡éªé¸å£ | â 宿并æµè§å¨å®æµï¼è§ç¬¬åè | | 3 | **å¼å ¥ Playwright + ä¸è½½ Chromium** | ⬠æªå¼å§ï¼**éè¦ç¨æ·éå¯å端** | | 4 | åºä»¶æ¥å£ + `data_snapshot` + HTML/PDF 彿¡£ï¼å«å ¥ç« HTML Sanitizationï¼ | ⬠æªå¼å§ | | 5 | Phase 4ï¼å®ä¾/çæ¬/æé/æ¥å¿ï¼ãPhase 5ï¼èªå¨å + E2Eï¼ | ⬠æªå¼å§ | | 6 | AI + OCR è¯å«æä»¶çææ¨¡æ¿è稿 | ⬠æªå¼å§ï¼å£å¾å·²ç¡®è®¤ï¼è§ Phase 2 第åèï¼ | ### ä¹ãSchema å¥çº¦å»ç» 1.1 + è§åæ°æ®ä¸¢å¤±ä¿®å¤ï¼2026-09-18 ç»ï¼ 第 2 æ¥åæ¬ä»¥ä¸ºåªæ¯"è¡¥åæ®µ"ï¼å®é ææ¥åº**ä¸ä¸ªçå®ç¼ºé·**ï¼ä¸å¹¶å¤çäºã #### 9.1 缺é·ä¸ï¼ä¿å䏿¬¡è®¾è®¡å¨å°±æå·²æå¤å®è§åæ¹æï¼é黿°æ®ä¸¢å¤±ï¼ `use-designer.ts` ç `collectSchema` é硬ç¼ç `rules: []`ï¼èå端 `updateVersion` æ¯ `BeanUtils.toBean(updateReqVO, DO.class)` + `updateById`ââ**æ´ä»½ Schema æ´ä½è¦ç**ã 两è å å çç»æï¼åªè¦æå¼æ¨¡æ¿ç¹ä¸æ¬¡ä¿åï¼åºéå·²æçè§åå°±è¢«æ¸ ç©ºï¼ è䏿²¡æä»»ä½æ¥éï¼ç¨æ·ä¸ä¼ç¥éèªå·±å¼ä¸¢äºä»ä¹ã ä¿®æ³æ¯è®©è®¾è®¡å¨æ"èªå·±ä¸è´è´£ç¼è¾çåæ®µ"åæ ·å¸¦åï¼`use-designer.ts`ï¼ï¼ ```ts // è½½å ¥æ¶çåï¼ä¿åæ¶å¸¦åï¼åå°ç©ºç½çæ¬æ¶è·çæ¸ ç©º let retainedSchema: Pick<QcReportVersionApi.ReportTemplateSchema, 'rules'> = {}; ``` `loadSchema` é `retainedSchema = schema?.rules ? { rules: schema.rules } : {}`ï¼ `collectSchema` æ«å°¾ `...retained`ã`rules` 为空æ°ç»æ¶æ¯**çå¼**ï¼æä»¥ "åå°ä¸ä¸ª rules:[] ççæ¬"伿£ç¡®å¸¦ä¸ç©ºæ°ç»è䏿¯æ®çä¸ä¸ä¸ªçæ¬çè§åï¼ `schema` 为 undefinedï¼æ²¡åè¿å 容çèç¨¿ï¼æ¶æ¸ 空çåï¼ä¹ä¸ä¼ä¸²å³ã **å·²ç¨æµè§å¨å®æµ**ï¼å两æ¡è§åå°æ¨¡æ¿ 3 çèç¨¿çæ¬ â éè½½è®¾è®¡å¨ â ç¹ãä¿åãâ åè¯»çæ¬ Schemaï¼`rules` 两æ¡é½å¨ãéªè¯å®å·²æè¯¥è稿è¿åï¼`rules: []`ã`schemaVersion: "1.0"`ï¼ã #### 9.2 缺é·äºï¼è§åæ ¡éªå好äºä½æ²¡æä»»ä½è°ç¨æ¹ `QualityReportEngine.validateRules` åªæå®ä¹æ²¡æè°ç¨ï¼`validateSchema` åªæ¥äº `schemaVersion` é空ãè§ååéè¦å°åºä»¶æ¶æç¸ï¼ç¨æ·çå°çæ¯"æ¥ååºä¸æ¥"ï¼ å´ä¸ç¥éæ¯æ¨¡æ¿éåªæ¡è§åååçãç°å·²å¨ä¿åè·¯å¾ä¸æ¥éï¼éæ¡ååºæ¯åªæ¡è§åãéå¨åªï¼ ```java throw new ServiceException(TEMPLATE_VERSION_RULE_INVALID.getCode(), TEMPLATE_VERSION_RULE_INVALID.getMsg() + "ï¼" + String.join("ï¼", ruleErrors)); ``` é误ç `1_070_101_006`ï¼`模æ¿éçå¤å®è§åä¸åæ³ï¼è¯·ä¿®æ¹åéè¯`ã **ä¸ä¸ªææçä¸çä¾å¤**ï¼`validateRules` **è·³è¿ç©ºè¡¨è¾¾å¼çè§å**ï¼ä¸å½ä¿åé误ã çç±å卿¹æ³ javadoc éââè¿ç±»é¡¹åªå¯è½æ¯åå²èæ°æ®ï¼è**è§åç¼è¾çé¢è¿ä¸åå¨**ï¼ æ¥äºéç¨æ·ä¹æ å¤å¯æ¹ï¼åªè½å¡å¨"ä¿åä¸äº"ï¼çäºæ"䏿¡åæ°æ®"å级æ"æ´ä»½æ¨¡æ¿ä¸è½ä¿å"ã æ§è¡æä»ä¼ç §å¸¸æå®æ¥åºæ¥ï¼`第 N 项ãXãçè§åãYãæ æ³æ§è¡ï¼è§å表达å¼ä¸ºç©º`ï¼å¹¶æ ä¸ºå¾ å¤å®ï¼ 䏿¯éé»åæã**è§åç¼è¾çé¢ååºæ¥ä¹åï¼è¿éåºå½æ¹ææç¡®æ¥éã** > **å·²å ç°**ï¼è§åç¼è¾çé¢ååºæ¥ä¹åï¼è§ç¬¬åè 10.1ï¼ï¼å端è¿éæä¸é¢è¿å¥è¯æ¹æäºæç¡®æ¥éï¼ > åæµ `ReportEvaluatorTest.blankExpressionRuleBlocksSave` è·çæ¹åæ"读å¾åºæ¥ãä½é»æä¿å"ã #### 9.3 缺é·ä¸ï¼`dataSources` / `bindings` / `styles` æ¯æ»æ°æ® ä¸ä¸ªåæ®µèª 1.0 èµ·å°±æ¯ç©ºæ°ç»ï¼æ¢æ 人åå ¥ä¹æ 人读åï¼ç±»åä¸è¿æ¯æ å¥çº¦ç `Record<string, unknown>[]`ãå·²ä»åå端ä¸å¹¶å é¤ï¼`schemaVersion` åå° `1.1`ã å æ»åæ®µåæ¬æä¸º `[]`ï¼**1.1 æ éæ°æ®è¿ç§»**ã åæ¶æ `components` ç± `Map` æ¹ä¸ºå¼ºç±»å `QualityComponentNode`ï¼`id`/`index`/`qualityType`/`attributes`ï¼ï¼ å¹¶å¨ `ReportTemplateSchema` javadoc éåæ**è¯ä¹å±æ¯ææçåé**ï¼ åªè¡¨è¾¾"ç¨å·²æ³¨åè´¨éç»ä»¶æåºæ¥çæå¹³ç»æ"ï¼**ä¸è¡¨è¾¾ä»»æ HTML**ã è¿æ¡è¾¹çæ¯ç» AI çææ¨¡æ¿ç¨ç约æââæäºå®æè½è¦æ±"åªè½ç¨å·²æ³¨åç»ä»¶å½ç§¯æ¨"ï¼ è䏿¯å䏿®µè°ä¹ä¸æ¢æ¸²æç HTMLãä»»ä½"ç»è¯ä¹å±å 个ä¸è½ customHtml åæ®µ"çæ¹å¨é½å¨æè¿æ¡è¾¹çï¼éè¦å 讨论ã #### 9.4 åéæ°æ®å ¼å®¹ `ReportTemplateSchemaCompatibilityTest` èµ°**çäº§åæ¬¾è¯»åè·¯å¾** ï¼MyBatis-Plus `Jackson3TypeHandler` + `QcReportTemplateVersionDO.schema` åæ®µï¼ï¼ 䏿¯æµè¯èªå·± new ä¸ä¸ª ObjectMapperââå 为è¦é²çå°±æ¯"ååºåå读å"è¿æ¡é¾è·¯ã 宿µæ¨¡æ¿ 3 çåºéè®°å½ç¡®å®è¿å¸¦ç `bindings`/`dataSources`/`styles` ä¸ä¸ªé®ï¼ç¨ä¾ä»¥å®ä¸ºèæ¬ã ä¸å¥æ¾æ¸ å å¾å人误解ï¼**è¿ä¸ªç¨ä¾ä¸è´è´£è¯æ `@JsonIgnoreProperties` çå¿ è¦æ§**ââ 宿µæé£è¡æ³¨è§£å»ææ¬ç¨ä¾ç §æ ·éè¿ï¼Jackson 3 é»è®¤å°±å¿½ç¥æªç¥å±æ§ï¼ã æ³¨è§£çæ£é²çæ¯å¦å¤ä¸¤æ¡ï¼Spring ååºåå请æ±ä½ç¨ç ObjectMapperï¼é ç½®ä¸åæ¬æ¨¡åæ§å¶ï¼ï¼ 以åå°æ¥æäººç»ç±»åå¤ç卿¢ä¸ä¸ªæ´ä¸¥æ ¼ç Mapperãæµè¯ç±» javadoc éå·²æ¾å¼å£°æå®**ä¸**æ¯é£æ¡æ³¨è§£çå®å«ã ### å «ãçç»ä¸ä¸æ¥çæé 1. **æ¹å¨ä»»ä¸ä¾§å¼æåå¿ é¡»éè·å¯¹æ**ï¼å è· `render-ts.ts` éç®ææäº§ç©ï¼åè· `FrontendConformanceTest`ã ææäº§ç©æ¯**å»ç»**çï¼ä¸éç®å°±åªæ¯ãå端è·ä¸ä»½æ§äº§ç©æ¯ãï¼æ¯è¿äºä¹ä¸ä»£è¡¨ä¸¤è¾¹ä¸è´ã 2. **第 3 æ¥å¿ é¡»ç±ç¨æ·æä½**ï¼Playwright ä¾èµè¦å è¿ `yudao-dependencies` å¹¶ä¸è½½ Chromiumï¼éè¦éå¯å端æçæã 3. **`.qc-conformance/` å¨ `mom-pro2-before` ä¸**ï¼è¯¥ç®å½æ´ä¸ªä¸è¢«æ¬ä»åºè·è¸ªï¼ï¼æ¯ä¸´æ¶å¯¹æå·¥å ·ï¼ä¸è¦å½ä½å端æ£å¼ä»£ç ã 4. **第ä¹èç两项æ¹å¨è¦å端é坿çæ**ï¼è§åä¿åæ ¡éªï¼`validateRules` æ¥çº¿ï¼ä¸å¥çº¦ 1.1 ï¼å é¤ä¸ä¸ªæ»å段ï¼é½å¨å端ã**éå¯å**æµè§å¨éªè¯å°çåªæå端é£ä¸åï¼è§åçåï¼ï¼ å端é£ä¸åç®åé åæµä¿è¯ãéå¯åå¯ç´æ¥çæ¨¡æ¿ 3 çè稿记å½ï¼ä¸ä¸ªæ»å段åºä» `schema` 鿶失ã 5. æ¨¡æ¿ 3 èç¨¿çæ¬ `qc_report_template_version.id=6` æ¯èè°éçæ°æ®ï¼æ¬è½®éªè¯å·²ç¨å°å¹¶**è¿å** ï¼`rules: []`ã`schemaVersion: "1.0"`ï¼ï¼å»ºè®®åç»è¿æ¨¡æ¿ä¸èµ·æ¸ æï¼é¿å 忣弿°æ®ççæ¬å·æ··æ·ã ## åãè§åç¼è¾çé¢ + ä¿ååæ ¡éªé¸å£ï¼2026-09-18 ç»ï¼ ç¨æ·éå®çé¡ºåºæ¯"**å è§å UIï¼ååºä»¶æ¥å£**"ï¼å åä¸éè¦éå¯å端çé¨å并卿µè§å¨éªè¯ï¼ åºä»¶æ¥å£åå®ååç»ä¸éå¯ä¸æ¬¡ãæ¬èæ¯ååæ®µã ### 10.1 交ä»ç© | æä»¶ | æ¹å¨ | |---|---| | `designer/modules/rule-editor.vue` | **æ°å¢**ï¼çº¦ 170 è¡ï¼ãæ½å±å¼è§åç¼è¾å¨ï¼æ°å¢/å é¤è§åãæ¹åç§°ãåä½ç¨åãå¯åãç¼è¾è¡¨è¾¾å¼ï¼éæ¡å èæ¥é | | `designer/use-designer.ts` | `retainedSchema` é£å¥"çåå带å"æ¢æä¸çç¶æ `rules` refï¼é `loadSchema` è½½å ¥ãé `collectSchema` äº§åº | | `designer/index.vue` | é¡¶æ å ãå¤å®è§åï¼Nï¼ãæé® + 红æ ãN æ¡è§åå¾ ä¿®æ£ãï¼`handleSave` / `handlePublish` åå `blockOnInvalidRules()` é¸å£ | | `components/quality/engine/report-evaluator.ts` | `validateRule` è¡¥ä¸**彿°ç½åå**æ ¡éªï¼åå åªæ¥è¯æ³ï¼ï¼å ¥åæ¹ç¨æ°ç±»å `RuleValidationInput` | | `components/quality/engine/rule-engine.ts` | æ°å¢ `listAllFunctionNames()`ï¼çé¢æç¤ºä¸å¼æç½ååå ±ç¨ä¸ä»½æ¥æº | | `components/quality/index.ts` | å¯¼åº `listAllFunctionNames` | | `engine/QualityReportEngine.java` | 空表达å¼è§åç±"æ¾è¡"æ¹ä¸º**æç¡®æ¥é**ï¼å ç°ç¬¬ä¹è 9.2 ççä¾å¤ï¼ | | `ReportEvaluatorTest.java` | 对åºç¨ä¾æ¹å为"读å¾åºæ¥ãä½é»æä¿å" | **为ä»ä¹ `listAllFunctionNames` è¦ä»å¼æé导åºè䏿¯å¨ UI é妿ä¸ä»½**ï¼ç½å忝弿çä¸é¨åï¼ æä¸ä»½å°±ä¼å¨å 彿°æ¶æ¼æ¹ï¼ç¨æ·çå°çå¯ç¨å½æ°å表ä¸å¼æå®é æ¯æçææå¯¹ä¸ä¸ã **为ä»ä¹ `validateRule` ä¸åå¤ç¨ `QualityRuleDefinition`**ï¼é£ä¸ªç±»åç `expression` æ¯å¿ å¡«ï¼ è设计å¨éæ£å¨ç¼è¾çè§åãå岿°æ®éçè§å项é½å¯è½æ¯ç©ºçï¼æ ¡éªå½æ°æ°æ°è¦æ¥ä½è¿ç§"è¿æ²¡åå®"çå ¥åï¼ æä»¥åç¬å®ä¹äº `RuleValidationInput`ï¼ä¸ä¸ªåæ®µå ¨å¯éï¼ã **å端为ä½ä¹è¦æ¦ç½åå**ï¼åªè®©å端ç `validateRules` æ¦ï¼ç¨æ·ç¹ä¿ååä¼å¨çé¢ä¸æ¶å°å端æ¥éï¼ ä½é£æ¶äººå·²ç»ç¦»å¼æ½å±ï¼è¿å¾èªå·±ç¿»æ¯åªæ¡è§åååçã设计å¨éç´æ¥æ¦ï¼çº¢åå°±æ å¨åºé®é¢çé£ä¸æ¡ä¸ï¼ åæ¶ææ½å±èªå¨æå¼ã ### 10.2 æ¬è½®åå®ä½å°ä¸ä¸ªçå®ç¼ºé·ï¼æ¹çº¸å¼ è®¾ç½®ä»æ¥æ²¡ä¿åæåè¿ **ç°è±¡**ï¼æ¯æ¬¡ç¹ãä¿åãæ§å¶å°é½æä¸ä¸ªæªå¤çå¼å¸¸ï¼`Unhandled error during execution of component event handler` æå¨ä¿åæé®ä¸ï¼ï¼ä½ä¿åæåçæç¤ºç §å¸¸å¼¹åºæ¥ï¼çº¸å¼ è®¾ç½®å´æ°¸è¿ä¸ä¼çæã **æ ¹å **ï¼`handleSave` æ«å°¾åæ¥çº¸å¼ 设置æ¶åªä¼ äºä¸ä¸ªå段ââ ```ts await updateTemplate({ id, pageSize, orientation }); // â æ§åæ³ ``` è `qc-report/template/update` æ¯**æ´ä½æ´æ°**ï¼`QcReportTemplateSaveReqVO` ä¸ `templateCode` / `templateName` / `pageSize` / `orientation` 齿¯ `@NotEmpty`ã æ¼ä¼ 模æ¿ç¼ç ï¼åç«¯ç´æ¥ 400ï¼`请æ±åæ°ä¸æ£ç¡®:模æ¿ç¼ç ä¸è½ä¸ºç©º`ã **为ä»ä¹ä»¥å没被åç°**ï¼è¿ä¸ª 400 åçå¨ `message.success('å·²ä¿åå° v1.0')` **ä¹å**ï¼ çæ¬æ¬èº«å·²ç»å好äºï¼æä»¥è¡¨é¢ä¸ç"ä¿åæ¯æåç"ï¼åªæè·¨çæ¬æ ¸å¯¹æ¨¡æ¿è¡¨æä¼åç°çº¸å¼ 设置没è½åºã åå 为设计å¨è¯»çº¸å¼ æ¯ä»**çæ¬ Schema** ç `page` 读çï¼`loadSchema`ï¼ï¼ 䏿¯ä»æ¨¡æ¿è¡¨è¯»çï¼éæ°è¿è®¾è®¡å¨çå°ç仿¯æ£ç¡®çº¸å¼ ï¼æè¿ä¸ªé®é¢å½»åºçä½äºã **ä¿®æ³**ï¼ææ¨¡æ¿åæå段ä¸èµ·å¸¦ä¸ï¼åªè¦ççº¸å¼ ä¸¤é¡¹ï¼ä¸å表页 `form.vue` æ´ä½æ´æ°çå£å¾ä¸è´ï¼ï¼ ```ts await updateTemplate({ ...template.value, id, pageSize: pageSetting.value.size, orientation: pageSetting.value.orientation, }); ``` ### 10.3 æµè§å¨å®æµè¯æ® ç¯å¢ï¼dev server `http://192.168.0.10:5666`ï¼æ¨¡æ¿ **5 / åºè´§æ£éªæ¥å模æ¿**ï¼QC_OQC_UIï¼ã | # | éªè¯é¡¹ | ç»æ | |---|---|---| | 1 | æ½å±æå¼ï¼ç©ºæä¸å¸®å©åï¼ä½ç¨å说æãå¯ç¨å段ãå¯ç¨å½æ°ï¼æ£å¸¸ | â | | 2 | æ°å¢è§å â ç«å»å èæ¥éãè§åãè§å 1ãç表达å¼ä¸ºç©ºã+ é¡¶é¨ãæ 1 æ¡è§åè¿æ²¡å宿´ã | â | | 3 | å¡«å ¥åæ³è¡¨è¾¾å¼åæ¥éè¡æ¶å¤± | â | | 4 | åå°ãæ¥å级ãï¼è¡¨è¾¾å¼å ä½æç¤ºåæ¥æ¢æ `PASS_RATE(...) >= 60` | â | | 5 | å¡«å ¥ `FOO(...)` â å èæ¥éãè°ç¨äºä¸æ¯æç彿° FOOï¼å¯ç¨å½æ°ï¼ABSãAVGãâ¦ã | â | | 6 | è§åæé®é¢æ¶ç¹ãä¿åãâ æ½å±èªå¨æå¼ + æç¤ºãæ 1 æ¡å¤å®è§åè¿æ²¡å宿´ï¼è¯·å ä¿®æ£åä¿åãï¼**ä¸ä¸åä»»ä½å请æ±**ï¼ç½ç»é¢æ¿ç¡®è®¤ï¼ | â | | 7 | æ¹æåæ³åä¿å â ãå·²ä¿åå° v1.0ãï¼**é载设计å¨åé¡¶æ æ¾ç¤ºãå¤å®è§åï¼2ï¼ãï¼ä¸¤æ¡è§åçä½ç¨åä¸è¡¨è¾¾å¼é½å¨** | â | | 8 | ä¿åçº¸å¼ è®¾ç½® â `template/update` è¿å `{"code":0}`ï¼æ æªå¤çå¼å¸¸ï¼æ¹çº¸å¼ æ¹ååå读模æ¿è¡¨ç¡®å®åäºï¼éªè¯å®å·²æ¹å landscapeï¼ | â | | 9 | åå¸ v1.0 â å·²åå¸çæ¬å 容ä¸åï¼åç¹ä¿å â å¦ååºæ°è稿 v1.1ï¼**两æ¡è§å宿´å¸¦è¿å»** | â | 第 7ã9 两æ¡åèµ·æ¥è¯æäº**è§åæ°æ®ä¸å丢失**ï¼æ¢ä¸ä¼è¢«è¦çæ¸ ç©ºï¼ä¹ä¸ä¼å¨å¦åæ°çæ¬æ¶æã `pnpm typecheck` åå°åºçº¿ **316 æ¡**ï¼æ¹å¨ååºçº¿ 316ï¼`qc/report` ä¸ `components/quality` ä¸ **0 æ¡**ï¼ã åç«¯åæµ **29 æ¡å ¨ç»¿**ï¼`ReportEvaluatorTest` 23 + `FrontendConformanceTest` 3 + `ReportTemplateSchemaCompatibilityTest` 3ï¼ã ### 10.4 æ¬æ¬¡å®æµçä¸çæ°æ® éªè¯å¨**æ¨¡æ¿ 5ï¼åºè´§æ£éªæ¥å模æ¿ï¼**ä¸åçï¼ç¶ææ¯ï¼ - çæ¬ `v1.0`ï¼`qc_report_template_version.id=5`ï¼ï¼**å·²åå¸**ï¼åªè¯»ï¼ï¼å¸¦ 2 æ¡ç¤ºä¾è§å - çæ¬ `v1.1`ï¼`id=7`ï¼ï¼**è稿**ï¼åæ · 2 æ¡ç¤ºä¾è§å 两æ¡ç¤ºä¾è§åï¼`宿µå¼å¨è§æ ¼å `ï¼é项ï¼ã`æ´ä½åæ ¼çä¸ä½äº 90%`ï¼æ¥å级ï¼ã 齿¯ææä¹çæ¼ç¤ºæ°æ®ï¼**没æå é¤**ï¼å¦æä¸æ³çï¼å ææ¨¡æ¿ 5 å³å¯ã ### 10.5 ä¸ä¸æ¥ æç¨æ·éå®ç顺åºï¼æ¥ä¸æ¥æ¯**åºä»¶æ¥å£**ï¼Phase 3 第 4 æ¥ï¼ï¼åºä»¶ + `data_snapshot` + HTML/PDF 彿¡£ + å ¥ç« HTML æå¡ç«¯ Sanitizationã卿åå¿ é¡»å å¤çä¸ä»¶äºï¼ `QcReportInstanceDO` éæä¸ª `pdfFileUrl` åæ®µï¼**è¿å** `.claude/rules/file-upload.md` ï¼ç¦æ¢ä¸å¡è¡¨/DO åºç° `file_url` / `attachment_url` ä¸ç±»çæä»¶å°ååæ®µï¼ã PDF å¿ é¡»èµ° `system_storage_attachment` ä¸é´è¡¨ï¼`recordType = 'qc_report_instance'`ã è¿ä¸ª DO ç®åæ¯ä¸ªå¤å¿ï¼æ²¡æ Mapper/Service/å»ºè¡¨èæ¬ï¼ï¼æ¹èµ·æ¥æ²¡æå ¼å®¹è´æ ã å¦ï¼`ErrorCodeConstants` é `1_070_102_000/001/002`ï¼å®ä¾ï¼ä¸ `1_070_103_000..005`ï¼æ¸²æï¼å·²é¢ç使ªä½¿ç¨ã --- ## åä¸ãåºä»¶æ¥å£ï¼Phase 3 第 4 æ¥ï¼ââ â 宿ï¼ä¸¤è½® HTTP èè°å ¨ç»¿ï¼27/27 + 24/24ï¼ï¼ä¿®å¤ 2 个çå®ç¼ºé·å¹¶å¤éª > 宿´æ¹æ¡è§è®¡åæä»¶ `C:\Users\äº\.claude\plans\smooth-greeting-garden.md` > **代ç å±é¢å·²å ¨é¨è½å°**ï¼Controller / åæµ / åå¹¶èæ¬é½å·²å®æï¼43 项æµè¯å ¨ç»¿ï¼ã > å¯ä¸æ²¡åçæ¯**端å°ç«¯ HTTP èè°**ââéå éå¯å端 48080 让æ°ä»£ç çæï¼è§ 11.5ã ### 11.1 æ¬è½®å£å¾ï¼ç¨æ·ææ¿ï¼ | è®®é¢ | å³ç | |---|---| | æ°æ®æ¥æº | **两æ¥èµ°**ï¼æ¬è½®æ¥å£åªå**è°ç¨æ¹ä¼ å ¥çä¸ä¸æ**ï¼qcreport **ä¸ä¾èµ** `yudao-module-mes`ï¼ä¸ä¸è½®åå `MesQcReportApi` æé IPQC/IPQC/OQC/RQC åç§è´¨æ£å | | PDF èå´ | **å åªå HTML**ãPDF é Playwright ä¸èµ·ä¸ï¼éèç½ä¸è½½ Chromium + éå¯å端ï¼ï¼é¡ºå¸¦æ¸ æè¿è§ç `pdf_file_url` | | æ¥åç¼å· | **qcreport èªå·±çæ**ï¼`QR20260918-0001`ï¼è°ç¨æ¹ä¹å¯æ¾å¼ä¼ å ¥è¦ç | | ç¼ºå£æ¸ åè½åºï¼ | **ä¸è½åº**ï¼åªå¨ preview/generate/regenerate çååºéè¿å | | `pdf_file_url` æä¹å | ç¨ MCP ç´æ¥å¨è¿è¡åºæ§è¡ `DROP COLUMN` + åæ¥æ¹ DDL èæ¬ | | åå¹¶åå§åèæ¬ç¼ºå£ | **æ¬è½®ä¸å¹¶è¡¥ä¸** | ### 11.2 已宿 **ï¼1ï¼æ¸ æè¿è§ç `pdf_file_url`** | å¨ä½ | ç»æ | |---|---| | `QcReportInstanceDO` å åæ®µ | â å·²å ï¼é¡ºææ `id` ä¸åéçæ³¨éãæ¥åç¼å·ãæ¹ä¸ºãå®ä¾ç¼å·ã | | `docs/sql/qc_report_platform_ddl.sql` å åå®ä¹ | â å·²å | | è¿è¡åº `ruoyi-vue-pro` æ§è¡ `ALTER TABLE ... DROP COLUMN` | â å·²æ§è¡ | | **å读éªè¯**ï¼`information_schema`ï¼ | â `ruoyi-vue-pro` **14 å / `has_pdf_url`=0**ï¼`mom-jhhg` 仿¯ 15 åï¼åå²éçåºï¼æ profile æåï¼**ææä¸å¨**ï¼ | **ï¼2ï¼éè¯¯ç æ°å¢ä¸¤ä¸ª**ï¼`enums/ErrorCodeConstants.java`ï¼1-070-101 段å 007/008 åæ¬ç©ºçï¼ ```java TEMPLATE_NO_PUBLISHED_VERSION = 1_070_101_007 // 该模æ¿è¿æ²¡æå·²åå¸ççæ¬ï¼æ æ³çææ¥åï¼è¯·å å¨è®¾è®¡å¨ä¸åå¸ä¸ä¸ªçæ¬ TEMPLATE_VERSION_CANVAS_EMPTY = 1_070_101_008 // 该模æ¿çæ¬æ²¡æç»å¸å å®¹ï¼æ æ³çææ¥åï¼è¯·å å¨è®¾è®¡å¨ä¸è®¾è®¡å¹¶ä¿å模æ¿å 容 ``` ä¸å è¿ä¸¤ä¸ªç å°±åªè½å¤ç¨ã模æ¿çæ¬ä¸åå¨ãï¼ç¨æ·ç䏿é®é¢å¨åªï¼è¿å `.claude/rules/error-message-precision.md`ï¼ã å ¶ä½å ¨é¨å¤ç¨å·²é¢ççç ï¼`REPORT_INSTANCE_NOT_EXISTS` / `REPORT_INSTANCE_NO_DUPLICATE` / `REPORT_INSTANCE_DATA_SNAPSHOT_MISSING` / `RENDER_BUSINESS_DATA_MISSING`ã **ï¼3ï¼ä¸¤ä¸ªçº¯é»è¾æ¥ç¼**ï¼å¯è±ç¦» Spring åæµï¼åæµè§ï¼8ï¼ï¼ | æä»¶ | èè´£ | |---|---| | `service/instance/ReportNoGenerator.java` | `prefix(day)` â `QR20260918-`ï¼`format(day, seq)` â è¡¥é¶ 4 ä½ãè¶ 9999 èªç¶å 宽ï¼`nextSequence(latestNo, day)` â å°¾å· +1ï¼**è·¨æ¥/æ ¼å¼ä¸è®¤è¯/为空ä¸å¾å½ 1** | | `engine/context/ReportContextCodec.java` | `toMap(ReportContext)`ï¼ï¼`context.toScope()`ï¼/ `fromMap(Map)`ã**æå**éåæ®µè¯»åï¼ä¸é Jackson åå° ââ å¼ææ¬æ¥å°±æ¯è¿ä¸ªé£æ ¼ï¼`reportMap()`/`itemMap()` å ¨æ¯æ¾å¼åæ®µæ¸ åï¼ï¼ä¸è½é¿å¼ Jackson 3 / Jackson 2 ä¸¤å¥ Mapper å¹¶å带æ¥çæ°å¼ç±»å䏿ªç¥å段差å¼ã**è¿æ¯ãå岿¥åå¯å¤ç°ãçå¯ä¸å®ç°è·¯å¾** | **ï¼4ï¼æä¹ å±ä¸ Service** - `dal/mysql/instance/QcReportInstanceMapper.java`ï¼`selectPage` / `selectByReportNo` / `selectLatestByReportNoPrefix`ã**æ«è çæåºå»æåæ `ORDER BY CHAR_LENGTH(report_no) DESC, report_no DESC LIMIT 1`**ï¼ åºå·è¡¥é¶ 4 ä½ï¼å½å¤©ä¸æ¦è¶ è¿ 9999 ä½å®½å°±åé¿ï¼çº¯åå ¸åºä¼æ `...-9999` æå° `...-10000` åé¢ï¼javadoc å·²åæï¼ã - `service/instance/QcReportInstanceService.java` + `Impl`ï¼`preview` / `generate` / `regenerate` / `validateInstanceExists` / `getInstance` / `getSnapshot` / `getInstancePage` / `deleteInstance`ã - `service/instance/GenerateResult.java`ï¼`record GenerateResult(QcReportInstanceDO instance, List<String> errors)`ã - `service/version/QcReportTemplateVersionService(+Impl)`ï¼**æ°å¢** `getVersionByTemplateIdAndVersion(templateId, version)`ï¼ åçæ¬ä½**ä¸è¦æ±å·²åå¸**ï¼`regenerate` è¦ç¨ï¼çç±è§ 11.3ï¼ã **ï¼5ï¼VO å 个**ï¼`controller/admin/instance/vo/`ï¼ `QcReportInstanceGenerateReqVO`ï¼`templateId` @NotNullã`version`ã`reportNo`ã`businessId`ã`businessType`ã `context` @NotNull **ç´æ¥å¤ç¨å¼æç `ReportContext`**ï¼ã`PageReqVO`ã`RespVO`ï¼**廿ä¸å« `dataSnapshot`**ï¼ã `PreviewRespVO`ï¼`reportNo`/`html`/`errors`ï¼ã`GenerateRespVO`ï¼`id`/`reportNo`/`templateVersion`/`status`/`errors`ï¼ã `RegenerateRespVO`ï¼`id`/`reportNo`/`errors`ï¼ã `context` ç¨ `ReportContext` èä¸å¦é éå VOï¼å®å°±æ¯è¿ä¸ªå¹³å°å¯¹å¤çå¯ä¸æ°æ®å¥çº¦ï¼å¦é ä¸ä¸ªåªä¼å¤ä¸å¤è¦åæ¥çåæ®µæ¸ åã **ï¼6ï¼ç¼è¯** â `mvn compile -pl yudao-module-qcreport -am -q -o` â **exit 0** **ï¼7ï¼Controller ä¸ä¸ªç«¯ç¹** â `controller/admin/instance/QcReportInstanceController.java`ï¼`@RequestMapping("/qc-report/instance")`ï¼ ç § `QcReportTemplateVersionController` çåæ³ï¼`@Tag` / `@RestController` / `@Validated` / `@PreAuthorize("@ss.hasPermission(...)")` / `CommonResult.success` / `BeanUtils.toBean`ï¼ï¼ | ç«¯ç¹ | æéç | ç¼æ | |---|---|---| | `POST /preview` | `qc-report:instance:query` | `instanceService.preview(reqVO)` â `PreviewRespVO`ï¼`reportNo` å `outcome.context().getReport().getReportNo()`ï¼å¯è½ä¸ºç©ºï¼`html`ï¼`errors`ï¼ | | `POST /generate` | `qc-report:instance:create` | `GenerateResult` â `GenerateRespVO`ï¼id/reportNo/templateVersion/status/errorsï¼ | | `GET /get?id=` | `qc-report:instance:query` | `getInstance(id)` â `BeanUtils.toBean(...)`ï¼**å¤ç©º**ï¼è½¯å åè¿å `data:null`ï¼ä¸æ¥éï¼ | | `GET /snapshot?id=` | `qc-report:instance:query` | `getSnapshot(id)` â `CommonResult<Map<String,Object>>` | | `GET /page` | `qc-report:instance:query` | `getInstancePage(reqVO)` â `PageResult<RespVO>` | | `POST /regenerate?id=` | `qc-report:instance:create` | `GenerateResult` â `RegenerateRespVO`ï¼id/reportNo/errorsï¼ | | `DELETE /delete?id=` | `qc-report:instance:delete` | `deleteInstance(id)` | **ï¼8ï¼ä¸¤ä¸ªåæµç±»ï¼14 æ¡ç¨ä¾** â | ç±» | æ¡æ° | å®çæ¯ä»ä¹ | |---|---|---| | `service/instance/ReportNoGeneratorTest` | 7 | åç¼ä¸æ ¼å¼ãè¶ 9999 ä½å®½èªç¶å 宽ã**locale æ å ³**ï¼è®¾ `th-TH-u-ca-buddhist-nu-thai` ä»è¾åº `QR20260918-0007`ï¼ã忥éå¢ãæ ç¼å·å½ 1ãè·¨æ¥å½ 1ãæ ¼å¼ä¸è®¤è¯å½ 1ï¼7 ç§ç¸å½¢è¾å ¥ï¼ | | `engine/context/ReportContextCodecTest` | 7 | **æ ¸å¿ä¸æ¡ `roundTripReproducesHtml`**ï¼è¯» fixture â æ¸²æ â `toMap` â `fromMap` â 忏²æï¼æè¨ä¸¤æ¬¡ HTML **éåç¸å**ä¸ `errors` ç¸å ââ è¿å°±æ¯ãå岿¥åå¯å¤ç°ãçæºå¨è¯æï¼åæ¶è¯åºå¤å®æ¯å¹ççãå¦ï¼å¿«ç §å¸¦çç®å¥½çå¤å®ä¸åæ ¼çãå¾è¿ä¸ä¸¢åæ®µãæ ä¸ä¸éä¿æ nullãç¼ºåæ®µè½ç©ºä¸²ãç©ºå¿«ç §ä¸æãæ°ç»éæ··å ¥é对象å ç´ åªè·³è¿è¯¥å ç´ | å ¨æ¨¡å **43 é¡¹å ¨ç»¿**ï¼åæ 29 + æ¬è½® 14ï¼ï¼`mvn -pl yudao-module-qcreport test` exit 0ã **ï¼9ï¼åå¹¶åå§åèæ¬ `config_export_all_20260918.sql`** â 0904 坿¬ + ä¸å¤æ¹å¨ï¼â 头é¨è¯´ææ¹åï¼â¡`qc_report_*` 6 å¼ è¡¨å»ºè¡¨æ®µæå¨ `oa_notice_user` ä¸ `qrtz_blob_triggers` ä¹é´ ï¼`qc_` < `qr_`ï¼ï¼`qc_report_instance` **ä¸å« `pdf_file_url`**ï¼â¢é¶æ®µä¸ç `system_dict_type` è¡¥ 6 è¡ï¼1065400~1065405ï¼ã `system_dict_data` è¡¥ 21 è¡ï¼1065410~1065466ï¼ã **宿µéªè¯**ï¼ä¸æ¯åªçè¯æ³ï¼ï¼å¨çå®å®ä¾ä¸å»ºä¸´æ¶åº `qc_init_check_tmp`ï¼æ´ä»½ 1.2 MB æä»¶è·å® â EXIT=0ï¼**419 å¼ è¡¨**ã6 å¼ `qc_report_*`ã`qc_report_instance` 14 åæ `pdf_file_url`ã 6 个åå ¸ç±»å + 21 æ¡åå ¸æ°æ®ã1 个 adminã1258 个èåã1 个 default oauth2 clientã 2349 æ¡ `system_role_menu`ãqcreport èå 1075400~1075405 é½å ¨ï¼éå drop 临æ¶åºå¹¶ç¡®è®¤æ®ç为 0ã > æ§æä»¶ `config_export_all_20260904.sql` **åæ ·ä¿ç夿¥**ï¼åªç»´æ¤æ°çï¼`.claude/rules/sql-config-export-init.md`ï¼ã > â é¶æ®µä¸å«å ¨åº `DROP TABLE IF EXISTS`ï¼**åªè½å¨å ¨æ°/å¯è¦çç©ºåºæ§è¡ï¼ç¦æ¢å¨è¿è¡åºä¸è·**ã **ï¼10ï¼åç°å¹¶ä¿®å¤äº 0904 åºçº¿éçä¸ä¸ªæ¢æç¼ºé·ï¼é¶æ®µäº â¥ï¼** â æ°åºé¦æ¬¡æ§è¡æ¶èæ¬å¨ `system_role_menu` 䏿¥ `Duplicate entry '6352' for key 'system_role_menu.PRIMARY'` æ´æ®µä¸æï¼**åºåå¨ååå§åç¶æ**ã æ ¹å **䏿¯æ¬è½®å¼å ¥ç**ï¼æ¯ 0904 åºçº¿æ¢æåæ³ï¼é¶æ®µäº â¥ åæ ```sql DELETE FROM `system_role_menu`; INSERT INTO `system_role_menu` SELECT rm.* FROM `ruoyi-vue-pro`.`system_role_menu` rm INNER JOIN `system_user_role` ur ON ur.`role_id` = rm.`role_id`; ``` æºåº `system_user_role` ç**主é®åªæ `id`**ï¼æä»¥åä¸ `(user_id, role_id)` å¯ä»¥åå¨å¤è¡ ââ 宿µè¯¥è¡¨ 5 è¡ / åªæ 4 个ä¸å `role_id`ï¼`1:1, 1:2, 1:160, 1:1, 1:3`ï¼admin ç `role_id=1` ç¡®å®æ 2 è¡ï¼ id=1 å»ºäº 2022-01-11ãid=129 å»ºäº 2026-09-02ï¼ã`INNER JOIN` æ `system_role_menu` çæ¯ä¸è¡æ¾å¤§æ N è¡ï¼ æä¸»é®å¯¼è´æ´èæ¬ä¸æã **å·²æ¹ä¸ºå»éåæ³**ï¼`IN` åæ¥è¯¢å¤©ç¶å»éï¼ä¸è¡æ°æ å ³ï¼ï¼ ```sql -- â ä¸è½åæ INNER JOIN `system_user_role`ï¼æºåº system_user_role ç主é®åªæ¯ idï¼ -- åä¸ (user_id, role_id) å¯ä»¥åå¨å¤è¡ï¼å®æµ admin ç role_id=1 å°±æ 2 è¡ï¼ï¼ -- JOIN 伿 system_role_menu çæ¯ä¸è¡æ¾å¤§æ N è¡ï¼æ system_role_menu 主é®å¯¼è´æ´èæ¬ä¸æã -- ç¨ INï¼åæ¥è¯¢å¤©ç¶å»éï¼è¡¨è¾¾ãè¿äºè§è²çèåãï¼ä¸è¡æ°æ å ³ã INSERT INTO `system_role_menu` SELECT rm.* FROM `ruoyi-vue-pro`.`system_role_menu` rm WHERE rm.`role_id` IN (SELECT `role_id` FROM `system_user_role`); ``` æ¹åéè·ä¸´æ¶åº EXIT=0ï¼ä¸è¡æ°ä¸æºåº**é项ç¸ç**ï¼`role_menu` 2349=2349ã`menus` 1258=1258ï¼ã **è¿ç±»ç¼ºé·åªæçå¨ç©ºåºè·ä¸éæä¼æ´é²ï¼çº¯è¯æ³æ£æ¥æä¸å°ã** **ï¼11ï¼â ç¯å¢åï¼MCP è¿ç䏿¯å端ç¨çé£ä¸ªåº** æ¬è½®åå°ä¸åæ¶ï¼åä¸ä¸ªæ¥è¯¢å è½è¿å `ruoyi-vue-pro` + `mom-jhhg`ï¼å åéåçªç¶è¿åç©ºã æ¥ `SELECT @@hostname, @@port, DATABASE(), @@version` â `6c8bf09fabf1` / 3306 / `product-inventory-management-rbhb` / 8.4.6ï¼æ¯**å¦ä¸å°æ²çå®ä¾** ï¼30+ 个 `product-inventory-management-*` åºã没æ `mom-jhhg`ãå ¶ `ruoyi-vue-pro` æ 410 å¼ è¡¨ä¸ **0 å¼ `qc_report_*`**ï¼ã æ¹èµ°æå¨è·¯å¾ï¼`docker ps` â å®¹å¨ `mysql8`ï¼`0.0.0.0:3306`ï¼â `docker exec mysql8 mysql ...` å¾ `@@hostname` = **`2a8dea946ada`**ã**416 å¼ è¡¨**ã6 å¼ `qc_report_*`ã å¨è¿å°çå®å®ä¾ä¸å读ï¼`ruoyi-vue-pro.qc_report_instance` **14 å / `has_pdf_url`=0** ï¼è¯´æ 11.2ï¼1ï¼ç `DROP COLUMN` è½å¯¹äºåºï¼ï¼`mom-jhhg` ä» 15 åï¼**ææä¸å¨**ï¼ï¼å®ä¾è¡¨ 0 è¡ã > ä¸è®°å¿éãMCP 常è¿å¦ä¸å°æ²çå®ä¾ãä¸è´ï¼**æ¬é¡¹ç®ç建表/è¿ç§»æ ¸éªä¸è¦ä¿¡ MCPï¼èµ° `docker exec mysql8`**ã > å¦ï¼ç¨ `docker exec mysql8 mysql -e "..."` åä¸ææ³¨é**å¿ é¡»å `--default-character-set=utf8mb4`**ï¼ > å¦ååè¿å»æ¯ä¹±ç ï¼æ¬è½®å·²è¸©è¿ä¸æ¬¡ï¼`å®ä¾ç¼å·` åæ `实ä¾â¹Ã§Â¼âå·`ï¼å äºåæ°éæ§è¡å > `column_comment` ä¸ `HEX()` åæ£ç¡®ï¼ã ### 11.3 廿ç设计åèï¼é½ä¸æ¯éæåçï¼ | # | åè | çç± | |---|---|---| | 1 | **`generate` ä¸å `@Transactional`** | æ´æ®µåªæä¸æ¬¡ insertï¼æ²¡æå¤è¯å¥ä¸è´æ§éæ±ï¼åèå äºäºå¡ä¹åï¼éè¯è·¯å¾ä¸è¢« `catch` æç `DuplicateKeyException` æè¢«æ è®°æ rollback-only çé£é©ï¼ä¼æãéè¯ãåæãå¿ å®å¤±è´¥ã | | 2 | **èªå¨ç¼å·æå·éè¯ 5 次** | å¹¶åä¸ä¸¤ä¸ªè¯·æ±ä¼ç®åºåä¸åºå·ï¼é `uk_report_no` å¯ä¸ç´¢å¼æ¡ä¸æ¥éç®å³å¯ï¼è¿æ 5 次已è¿è¶ ãåæ¶åºä¸ä»½æ¥åãçæ£å¸¸å¹¶åï¼å失败就å¦å®æ¥éï¼ææ¡å¸¦ä¸éè¯æ¬¡æ°ä¸ã坿¾å¼æå®ç¼å·ãï¼ï¼ä¸åæ ééè¯ | | 3 | **æ¾å¼ç¼å·ä¸éè¯** | æå®äºç¼å·å°±æ¯æ¯æ¬¡åä¸ä¸ªå·ï¼éè¯æ²¡ææä¹ï¼å æ¥éï¼æäºç´æ¥æ¥ãæ¥åç¼å·ãXã已被å ç¨ï¼è¯·æ¢ä¸ä¸ªç¼å·ï¼æç空ç±ç³»ç»æ QR+æ¥æ+æµæ°´ èªå¨çæã | | 4 | **`regenerate` æ¥çæ¬ä¸è¦æ±å·²åå¸** | å·²åå¸çæ¬çå 容æ¹ä¸äºï¼`updateVersion` æ¦çï¼ï¼ä½å®å¯è½åæ¥è¢« `disableVersion` åç¨ï¼ä¸ä»½**å·²ç»åºè¿**çå岿¥åä¸è¯¥å æ¤åææä¸å¼ãç¨ `getPublishedVersion` 伿è¿ä¸ªåºæ¯å¡æ»ï¼æä»¥æ°å¢äº `getVersionByTemplateIdAndVersion` | | 5 | **`regenerate` ä¸è¦ç `data_snapshot`** | å»ç»çæ°æ®æ¯å岿¥åç便®ï¼éæ°çæå¨çåªæ¯**æç产ç©**ã弿å级å鿏²å¯è½å¾å°ä¸å HTMLï¼é£æ£æ¯ãéæ°çæã该æçææ | | 6 | **å¿«ç §åãå¤å®åãçä¸ä¸æ**ï¼`outcome.context().toScope()`ï¼ | è¿ææ¯äº§åºè¿ä»½ HTML çé£ä»½æ°æ®ï¼å«ç®å¥½ç PASS/FAIL ä¸åæ ¼çãåå¤å®**å**çåå§æ°æ®ï¼äºåå°±çä¸åºå½æ¶æ¯æä¹å¤ç | | 7 | **çæç¼å·æ æ¡ä»¶åå context 忏²æ** | 纸é¢ä¸çæ¥åç¼å·ä¸å®ä¾ç¼å·å¿ é¡»æ¯åä¸ä¸ªï¼ä¸è½åºç°ãæ¥åä¸å°çä¸ä¸ªå·ãç³»ç»éåçå¦ä¸ä¸ªå·ã | | 8 | **`preview` ä¸çæã䏿¶èåºå·** | ç¼å·æ¯åºä»¶æ¶æç¡®å®çæµæ°´å·ï¼é¢è§ä¸è¯¥æ¶èå®ï¼é¢è§åªç¨å ¥åéå¸¦è¿æ¥çç¼å· | | 9 | **`resolveVersion` è¦æ±æ¨¡æ¿ `ENABLE`** | ä¸ `createVersion` ä¿æä¸è´ï¼åç¨ç模æ¿ä¸åäº§åºæ°æ¥åï¼å岿¥åéæ°çæä¸èµ°è¿æ¡è·¯å¾ï¼ | | 9b | **`resolveVersion` ç `requirePublished` å¼å ³ï¼åºä»¶è¦ãé¢è§ä¸è¦** | åºä»¶ä¼æçæ¬å·å»ç»è¿å®ä¾ãèå®ä¾åªè®°çæ¬å·ä¸è®°å 容ï¼ç¨è稿åºä»¶çäºåä¸é¢ãçæ¬å·å¯¹å¾ä¸ãå 容已ç»åäºãçé·ï¼é¢è§ä¸è½åºãä¸å»ç»æ°æ®ã䏿¶èç¼å·ï¼è稿ä¼åå¨è¿é没æåæï¼èãä¿åè稿 â ç¹é¢è§ãæ£æ¯è®¾è®¡å¨ä¸»æµç¨ï¼è§ 11.7 ç¼ºé· 2ï¼ | | 9c | **ç¼å·åªå¢ä¸åï¼å æçå·ä¸åæ¶** | `uk_report_no` å¯ä¸ç´¢å¼ä¸è®¤ `deleted`ï¼è¥æãå¯è§çæå¤§å· +1ãç®ï¼å æå½å¤©æå¤§å·åå¿ ç¶å夿å·ï¼è§ 11.7 ç¼ºé· 1ï¼ãå æ¤åæå¤§å·æ¶**æè½¯å é¤çè¡ä¹ç®è¿æ¥**ï¼ä»£ä»·æ¯å½å¤©æµæ°´ä¼è·³å· | | 10 | **`prepareContext` å `report == null`** | 请æ±ä½éæ¾å¼ä¼ `"report": null` 伿å®ç½®ç©ºï¼ä¸å çè¯ `reportMap()` ä¼å¨æ¸²ææ·±å¤æ NPEï¼ç¨æ·åªçå°ãç³»ç»å¼å¸¸ã | | 11 | **æ ¡éª `inspectionItems` é空** | æ²¡ææ£éªé¡¹çãæ¥åãæ²¡ææä¹ï¼æ¥åºæ¥æ¯ç»ä¸å¼ 空表好 | | 12 | **æ¬è½®ä¸å¾ `system_menu` ææéè¡** | åç«¯è¿æ²¡æå®ä¾åè¡¨é¡µï¼æä¸ä¸ªæåä¸åå¨ç»ä»¶ç页é¢èåä¼æå¯¼èªæåãå½å `admin` æ¯ `super_admin`ï¼è¶ ç®¡ç´æ¥æ¾è¡ï¼èè°ä¸åå½±åã**å端å®ä¾é¡µè½å°æ¶ä¸å¹¶è¡¥èå + æé®æé**ï¼å¹¶æ `.claude/rules/sql-config-export-init.md` å½å ¥é¶æ®µäº | ### 11.4 æ¬è½®æ°å¢/ä¿®æ¹æä»¶æ¸ å **æ°å¢ï¼å端 `yudao-module-qcreport`ï¼** ``` service/instance/{QcReportInstanceService,QcReportInstanceServiceImpl,GenerateResult,ReportNoGenerator}.java engine/context/ReportContextCodec.java dal/mysql/instance/QcReportInstanceMapper.java controller/admin/instance/QcReportInstanceController.java controller/admin/instance/vo/{GenerateReqVO,PageReqVO,RespVO,PreviewRespVO,GenerateRespVO,RegenerateRespVO}.java ``` **æ°å¢ï¼æµè¯ï¼** ``` src/test/java/.../service/instance/ReportNoGeneratorTest.java (7 æ¡) src/test/java/.../engine/context/ReportContextCodecTest.java (7 æ¡) ``` **ä¿®æ¹ï¼å端 `yudao-module-qcreport`ï¼** | æä»¶ | æ¹å¨ | |---|---| | `enums/ErrorCodeConstants.java` | +2 个é误ç ï¼è§ 11.2ï¼ | | `dal/dataobject/instance/QcReportInstanceDO.java` | å `pdfFileUrl` åæ®µï¼ä¿®æ£ `id` çé误注é | | `service/version/QcReportTemplateVersionService(+Impl)` | +`getVersionByTemplateIdAndVersion` | | `service/instance/ReportNoGenerator.java` | å `Locale.ROOT`ï¼`ofPattern` ä¸ `String.format` 两å¤ï¼è§ä¸ï¼ | | `dal/mysql/instance/QcReportInstanceMapper.java` | **èè°åä¿®ç¼ºé· 1**ï¼`selectLatestByReportNoPrefix` â æå `@Select selectLatestReportNoByPrefix`ï¼ç»è¿é»è¾å é¤ãåªåç¼å·åï¼è§ 11.7ï¼ | | `service/instance/QcReportInstanceServiceImpl.java` | **èè°åä¿®ç¼ºé· 2**ï¼`resolveVersion` å `requirePublished` å¼å ³ï¼`preview` æ¾è¡è稿ï¼è¡¥ `versionDO == null â TEMPLATE_VERSION_NOT_EXISTS`ï¼è§ 11.7ï¼ | **æ°å¢ï¼ææ¡£ / SQLï¼** | æä»¶ | 说æ | |---|---| | `docs/qc_report_instance_frontend_integration.md` | å端èè°æ¹æ¡ï¼`.claude/rules/frontend-docs.md` 强å¶ï¼ï¼**æ ä»»ä½å端代ç çæ®µ** | | `docs/sql/config_export_all_20260918.sql` | åå¹¶åå§åèæ¬æ°çï¼è§ 11.2ï¼9ï¼ï¼ | | `docs/sql/qc_report_platform_ddl.sql` | å `pdf_file_url` åå®ä¹ï¼`id` æ³¨éæ¹ãå®ä¾ç¼å·ã | | `docs/project-business/` | å¢éï¼`01-business-mindmap` / `05-quality-flow` / `08-module-relation` / `09-data-flow` / `10-business-object` äºå¼ å¾ + `data/{objects,modules,flows,business}.json`ï¼æ¨¡å 12â13ã对象 46â48ï¼ï¼å·²è· `sync_index.js`ã**已卿µè§å¨å®æµ**ï¼èµ·éææå¡æå¼ `index.html`ï¼5 å¼ æ¹å¨å¾ `mermaid.parse` å ¨é¨éè¿ãè´¨éæµç¨å¾æ¸²ææ SVG 䏿¥éª¤è¡¨ 7 è¡ï¼å«æ°å¢ãåºä»¶ãï¼ã模åå¡ç 13 å¼ å«ãæºè½è´¨æ£æ¥åãã对象页åºç°ãæ¥å模æ¿/æ¥åå®ä¾ããæ ä»»ä½ mermaid éè¯¯æ¨ªå¹ | **æ°æ®åº** | åº | å¨ä½ | |---|---| | `ruoyi-vue-pro`ï¼å®¹å¨ `mysql8` / `2a8dea946ada`ï¼ | `ALTER TABLE qc_report_instance DROP COLUMN pdf_file_url`ï¼å·²æ§è¡ãå·²å读éªè¯ 14 åï¼ | | `mom-jhhg` | **ææä¸å¨**ï¼åå²éçåºï¼æ profile æåï¼ | | `qc_init_check_tmp` | 临æ¶åºï¼ç¨äºå®è·åå¹¶èæ¬ï¼**éªè¯å®å·² drop** | > â æäº¤æéï¼`.gitignore:64` æ `/docs/`ï¼`docs/` ä¸çæ°æä»¶**é»è®¤æ¯æªè·è¸ªä¸è¢«å¿½ç¥ç**ï¼ > éè¦ `git add -f`ï¼æ¢æç `docs/sql/config_export_all_20260904.sql`ã`docs/æºè½è´¨æ£æ¥å*.md` 齿¯è¿ä¹è¿æ¥çï¼ã > `yudao-module-qcreport/` æ´ä¸ªæ¨¡åç®å½ä¹è¿æ¯æªè·è¸ªç¶æã **ä¸ä¸ªé¡ºæä¿®æçéæ§ bug**ï¼`ReportNoGenerator` åæ¥æ¯ `DateTimeFormatter.ofPattern("yyyyMMdd")` + `String.format("%04d", seq)`ï¼ä¸¤å¤é½è·é**é»è®¤ locale**ï¼ é»è®¤ locale 另忳æ¶ï¼å¦æ³°åï¼å¹´ä»½ä¼åæ 2569ï¼æ°ååå½¢éæä¸æ¶ä¼ååºé¿æä¼¯-å°åº¦æ°åã å·²é `Locale.ROOT`ï¼å¹¶è¡¥äºå¯¹åºçåå½ç¨ä¾ã ### 11.5 ⬠æªå®æé¡¹ï¼æ¥æå çè¿éï¼ **å端æ¥å£è¿é¨å没æéç项äº** ââ 两个缺é·å·²ä¿®å·²å¤éªï¼è§ §11.7/§11.8ãå©ä¸ç齿¯ãä¸ä¸è½®ãçäºï¼ | # | äºé¡¹ | ç¶æ | 说æ | |---|---|---|---| | 1 | **å ¥ç« HTML Sanitization** | ⬠æ¬è½®**ææä¸å** | æ¬è½®æ²¡æå¤æ¥ HTML å ¥ç«ï¼æ¨¡æ¿ Schema åªåç»å¸ JSONï¼åºç« HTML ç± `HtmlRenderer` çæä¸å·²å¥ç¦» `on*` ä¸ `javascript:`/`vbscript:`/`data:` åè®®ï¼æ¦ä¸çä¼è¿ç¼ºå£æ¸ åï¼ãç AI/OCR 飿¡è·¯ï¼ä¼å¸¦è¿ä»»æ HTMLï¼å¼å·¥æ¶ååï¼ä¸¤è ä¸éå | | 2 | **PDF / Playwright / `system_storage_attachment` 彿¡£** | ⬠æ¬è½®æªå¼å§ | ä¸ä¸è½®ï¼éèç½ä¸è½½ Chromium + éå¯å端ï¼ã**ç¦æ¢ç¨æªå¾æ¹å¼çæ PDF** | | â | å端å®ä¾å表/é¢è§/详æ 页 | ⬠æªå¼å§ | å端æ¥å£ç¨³å®åååã**è½å°æ¶è¦ä¸å¹¶è¡¥ `qc-report:instance:{query,create,delete}` çèå䏿鮿éå¹¶å½å ¥åå¹¶èæ¬é¶æ®µäº**ï¼å¦åéè¶ ç®¡è§è²è°è¿äºæ¥å£ä¼ 403ï¼è¯¦è§ 11.3 #12 ä¸èè°æ¹æ¡ææ¡£ï¼ | ### 11.6 æç¡®ä¸åï¼é¿å èå´èå»¶ï¼ - `MesQcReportApi` + åç§è´¨æ£åå½ä¸å ââ ä¸ä¸è½®ã - å端å®ä¾å表/é¢è§/详æ 页 ââ å端æ¥å£ç¨³å®åååã - ç¼ºå£æ¸ åè½åºï¼ä¸å `render_errors` åï¼ââ å·²æç¨æ·éæ©ï¼åªå¨ååºéè¿åã - `qc_report_data_source` / `qc_report_rule` / `qc_report_render_record` ä¸å¼ æ Java 对åºç表 ââ ç»´æç°ç¶ã ### 11.7 é¦è½® HTTP èè°è®°å½ï¼2026-09-18ï¼ââ 27/27 éè¿ï¼é 2 个çå®ç¼ºé· èè°èæ¬å¨ `D:/qcl-tmp/`ï¼**ä»åºå¤**ï¼ä¸æ¬¡æ§éªæ¶å·¥å ·ï¼ä¸æäº¤ï¼ï¼ `e2e.mjs` 27 é¡¹éªæ¶ + `preview2.mjs` é¢è§è¡ä¸ºç©éµãåè¯ç¨ `POST /admin-api/system/auth/login` ï¼local profile å éªè¯ç ï¼`admin/admin123`ï¼ç´æ¥å tokenï¼ä¸èµ°å端 localStorageã **27/27 PASSï¼å ¶ä¸è¿äºæ¯ä¹ååªæåæµãæ²¡æè¿è¡æ¶è¯æ®ç**ï¼ | éªæ¶ç¹ | 宿µç»æ | |---|---| | æ´é¾æé | `generate` â `code=0`ï¼ç¼å· `QR20260918-0001`ã`id=1`ã`status=1`ã`templateVersion=v1.0` | | ç¼å·æ ¼å¼ | æ£å `^QR\d{8}-\d{4}$` å½ä¸ | | å¤å®ççç®åºæ¥äº | `snapshot` é å¤è§=FAIL / é¿åº¦=PASS / 硬度=FAILï¼`passRate=33.33%`ï¼`total=3 pass=1 fail=2` | | **ä¸åç«¯å¼æé¶æ¼ç§»** | ä¸è¿°ä¸é¡¹å¤å®ä¸ `passRate` ä¸å端å»ç» fixture **éå¼ç¸å** ââ ã两å¥å¼æä¼æ¼ãçé£é©å°±æ¤ä»ãåæµä¿è¯ãå级为ãç宿°æ®å®æµã | | å¿«ç §ç»æ | `snapshot` ä¸ `context.json` åæï¼`get` **ä¸å«** `dataSnapshot` | | regenerate å¤ç°æ§ | åå `renderHtml` **éåç¸å**ï¼ä¸ç¼å·ä¸åãåºè¡æ°ä¸å | | é夿¾å¼ç¼å· | `code=1070102001`ï¼msg **带åºå ·ä½ç¼å·** `QR20260918-0001` | | æ å·²åå¸çæ¬ / èç¨¿çæ¬ / 模æ¿ä¸åå¨ | åå« `1070101007` / `1070101003` / `1070100000` | | 空 `inspectionItems` / 缺 `templateId` | `1070103000` / `400` | | `page` è¿æ»¤ï¼templateId+businessType+businessId / ä¸å¹é / ç¼å·æ¨¡ç³ï¼ | å ¨é¨ç¬¦å颿 | | å é¤ | `delete` æåï¼å é¤å `get` è¿å `code=0, data=null`ï¼**䏿¥é**ï¼ï¼`regenerate` æ¥ `1070102000` | **å¦å¤ç¡®è®¤**ï¼`data_snapshot` åçæ¯**å¤å®å**çä¸ä¸æï¼æ¨¡æ¿ 5 v1.0 飿¡ `scope:"item"` è§å ï¼`actualValue >= lowerLimit AND actualValue <= upperLimit`ï¼å¯¹æ²¡æä¸ä¸éçãå¤è§ãå¤ FAIL ââ è¿æ£æ¯åç«¯å¼æçè¡ä¸ºï¼ä¸æ¯å端ç®éã #### ç¼ºé· 1ï¼è½¯å é¤çæ¥å伿¯æ»å½å¤©çèªå¨ç¼å·ï¼ä¸¥éï¼ **ç°è±¡**ï¼`e2e.mjs` åºä»¶å¾å° `QR20260918-0001`ï¼id=1ï¼å软å é¤å®ï¼ä¸ä¸æ¬¡ `generate` ç´æ¥å¤±è´¥ï¼ `1070102001 èªå¨çææ¥åç¼å·æ¶è¿ç» 5 次ä¸å·²ææ¥åå²çªï¼è¯·ç¨åéè¯ï¼ä¹å¯å¨è¯·æ±éæ¾å¼æå®ä¸ä¸ªæªè¢«å ç¨çæ¥åç¼å·`ã **æ ¹å **ï¼`selectLatestByReportNoPrefix` ç¨çæ¯æ¡ä»¶æé å¨ï¼MyBatis-Plus èªå¨è¿½å `deleted = 0`ï¼ ä½ `uk_report_no` å¯ä¸ç´¢å¼**ä¸è®¤ `deleted`**ã软å é¤åï¼ 1. æ¡ä»¶æé å¨çä¸å°å½å¤©æå¤§å·é£è¡ â `nextSequence` æ¯è½®é½ç®ååä¸ä¸ª `0001`ï¼ 2. æå ¥æ¯æ¬¡é½æå¯ä¸ç´¢å¼ â 5 次éè¯æ¯ 5 次åä¸ä¸ªç»æï¼ä¸æ¯ãè¿æ°å·®ãï¼ 3. åææ¯**å è¿ä¸ä»½æ¥åï¼è¿ä¸æ´å¤©å°±åä¹åºä¸äºä»¶**ï¼è䏿¥éææ¡æè´£ä»»æ¨ç»ãå¹¶åå²çªãï¼æäººå¾éæ¹åå¼ã **ä¿®å¤**ï¼`QcReportInstanceMapper`ï¼ï¼æ¹æå `@Select` ç»è¿é»è¾å é¤ï¼å¹¶åªåç¼å·åæ¬èº« ï¼ä¸å¿ æ JSON å¿«ç §æ´åæ½åºæ¥ï¼ï¼ ```sql SELECT report_no FROM qc_report_instance WHERE report_no LIKE CONCAT(#{prefix}, '%') ORDER BY CHAR_LENGTH(report_no) DESC, report_no DESC LIMIT 1 ``` **è¡ä¸ºåå**ï¼ç¼å·**åªå¢ä¸åï¼å æçå·ä¸åæ¶**ãå æ `â¦-0001` åä¸ä¸ä»½æ¯ `â¦-0002`ãå·²åè¿èè°æ¹æ¡ã å¹¶åéè¯è·¯å¾ä»ç¶ä¿çï¼çæ£ç并忢å·è¿å¾é å®ï¼ï¼åªæ¯ä¸åæ¯ãå é¤åå¿ å®è§¦åãã **åºä¸çç¡¬è¯æ®**ï¼å®¹å¨ `mysql8`ï¼`ruoyi-vue-pro`ï¼ï¼ ``` qc_report_instance: id=1 report_no=QR20260918-0001 deleted=1 â 软å é¤åä»å¨ç©ç表é uk_report_no: Non_unique=0 Column_name=report_no â å¯ä¸ç´¢å¼åªæ report_noï¼ä¸å« deleted ``` å䏿¡åç¼æ¥è¯¢ï¼ä¸¤ç§åæ³ç»æä¸å ââ è¿å°±æ¯ç¼ºé·çå ¨é¨æºå¶ï¼ ```sql -- æ° SQLï¼æè½¯å é¤ç®è¿æ¥ï¼â è¿å QR20260918-0001 â nextSequence å¾ 2 â æå ¥æå SELECT report_no FROM qc_report_instance WHERE report_no LIKE CONCAT(@prefix,'%') ORDER BY CHAR_LENGTH(report_no) DESC, report_no DESC LIMIT 1; -- æ§åæ³çä»·ç©ï¼æ¡ä»¶æé å¨èªå¨å deleted=0ï¼â ä¸è¡é½æ¥ä¸å° â nextSequence å¾ 1 â æå¯ä¸ç´¢å¼ SELECT report_no FROM qc_report_instance WHERE report_no LIKE CONCAT(@prefix,'%') AND deleted = 0 ORDER BY CHAR_LENGTH(report_no) DESC, report_no DESC LIMIT 1; ``` **è¿æ¡ä¿®å¤æ²¡æä»»ä½åæµè¦ç**ï¼æ¨¡åæ H2/DB æµè¯åºåº§ï¼ç¼ºé·å¨è¯» SQL èä¸å¨ Java 忝éï¼ï¼ æä»¥ä¸é¢è¿æ¡ SQL æ¯ç´æ¥æ¿çåºéªçï¼ä¿®å¤åçè¡ä¸ºè¦é §11.5 第 1 项ç HTTP å¤éªã #### ç¼ºé· 2ï¼`preview` è¦æ±çæ¬å·²åå¸ï¼æè®¾è®¡å¨ä¸»æµç¨å¡æ»ï¼ä¸¥éï¼ **ç°è±¡**ï¼æ¨¡æ¿ 5 ç `v1.1`ï¼è稿ï¼ä¸æ¨¡æ¿ 3 ç `v1.0`ï¼è稿ï¼ä¸æ¯**åºéå¯ä¸ä¸ä»½çå®ç»å¸**ï¼14657 åèï¼ è° `preview` é½è¿å `1070101003 模æ¿çæ¬æªåå¸ï¼æ æ³çææ¥å`ã **æ ¹å **ï¼`preview()` å¤ç¨äºä¸ºåºä»¶åç `resolveVersion()`ï¼éé¢è°çæ¯ `getPublishedVersion()`ã ä½è®¾è®¡å¨ç主æµç¨æ£æ¯ãä¿åè稿 â ç¹é¢è§ãï¼ä¿ååºæ¥çå¿ ç¶æ¯è稿 ââ ä¹å°±æ¯è¯´è¿æ¡è·¯å¾**ä¸å¯è½æå**ã æ¹æ³ä¸ç javadoc åçãç¨åªä¸ªæ¨¡æ¿çæ¬**åºä»¶**ãï¼æ¬èº«å°±æ´é²äºå®æ¯è¢«åç¨çã **ä¿®å¤**ï¼`resolveVersion(reqVO, boolean requirePublished)` å å¼å ³ï¼ `generate` ä¼ `true`ã`preview` ä¼ `false`ï¼èµ° `getVersionByTemplateIdAndVersion`ï¼ã `getPublishedVersion` æ¥ä¸å°ä¼æéã`getVersionByTemplateIdAndVersion` è¿å `null`ï¼æä»¥è¡¥äºä¸å¥ `versionDO == null â TEMPLATE_VERSION_NOT_EXISTS`ã **为ä»ä¹ä¸æ¾æ¾ `generate`**ï¼åºä»¶ä¼æçæ¬å·å»ç»è¿å®ä¾ï¼èå®ä¾é**åªè®°çæ¬å·ãä¸è®°å 容**ï¼ ç¨è稿åºä»¶çäºç»å岿¥ååä¸é¢ãçæ¬å·å¯¹å¾ä¸ãå 容已ç»åäºãçé·ï¼ é¢è§ä¸è½åºãä¸å»ç»æ°æ®ã䏿¶èç¼å·ï¼è稿ä¼åå¨è¿é没æä»»ä½åæã **顺带记ä¸ç¬**ï¼åºéå¯ä¸ä¸ä»½æçå®ç»å¸ççæ¬æ¯**è稿**ï¼æ¨¡æ¿ 3 v1.0ï¼ï¼ä¸æ¨¡æ¿ 3 ç `current_version` 为 `NULL` ââ æä»¥é¦è½®èè°ç渲æéªè¯åªè½ç¨æ¨¡æ¿ 5 ç**空ç»å¸**ï¼560 åèãbody 为空ï¼ã éå¹³å¡ HTML çæ¸²æéªè¯è¢«ç¼ºé· 2 æ¡çï¼ä¿®å®åæåå¾äºã #### èè°é两个ãå缺é·ãï¼å«é踩 - **`500 ç³»ç»å¼å¸¸` 䏿¬¡ï¼å®é æ¯æèªå·±çé **ï¼bash `for` 循ç¯éå è䏿ï¼shell æ GBK ç¼ç ååºå»ï¼ åç«¯æ¥ `tools.jackson.core.exc.StreamReadException: Invalid UTF-8 middle byte 0xe2` ï¼è·¯å¾ç´æ `ReportContext["inspectionItems"][0]["itemName"]`ï¼ãæ¢æ node èæ¬å UTF-8 JSON å³å¥½ã **æè®**ï¼å¸¦ä¸æç请æ±ä½å«ç¨ bash å èã - **`file://` æä¸å¼ `docs/project-business/index.html`**ï¼Playwright ç¦æ¢ `file://` 导èªï¼ èµ·äºä¸ªä¸´æ¶éææå¡å¨æéªæãéªè¯æ¬èº«æ¯å¿ è¦ç ââ 5 个æ¹å¨ç `.mmd` ç¨ mermaid 11 ç `mermaid.parse()` é个解æéè¿ãç¹è¿å¯¼èªæ¸²æåº SVGãæ æ¥éæ¨ªå¹ ï¼13 个模åå¡çä¸ãæ¥å模æ¿/æ¥åå®ä¾ã对象é½å¨ã ### 11.8 ä¿®å¤åå¤éªï¼åæ¥ï¼éå¯å端åï¼ââ 24/24 + 27/27 å ¨ç»¿ æ°å¢ `D:/qcl-tmp/real-canvas.mjs`ï¼ä»åºå¤ï¼ä¸æ¬¡æ§èæ¬ï¼ãå®è¦è§£å³ä¸ä¸ªé¾é¢ï¼**åºéå¯ä¸æçå®ç»å¸ççæ¬æ¯è稿** ï¼æ¨¡æ¿ 3 v1.0ï¼14657 åèï¼`rules: []`ï¼ä¸æ¨¡æ¿ 3 ç `current_version` 为 `NULL`ï¼ï¼ è `generate` æè®¾è®¡æç»è稿ãèæ¬çåæ³æ¯**建ä¸ä¸ªä¸´æ¶æ¨¡æ¿ â ææ¨¡æ¿ 3 çç»å¸å¤å¶æå®ç v1.0 â åå¸ â è·å ¨é¾**ï¼ è·å®å å¹²åã**ä¸ç¢°æ¨¡æ¿ 1/3/4/5/6 ç任使¢ææ°æ®ã** | éªæ¶ç¹ | 宿µç»æ | |---|---| | ç¼ºé· 2 ä¿®å¤ï¼`preview` æ¾è¡è稿 | æ¨¡æ¿ 3 v1.0ï¼çå®ç»å¸ï¼â `code=0`ï¼**HTML 6719 åè**ï¼æ¨¡æ¿ 5 v1.1ï¼è稿ï¼â `code=0` | | ç¼ºé· 2 çè¾¹çï¼`generate` 仿è稿 | æ¨¡æ¿ 3 v1.0 䏿¨¡æ¿ 5 v1.1 â **é½ä»æ¯ `1_070_101_003`**ï¼ææçä¸å¯¹ç§°ï¼æ²¡è¢«é¡ºææ¾æ¾ï¼ | | ç¼ºé· 2 çè¿å¸¦ï¼å·²åç¨çæ¬ä¹å¯é¢è§ | æ¨¡æ¿ 1 ä¸ä¼ `version`ï¼`currentVersion=v1.0`ï¼status=2 åç¨ï¼â `code=0`ï¼468 åèï¼é¦è½®è¿éæ¯ `1_070_101_003`ï¼ | | **çå®ç»å¸ç preview äº§ç©æ¯ç渲æ** | 6719 åèéå« `å¤å®ç»è®ºï¼ä¸åæ ¼`ã`åæ ¼çï¼33.33%`ãä¸ä¸ªæ£éªé¡¹åä¸è¡ï¼`data-qc-repeat` å±å¼äºï¼ | | **çå®ç»å¸ç preview â generate ä¸è´** | `preview.html === get.renderHtml`ï¼ä¸¤ä¾§é½æ¯ 6719 åèï¼**éåç¸å** | | **çå®ç»å¸ç regenerate å¤ç°æ§** | 鿏²åå `renderHtml` **éåç¸å**ï¼6719 â 6719ï¼ï¼ç¼å·ä¸å | | çå®ç»å¸çå¤å®è½è¿å¿«ç § | `passRate=33.33% total=3 pass=1 fail=1`ï¼`items=[å¤è§:(空) é¿åº¦:PASS 硬度:FAIL]` | | ç¼ºé· 1 ä¿®å¤ï¼è½¯å é¤åä»è½åºä»¶ | åº `â¦-0003` â 软å é¤å® â ååº `code=0`ï¼å¾ `â¦-0004`ï¼**ä¿®å¤åè¿éå¿ å®æ¯ `1_070_102_001`**ï¼ | | ç¼ºé· 1 çè¡ä¸ºï¼ç¼å·åªå¢ä¸å | `QR20260918-0003 â QR20260918-0004`ï¼ä¸¥æ ¼éå¢ï¼æ²¡åæ¶ | | ç¼ºé· 1 çä¸è´ï¼æ¾å¼å ç¨è¢«è½¯å é¤çå·ä»è¢«æ | æå® `â¦-0003` â `1070102001`ï¼msg 带åºå ·ä½ç¼å· | | åå½ï¼é¦è½® 27 é¡¹å ¨è¿ | `e2e.mjs` éè· **27/27 PASS**ï¼å«ç¼å·æ ¼å¼ / å¿«ç §å¤å® / regenerate éå / åé误ç / page / deleteï¼ | | åå½ï¼é¢è§è¡ä¸ºç©éµ | `preview2.mjs` ç©éµå ¨é¨ç¬¦åé¢æï¼æ¨¡æ¿ 3 ä¸ä¼ çæ¬ä» `1_070_101_007`ï¼`currentVersion` 为 NULLï¼**é¢è§ä¹å¾æ¾å¼ä¼ çæ¬**ï¼ | **éªè¯è¿ç¨ä¸çº æ£çä¸ä¸ªè¯¯å¤**ï¼è®°ä¸æ¥å å¾å人é踩ï¼ï¼ `real-canvas.mjs` é¦è· `1.3` å¤ FAILï¼æè¨ãäº§ç© HTML éåºå« `PASS`/`FAIL` åæ ·ãââæ¯**æçæè¨åéäº**ï¼ä¸æ¯ç¼ºé·ã æ¨¡æ¿ 3 çç»å¸æ¾ç¤ºçæ¯ `resultText`ï¼ä¸æ `åæ ¼` / `ä¸åæ ¼` / `å¾ å¤å®`ï¼ï¼ `result`ï¼`PASS`/`FAIL`ï¼æ¯æ°æ®å±æºå¨å¼ãå·²ææè¨æ¹ææ£æ¥ä¸æææ¡ + ç¼ºå£æ¸ åï¼æ¹æ 24/24ã **ç»è®º**ï¼äº§ç© HTML éæä¸å° `PASS` ä¸ä»£è¡¨æ²¡å¤å®ï¼è¿æ¡å·²åè¿èè°æ¹æ¡ï¼å å¾å端ä¹è¿ä¹è¯¯å¤ã **顺带确认ç䏿¡å¼æè¯ä¹**ï¼æ¢æ è§æ ¼ä¸ä¸éãåæ²¡å½ä¸ä»»ä½è§åçæ£éªé¡¹ â `result` 为空ã`resultText` 为 `å¾ å¤å®`ï¼ **ä¸è®¡å ¥ `passCount`/`failCount` ä½è®¡å ¥ `total`**ï¼æä»¥ä¼åºç° `total=3 pass=1 fail=1`ï¼ï¼ åæ¶è¿ç¼ºå£æ¸ åï¼ã第 1 项ãå¤è§ã没æè§æ ¼ä¸ä¸é乿²¡æå¤å®è§åï¼æ æ³å¤å®ãï¼ãæ¨¡æ¿ 3 ç `rules` æ¯ç©ºæ°ç»ï¼ æ£å¥½æä¸è¿ä¸ªåæ¯ ââ æ¨¡æ¿ 5 æé£æ¡ `scope:"item"` è§åï¼æä»¥åæ ·çä¸é¡¹å¨æ¨¡æ¿ 5 䏿¯ FAIL/PASS/FAILã **æ¶å°¾ï¼å·²æ§è¡ï¼**ï¼ä¸´æ¶æ¨¡æ¿ 8/9ã临æ¶çæ¬ 8/9ãä»¥åæ¬è½®äº§çç 9 è¡æ¥åå®ä¾å ¨é¨ç¡¬å é¤ã åè¯»æ ¸å¯¹ï¼`qc_report_template` **5 è¡**ã`qc_report_template_version` **7 è¡**ã`qc_report_instance` **0 è¡**ã `qc_report_render_record` **0 è¡** ââ ä¸å¼å·¥åä¸è´ï¼æ¨¡æ¿ 1/3/4/5/6 åå ¶ `current_version` åæ ·æªå¨ã ### 11.9 â å·²éªè¯ / æªéªè¯ï¼å¿ é¡»åå¼è¯´ **å·²ç»æ¿å°å®æµè¯æ®ç**ï¼è¿äºå¯ä»¥è¯´ãéªè¯è¿ãï¼ | ç»è®º | è¯æ® | |---|---| | ç¼è¯éè¿ | `mvn compile -pl yudao-module-qcreport -am -q` exit 0ï¼ä¸¤ä¸ªç¼ºé·ä¿®å®åéè·ä» exit 0ï¼ | | 43 项æµè¯å ¨ç»¿ | `mvn -pl yudao-module-qcreport test` â `Tests run: 43, Failures: 0, Errors: 0`ï¼å¯¹æ 3 + å¤å® 23 + Codec 7 + ç¼å· 7 + åéå ¼å®¹ 3ï¼ | | å¤å®å¹çãå¿«ç §å¾è¿å¯å¤ç° | `ReportContextCodecTest.roundTripReproducesHtml` æè¨ä¸¤æ¬¡ HTML éåç¸å | | ç¼å·çæ locale æ å ³ | `ReportNoGeneratorTest.independentOfDefaultLocale` 卿³°å + æ³°ææ°å locale ä¸ä»å¾ `QR20260918-0007` | | **åºä»¶æ¥å£æ´é¾å¯ç¨ï¼ä¸¤è½®ï¼** | é¦è½® `e2e.mjs` 27/27ï¼ä¿®å¤åéè·ä» 27/27ï¼`real-canvas.mjs` 24/24ãæ¯é¡¹é½èµ°çå® HTTP æ´é¾ï¼é´æ â åæ°ç»å® â ç¼æ â è½åº â åºååï¼ | | **å端å¤å®ä¸åç«¯å¼æé¶æ¼ç§»** | æ¨¡æ¿ 5 v1.0ï¼æè§åï¼â FAIL/PASS/FAILã`passRate=33.33%`ï¼ä¸å端 fixture éå¼ç¸å | | **éå¹³å¡ HTML çæ¸²æä¸å¤ç°** | æ¨¡æ¿ 3 ççå®ç»å¸ï¼14657 åèï¼â 6719 åè产ç©ï¼`preview === get.renderHtml` ä¸ `regenerate` ååéåç¸å | | **两个缺é·çä¿®å¤å·²è¢«å¤éª** | 软å é¤åä»è½åºä»¶ä¸ç¼å·éå¢ï¼`preview` æ¾è¡è稿è `generate` 仿è稿ï¼è§ §11.8ï¼ | | `pdf_file_url` å·²ä»è¿è¡åºå é¤ | å®¹å¨ `mysql8`ï¼`@@hostname=2a8dea946ada`ï¼å读 `information_schema`ï¼`ruoyi-vue-pro.qc_report_instance` **14 å / `has_pdf_url`=0** | | åå¹¶åå§åèæ¬å¨ç©ºåºè½ä¸é®è·é | 临æ¶åº `qc_init_check_tmp` å®è·æ´ä»½ 1.2 MB æä»¶ â EXIT=0ï¼419 表 / 6 å¼ `qc_report_*` / 6 åå ¸ç±»å / 21 åå ¸æ°æ® / 1258 èå / 2349 `system_role_menu`ï¼**éªå®å·² drop** | | ä¸å¡å¯è§åå¢é卿µè§å¨éççæ¸²æåºæ¥äº | 5 个æ¹å¨ç `.mmd` å¨ mermaid 11 ä¸ `parse()` å ¨ okï¼è´¨éæµç¨å¤äºãåºä»¶ãä¸è¡ï¼13 个模åå¡çï¼å¯¹è±¡é¡µæãæ¥å模æ¿/æ¥åå®ä¾ã | **è¿æ²¡æä»»ä½å®æµè¯æ®ç**ï¼è¿äº**ä¸è½è¯´ãå¯ç¨ã**ï¼ - **å¹¶åèªå¨ç¼å·çéè¯è·¯å¾æ²¡åè¿**ãé»è¾å¨ï¼æ `uk_report_no` å°±éç®åºå·åè¯ï¼æå¤ 5 次ï¼ï¼ä½æ²¡æå¹¶åæµè¯ã ç¼ºé· 1 ä¿®å¤åï¼è¿æ¡è·¯å¾åªä¼å¨**çæ£çåæ¶æ¢å·**æ¶è§¦åï¼æ£å¸¸ä½¿ç¨ä¸è¯¥çå°å®ã - **ç¼ºé· 1 çä¿®å¤æ²¡æåæµè¦ç**ãæ¨¡å没æ H2/DB æµè¯åºåº§ï¼è缺é·å¨è¯» SQL ä¸å¨ Java 忝é ï¼`@Select` ç»è¿é»è¾å é¤ï¼ãå®ç°å¨åªæãçåºä¸è·ä¸¤ç§åæ³çå¯¹æ¯ + HTTP å¤éªãä¸¤ç±»è¯æ®ï¼ ä¸è¦å» `ReportNoGeneratorTest` éæ¾å®ï¼é£éæµä¸å°ã - **å端å®ä¾å表/é¢è§/详æ 页没æåï¼æä»¥æ²¡æä»»ä½ UI éªè¯**ãä¸å¡å¯è§åçæµè§å¨éªè¯ä¸è½ç®ä½ãå®ä¾é¡µè¢«éªè¯è¿ãã - **åç«¯è¿æ²¡æ¥ `preview`**ï¼`grep -r "instance/preview" mom-pro2-before/src` æ å½ä¸ï¼ï¼è®¾è®¡å¨çé¢è§æé®ä»èµ°èé»è¾ã - **`qc-report:instance:{query,create,delete}` æéç æ²¡å ¥åº**ï¼éè¶ ç®¡è§è²è°è¿äºæ¥å£ä¼ 403ï¼å端页é¢è½å°æ¶ä¸å¹¶è¡¥ï¼ã - **级èå é¤ / æ°æ®æé / 导åº** é½ä¸å¨æ¬è½®èå´ï¼`GENERATING`/`FAILED` ä¸¤ä¸ªç¶æææªä½¿ç¨ï¼ç¼ºå£æ¸ åä¸è½åºã - PDF é¾è·¯ï¼Playwright + `system_storage_attachment` 彿¡£ï¼**æªå¼å§**ã > **åç¡®ç说æ³**ï¼åºä»¶æ¥å£ï¼åªå HTMLï¼**å端é¨åå·²å®æå¹¶ä¸¤è½®å®æµéè¿**ï¼ > ä½å®**è¿æ²¡æä»»ä½åç«¯é¡µé¢æ¥å ¥**ï¼æéç ä¹è¿æ²¡è¿ `system_menu`ã > ä¸è¦æãHTTP èè°å ¨ç»¿ã说æãåè½å·²ä¸çº¿ãï¼ä¹ä¸è¦æãä¸å¡å¾æ¸²ææ£å¸¸ã说æãå®ä¾é¡µéªè¯è¿ãã ## åäºãPDF åºä»¶ï¼Phase 3 第 5 æ¥ï¼ââ â çå® HTTP èè° + è§è§éªæ¶å·²å®æï¼8 次çå®å¯¼åºã7 æ¡æ¸²æè®°å½ã3 页䏿 PDF å·²çè¿ï¼ï¼ð¡ 页çä¿®å¤ä¸ä¸æ¡éè¯¯åæ¯ä»å¾ å¤éª > ç®æ ï¼æå®ä¾éå·²åçäº§ç© HTML **ç±åç«¯è° Chromium æå°æ PDF**ï¼å½æ¡£è¿éä»¶åºï¼åç«¯åªæ¿ä¸è½½å°åã > §30 æç¡®ç¦æ¢ãæ¯æ¬¡è¯·æ± launch æµè§å¨ â çæ PDF â close æµè§å¨ãï¼Â§41 è¦æ±ç渲æè®°å½ã ### 12.1 交ä»ç© | å± | æä»¶ | èè´£ | |---|---|---| | é ç½® | `config/QcReportPdfProperties.java` | `yudao.qcreport.pdf` åç¼ï¼`enabled` / `channel` / `executable-path` / `concurrency`(2) / `timeout-ms`(30s) / `show-page-number` / `browser-args` | | 纯æ¥ç¼ | `service/render/PdfPrintOptions.java` | **çº¯å½æ°**ï¼çº¸å¼ é ç½® â æå°å ä½ + 页è/é¡µçæ¨¡æ¿ + ä» ä¾æå°çå åº CSSãä¸ä¾èµ Springãä¸ä¾èµ Playwrightï¼å æ¤å¯åæµ | | æµè§å¨æ± | `service/render/BrowserManager.java` | Playwright çå½å¨æï¼`volatile Semaphore` éå¹¶åã`synchronized(browserLock)` ä¿è¯æµè§å¨å®ä¾å¯ä¸ãå´©äºè½éå»ºå¹¶æ¥ `RESTARTED` | | æå° | `service/render/PdfRenderService.java` | è½½å ¥ HTML â `addStyleTag` æ³¨å ¥æå° CSS â `page.pdf()`ãè¿å `PdfResult(content, pdfDurationMs, browserStatus)` | | åºä»¶ä¾§ | `service/instance/QcReportInstanceServiceImpl#exportPdf` + `PdfArchiveResult` + `QcReportInstancePdfRespVO` | æå®ä¾å·²å HTML â å blob â æ¢éä»¶ â 忏²æè®°å½ | | ç«¯ç¹ | `POST /qc-report/instance/pdf?id=` | æéç `qc-report:instance:export` | | çç | `QcReportRenderRecordDO` / `Mapper` / 表 `qc_report_render_record` | §41ï¼`reportId` `templateId` `businessId` `renderStartTime/EndTime` `renderDuration` `pdfDuration` `browserStatus` `errorStack` | | ä¾èµ | `yudao-module-qcreport/pom.xml` | `com.microsoft.playwright:playwright` | | é ç½® | `application-local.yaml` + `application-test.yaml` | `yudao.qcreport.pdf` æ´æ®µï¼`channel: chrome`ï¼æ¬æºå·²è£ Chromeï¼ | ### 12.2 ä¸ä¸ªå»æçè®¾è®¡éæ© | éæ© | çç± | |---|---| | **æçæ¯å®ä¾éå·²åç HTMLï¼ä¸é渲** | ã详æ 页çå°çãä¸ãæåºæ¥çãå¿ ç¶åºèªåä¸ä»½åèãè¥é渲ï¼é¡µé¢ä¸æ¯æ°æçãä¸è½½å°çæ¯æ§æä»¶ï¼ä¼éé»ä¸ä¸è´ | | **å ä½ä¸å¾ä» `PageSizes.resolve` å** | ä¸ `HtmlRenderer` åè¿ HTML çé£è¡ `@page { size: â¦; margin: â¦; }` **åæºãåä¸ä¸ªæ°åæ ¼å¼å彿°**ï¼`Numbers.toString`ï¼ï¼å æ¤ä¸åå¨ãé¢è§ 210mmãPDF æ 210.5mmãè¿ç±»æ¼ç§»ãç¨ `String.format` ä¼å¼å ¥ locale å·®å¼å¹¶è¾åº `210.0` | | **`PRINT_CSS` ä¸è¿æ¸²æå¼æç `BASE_CSS`** | äº§ç© HTML ä¸å端å»ç»æ ·ä¾æ¯**é忝坹**çï¼`FrontendConformanceTest`ï¼ï¼å¨ `BASE_CSS` ä¼è®©å鿥åçãéæ°çæãç»æåæ ·ãæä»¥å åºæ ·å¼ï¼è¡¨å¤´è·¨é¡µéå¤ãé¿å å ç´ è¢«åæãå¼ºå¶æå°èæ¯è²ï¼èµ° Playwright `addStyleTag` åç¬æ³¨å ¥ | | **é¡µçæ¨¡æ¿æ¾å¼ç»ä¸ä¸ªç©º `<div>`** | `displayHeaderFooter=true` è `headerTemplate` ç空æ¶ï¼Chromium ä¼ç¨å®èªå·±çé»è®¤é¡µçï¼å·¦ä¸è§æå°æ¥æãå³ä¸è§ææ¡£æ é¢ãé£ä¸¤ä¸²æ²¡äººè¦è¿ï¼é¡µçéçæ¶é´è¿å®¹æè¢«è¯¯å½ææ¥ååºå ·æ¶é´ | | **彿¡£ä¸æ¥é¡ºåºï¼åæ° blob â å æ§éä»¶ â ç»æ°éä»¶** | `bindAttachments` åªæ¢å ³èã**ä¸å æ§ blob**ï¼ä¸å å 就伿¯å¯¼ä¸æ¬¡å¨ç£çä¸å¤çä¸ä»½æ²¡äººè®¤é¢ç PDFï¼èãå å ååãåä¼å¨åæ¡£å¤±è´¥æ¶æè¿å¥½çæ§ PDF ä¸èµ·å¼ä¸¢ãæä»¥æ° blob å è½å°ï¼åå æ§çï¼æåç»å® | | **`regenerate` å³ä½åºå·²å½æ¡£ PDF** | äº§ç© HTML æ¢äºï¼åççæ§ PDF å°±æ¯ã页颿¯æ°æçãä¸è½½å°çæ¯æ§æä»¶ãçéé»ä¸ä¸è´ãå®å¯ä½åºï¼éæ¶è½å坼䏿¬¡ï¼ | | **失败ä¹å渲æè®°å½** | `catch` éå å䏿¡å¸¦å¼å¸¸æ çè®°å½åæå¼å¸¸åæ ·æåºï¼ä¸å | `exportPdf` ç¨çæ¯ `getVersionByTemplateIdAndVersion` è**ä¸è¦æ±çæ¬å½åä»å·²åå¸** ââ ä¸ `regenerate` åä¸å£å¾ï¼å岿¥åä¸è¯¥å ä¸ºçæ¬åæ¥è¢«åç¨å°±å¯¼ä¸åºæ¥ã ### 12.3 宿µè¯æ® #### 12.3.1 ç¼è¯ + åæµå± | ç»è®º | è¯æ® | |---|---| | ç¼è¯éè¿ | `mvn compile -pl yudao-module-qcreport -am -q` exit 0 | | 51 项æµè¯å ¨ç»¿ | `mvn -pl yudao-module-qcreport test` â **51 项 0 失败**ï¼43 â 51ï¼æ°å¢ `PdfPrintOptionsTest` **8 项**ï¼ | | çº¸å¼ å ä½ä¸ HTML åæº | `PdfPrintOptionsTest` æè¨ width/height/margin ç CSS ææ¬ä¸ `PageSizes.resolve` + `Numbers.toString` ç产ç©éåä¸è´ï¼å« A3/A5/Letter 䏿¨ªç«åï¼ | | é¡µèæ¨¡æ¿èªå¸¦å èæ ·å¼ | `PdfPrintOptionsTest` æè¨å« `pageNumber` / `totalPages` å ä½ç¬¦ä¸å è `font-size`ï¼Playwright ç header/footer 模æ¿ä¸è¿æµè§å¨é»è®¤æ ·å¼ï¼ä¸åå èå°±æ¯ 0 å·åçä¸è§ï¼ | | é¡µçæ¨¡æ¿é null | `PdfPrintOptionsTest` æè¨ `HEADER_TEMPLATE` é空å符串 | | `PRINT_CSS` æªæ³è¿æ¸²æå¼æ | `FrontendConformanceTest` ä» 3/3 绿ï¼ä¸å端å»ç»äº§ç©é忝坹ï¼ï¼è¯´æ `BASE_CSS` 没被å¨è¿ | #### 12.3.2 çå® HTTP å±ï¼8 次çå®å¯¼åºè¯·æ±ï¼ å ¨é¨åèª `logs/yudao-server.log` ç `ApiAccessLogInterceptor`ï¼äº¤åæ ¸å¯¹ `qc_report_render_record`ã`system_storage_attachment`ã`system_storage_blob` ä¸ç£çç®å½ï¼ | # | å®ä¾ | åèµ·æ¶å» | èæ¶ | ç»æ | |---|---|---|---|---| | 1 | 22 `QR20260918-0001` | 21:14:17 | **611 249 ms** | â 失败ï¼`java.lang.RuntimeException: Failed to create driver ⦠com.microsoft.playwright.impl.driver.Driver.createAndInstall` | | 2 | 22 `QR20260918-0001` | 21:21:28 | **628 741 ms** | â æåï¼é¦æ¬¡æåï¼Playwright 馿¬¡ä½¿ç¨è¦è£ driver + Chromiumï¼æä»¥æ ¢å° 10 åéï¼ | | 3 | 23 `QR20260918-0002` | 21:32:13 | 921 ms | â æå | | 4 | 23 `QR20260918-0002` | 21:32:14 | 904 ms | â æåï¼éå¤å¯¼åºï¼ | | 5 | 23 `QR20260918-0002` | 21:32:15 | 871 ms | â æåï¼éå¤å¯¼åºï¼ | | 6 | 24 `QR20260918-0003` | 21:32:16 | 868 ms | â æå | | 7 | 25 `PG20260918001` | 21:34:20 | 1 316 ms | â æåï¼3 页䏿 PDFï¼ | | 8 | 26 `ZZ-DIRTY-HTML-TEST` | 21:40:22 | 47 ms | â 失败ï¼å®ä¾ `render_html` 为空 â `REPORT_INSTANCE_HTML_MISSING`ï¼**å¨ try ä¹å**æï¼æ æªå渲æè®°å½ | - **æ´æ¡é¾è·¯èµ°éäº**ï¼é´æ â åæ°ç»å® â åå®ä¾ â åçæ¬ â Playwright å¯å¨ â æå° â å blob â å æ§éä»¶ â ç»æ°éä»¶ â 忏²æè®°å½ã - **å·çå¯å¨èæ¶å·® 3 个æ°é级**ï¼é¦æ¬¡æå 628 741 msï¼è£ driver + Chromiumï¼ï¼æ¤å稳å®å¨ **47â1 316 ms**ãé¨ç½²æ¶è¥æè¿æ®µç®è¿è¯·æ±è¶ æ¶ï¼åç«¯é£æ¡ axios ä¸ç½å ³è¶ æ¶é½è¦æ¾å®½ï¼å端æ¬è½®ç AI 飿¡å·²åç¬æ¾å®½å° 180 ç§ï¼PDF è¿æ¡åæ ·éè¦ï¼ã - **渲æè®°å½ç¡®å®åå ¥äº**ï¼`qc_report_render_record` 7 è¡ãå¤±è´¥é£æ¬¡ï¼#1ï¼çäºå®æ´å¼å¸¸æ ï¼`browser_status` 宿µè§æµå° `RESTARTED`ï¼#2ï¼æµè§å¨å®ä¾æ¯éå»ºåºæ¥çï¼ä¸ `READY`ï¼#3â#7ï¼ã#8 æ è®°å½æ¯**è®¾è®¡å¦æ¤**ï¼æ ¡éªå¨ try ä¹å¤ï¼ã - **渲æè®°å½çææ¥ä»·å¼è¢«çå®éªè¯**ï¼`Drivers.createAndInstall` è¿æ¡æ å°±æ¯é 渲æè®°å½è¡¨æ¿å°çï¼å¦ååªæä¸è¡ 500 æ¥å¿ã #### 12.3.3 è§è§å±ï¼3 页䏿 PDF å·²é页çè¿ï¼ å®ä¾ 25ï¼`PG20260918001`ï¼æ¨¡æ¿ 11ï¼æ¯ä¸é¨é çè§è§ fixtureï¼60 è¡è¡¨æ ¼ + å åµçº¢ç»¿æ¸åå¾ï¼å¯¼åºåå为 `D:/qcl-tmp/verify-60.pdf`ï¼87 842 åèï¼ï¼é页æªå¾ `pdf-60-p1.png` / `pdf-60-p2.png` / `pdf-60-p3-full.png`ã | ç»è®º | è¯æ® | |---|---| | 䏿æ£å¸¸ | æ é¢ãæ¥ææ£éªæ¥åï¼60é¡¹Â·è·¨é¡µéªæ¶ï¼ãä¸è¡¨å 䏿å为ç¢éå¯è¯»ææ¬ï¼æªåºç°æ¹å/ä¹±ç ï¼æ¬æº Chrome + 微软é é»ï¼ | | èæ¯è²/å¾çæåºæ¥äº | å 嵿¸åå¾å¨ PDF éå¯è§ â `print-color-adjust: exact` çæ | | 跨页ä¸é¡µè | 3 页ï¼è¡¨æ ¼ä»ç¬¬ 2 页溢å°ç¬¬ 3 页ï¼é¡µèé¡µç æ£å¸¸åºç° | | â **é¿è¡¨ä¼æé¦é¡µé¡¶ç©º** | `PRINT_CSS` é `table { break-inside: avoid }` ä½ç¨äºæ´å¼ è¡¨ï¼æµè§å¨ä¸ºäºãä¸åæè¡¨æ ¼ãæ 60 è¡è¡¨æ´ä½æ¨å°ç¬¬ 2 页 â **第 1 é¡µåªææ¥å头åä¸å¼ å¾ï¼ä¸é¢å¤§ç空ç½**ãè¿æ¯å·²è§æµå°çæç缺é·ï¼ä¸æ¯æ¥éï¼ï¼é¿æè¡¨æ ¼æ¥åéè¦æ `table` ä» `break-inside: avoid` ååéææï¼åªä¿ç `tr, td, th, img` | | â **页çä¿®å¤æ²¡èµ¶ä¸è¿æ¬¡å¯¼åº** | 第 1 页页ç仿¯ Chromium é»è®¤çã`2026/9/18 21:34` + ææ¡£æ é¢ãã`PdfPrintOptions` ç `HEADER_TEMPLATE = "<div></div>"` åäº **21:36**ï¼èæå䏿¬¡å¯¼åºæ¯ **21:34:21** ââ å³ä¿®å¤**å°æªè¢«éªè¯è¿**ï¼è§ 12.4ï¼ | #### 12.3.4 彿¡£å¹çæ§ï¼å®ä¾ 23 è¿å¯¼ 3 æ¬¡ï¼ | ç»è®º | è¯æ® | |---|---| | åºéä¸ç´¯ç§¯çæéä»¶ | `system_storage_attachment` ä¸ `record_id=23` æ 3 è¡ï¼ä½**å两è¡ä¸ç¬¬ 3 è¡å ¨é¨ `deleted=1`**ï¼é»è¾å é¤ï¼â 任䏿¶å»çæçåªæ 1 æ¡ | | æ§ blob 被çå ï¼ä¸æ¯ç´¯ç§¯ï¼ | `system_storage_blob` 6 è¡å ¨ä¸º `deleted=1`ï¼ç£çç®å½ `D:/uploads/2026/0918/` **为空** â ãåæ° blob â å æ§éä»¶ï¼çº§èå æ§ blob ä¸ç£çæä»¶ï¼â ç»æ°éä»¶ã䏿¥ç¡®å®æä¸ä¸ä»½ PDF æ¸ æäº | | éå¤å¯¼åºçè¯ä¹è¢«å®æµç¡®è®¤ | ä¸ `docs/qc_report_instance_frontend_integration.md` ç表述ä¸è´ï¼**ä¸ä¼è¶å¯¼è¶å¤ãä¹ä¸ä¼çå¤å¿æä»¶** | | æ¸ çå½»åº | å®ä¾ 22â26ãæ¨¡æ¿ 5/11 å已软å ï¼ç£çæ æ®ç | > **ä¸åº¦è¢«æè¯¯å¤**ï¼åªæè¡æ°çä¼ä»¥ä¸ºãå®ä¾ 23 çä¸ 3 æ¡éä»¶ + 3 个 blob = 累积 bugããå®é æ¯æ¼çäº `deleted` ä½ ââ yudao ç `deleteByIds` æ¯é»è¾å é¤ï¼è¡è¿å¨ãä½å·²å¤±æï¼ç©çæä»¶å·²ä»ç£çå é¤ã**æ¥ yudao çä»»ä½å æ¹ç»æé½å¿ é¡»æ¾å¼ç `deleted`**ï¼å¦åä¼æè½¯å è¯¯è¯»ææ®çã ### 12.4 â å·²éªè¯ / æªéªè¯ï¼æ¬èçéç¹ï¼ **å·²éªè¯**ï¼æå®æµè¯æ®ï¼å¯ä»¥è¯´ãå¯ç¨ãï¼ï¼ - æ´æ¡ HTTP é¾è·¯ï¼8 次çå®è¯·æ±ã7 æ¡æ¸²æè®°å½ï¼æå/失败两æ¡è·¯å¾é½èµ°è¿ï¼12.3.2ï¼ã - æµè§å¨å¯å¨å¤±è´¥ç**ç宿¥éä¸çç**ï¼#1ï¼611 249 msï¼`Drivers.createAndInstall` æ è¿äºæ¸²æè®°å½è¡¨ï¼ã - 空 `render_html` çæç»è·¯å¾ï¼#8ï¼47 msï¼ã - ç¨³å®ææ§è½ï¼47â1 316 msï¼å·å¯å¨ 628 741 msã - `.doc` ä¹å¤ç**ä¸ææ¸²æãèæ¯è²/å¾çæå°ã跨页ã页è页ç **ï¼12.3.3ï¼ã - 彿¡£ä¸æ¥é¡ºåºçå¹çæ§ï¼12.3.4ï¼ã **æªéªè¯**ï¼**ä¸è½è¯´ãå¯ç¨ã**ï¼ï¼ - **`HEADER_TEMPLATE = "<div></div>"` ä¿®å¤å°æªå¤éªã** 代ç å·²ç¼è¯ãåæµåªæè¨ãé空ãï¼æå䏿¬¡å¯¼åºï¼21:34:21ï¼ç PDF 第 1 页**ä»ç¶å¸¦ç Chromium é»è®¤é¡µç**ï¼èä¿®å¤åäº 21:36 ââ æ²¡æä¸æ¬¡å¯¼åºæ¯å¨ä¿®å¤ä¹åè·çã**è¿æ¯æ¬è½®æè¯¥å è¡¥ç䏿¡**ï¼éå¯åç«¯å¯¼ä¸æ¬¡ PDFï¼ç第 1 页左ä¸è§æ¯å¦è¿ææ¥æã - **`yudao.qcreport.pdf.enabled=false â RENDER_PDF_DISABLED(1_070_103_008)` æªå®æµ**ï¼éä¸´æ¶æ¹é ç½® + éå¯ï¼ã - **æå°å¤±è´¥ / åæ¡£å¤±è´¥ä¸¤æ¡éè¯¯åæ¯æªå®æµ**ï¼`1_070_103_005/006`ï¼ãæ¥æå¸¦äºå®é åå ï¼ä½æ²¡äººè·è¿ã - **å¹¶åé¸ï¼`concurrency=2`ï¼ä¸æå°è¶ æ¶ï¼30sï¼æªåè¿**ã`BrowserManager` æ Semaphoreï¼ä½æ²¡åå¹¶åæµè¯ï¼æéè¶ 30 ç§ç `1_070_103_007` 仿ªè§¦åè¿ã - **`regenerate` ä½åºå·²å½æ¡£ PDF** æªå®æµï¼`deleteAttachmentsByRecord` çè°ç¨ä½ç½®å·²æ ¸å¯¹ï¼`deleteInstance` 飿¡æ¸ çèµ°éå¹¶çäºçï¼ã - **é¿è¡¨çé¦é¡µçç½**ï¼12.3.3ï¼å·²è§æµãæªä¿®ã > **åç¡®ç说æ³**ï¼PDF åºä»¶**代ç ãåæµãçå® HTTP é¾è·¯ä¸è§è§ç»æé½å·²éªè¿**ï¼å©ä¸çæ¯**页çä¿®å¤å¤éª**ä¸ä¸æ¡æªè§¦åçéè¯¯åæ¯ã > ä¸è¦æã12.3.4 ç䏿¥é¡ºåºéªè¯è¿ãæ¨å¹¿æãææå¹¶å/è¶ æ¶è·¯å¾é½éªè¿ãã ### 12.5 ä¸ä¸æ¥ 1. **å¤éªé¡µçä¿®å¤**ï¼æçäºç䏿¡ï¼ï¼éå¯å端 â 坼䏿¬¡ PDF â ç第 1 页左ä¸è§æ¯å¦è¿æ `2026/9/18 â¦` ä¸ææ¡£æ é¢ã 2. **ä¿®é¿è¡¨é¦é¡µçç½**ï¼æ `PdfPrintOptions.PRINT_CSS` ç `table` ä» `break-inside: avoid` ååéææï¼ä¿ç `tr, td, th, img`ï¼ï¼å坼䏿¬¡ 60 è¡ fixture 确认第 1 页ä¸å空ã**注æ `PdfPrintOptionsTest` ä¸å端å»ç»äº§ç©é½ä¼åå½±åï¼æ¹åå 确认 `PRINT_CSS` ä¸è¿ `BASE_CSS` è¿æ¡è¾¹ç没被碰ã** 3. **è¡¥ä¸æ¡æªè§¦åçéè¯¯åæ¯**ï¼`pdf.enabled: false` â `1_070_103_008`ï¼å¹¶å/è¶ æ¶ï¼`1_070_103_007`ï¼å åï¼å档失败ï¼`1_070_103_006`ï¼ã 4. **`regenerate` ä½åºå·²å½æ¡£ PDF** 宿µä¸æ¬¡ã 5. **é¨ç½²æ³¨æé¡¹åè¿äº¤ä»è¯´æ**ï¼åç«¯é£æ¡ axios ä¸ç½å ³è¶ æ¶é½è¦ ⥠å·å¯å¨é£æ¬¡ï¼628 741 ms + ä½éï¼ï¼å¦å馿¬¡å¯¼åºå¿ 被å端ææã ## åä¸ãAI å¯¼å ¥ï¼Phase 4 · è稿æï¼ââ â çå® HTTP èè° **54/54** + 设计å¨ãä¿åâå·æ°ãå¾è¿éªæ¶éè¿ï¼é¡ºå¸¦å¤éª PDF 页çä¿®å¤ > ç®æ ï¼ä¸ä¼ ä¸ä»½å·²æçæ£éªæ¥åæä»¶ï¼æ«æä»¶/ç §çãçµåç PDFãWord/Excelï¼â AI 读æ â 产åº**模æ¿è稿** â > 人工å°è®¾è®¡å¨ç¡®è®¤åæä¿å为æ£å¼çæ¬ã**è¿æ¯ç¨æ·æç¡®è¦æ±è¡¥ä¸çè½åã** ### 13.1 交ä»ç© | å± | æä»¶ | èè´£ | |---|---|---| | **AI æ¨¡åæ¶å£**ï¼è·¨æ¨¡åï¼ | `yudao-module-ai` ç `AiAutoConfiguration` / `YudaoAiProperties` / `AiModelFactory(+Impl)` / `AiModelServiceImpl` | å `timeout`ï¼é»è®¤ 60sï¼ï¼æ `AiModelDO.temperature` / `maxTokens` **çæ£è¯»è¿å»**ï¼æ¤å声æäºä½å ¨é¡¹ç®æ ä¸å¤è¯»åï¼ï¼**cacheKey ææ°åæ°ä¸èµ·ç®è¿ key**ï¼å¦åæ¹äº DB éç temperature ä¼éé»è¿åæ§æ¨¡å | | è¾å ¥éé å¨ | `service/aiimport/document/`ï¼`QcReportImportAdapter` / `QcReportDocumentExtract` / `QcReportDocumentExtractService` / `ImageImportAdapter` / `PdfImportAdapter` / `OfficeImportAdapter` | ä¸ç±»è¾å ¥ç»ä¸äº§åºãææ¬éé æ å¾çééãçä¸é´ç»æã`@Order` åºå® **Image â Pdf â Office**ï¼æ¶é¤ãpdf åæ¶è¢« Pdf ä¸ Office å¹é ãè¿ç±»æ§ä¹ | | çº¯å½æ°å± | `service/aiimport/llm/`ï¼`QcReportTemplatePromptBuilder` / `QcReportAiDraftParser` / `QcReportAiDraftNormalizer` / `QcReportLlmCallPlanner` / `QcReportLlmCall` | æ¼ promptãåçè§£æãå½ä¸è¿æ»¤ã**è°ç¨è®¡åæ½æçº¯å½æ°** | | å¥çº¦ | `controller/admin/aiimport/` + `vo/`ï¼`QcReportAiDraftReqVO` / `QcReportAiDraftRespVO` / `QcReportComponentSpecVO`ï¼ | `POST /qc-report/ai-import/draft`ï¼æéç `qc-report:template:ai-import` | | ç¼æ | `service/aiimport/QcReportAiImportService(+Impl)` | æ ¡éª â 读 blob åè â éæä»¶èµ°éé å¨ â æ§è¡è°ç¨è®¡å â è§£æå½ä¸ â åå¹¶å»é â è¿åã**å ¨ç¨ä¸è½åº** | | é ç½® | `config/QcReportAiImportProperties.java` + 两个 profile | `enabled` / `maxFiles`(3) / `maxPagesPerFile`(5) / `maxPagesPerRequest`(8) / `maxFileSizeMb`(20) / `maxComponents`(200) / `pdfTextPageThreshold`(30) / `renderDpi`(200) / `maxXlsxRows`(300) | | é误ç | `enums/ErrorCodeConstants.java` æ°å¢ `1_070_104_000`ï½`014` | 15 æ¡ï¼æ¯æ¡æ `error-message-precision.md` ç»ã维度 + åæ¹å®é å¼ + 坿§è¡å¨ä½ã | | æºæä»¶å½å± | `yudao-module-system` ç `StorageRecordTypeEnum` å `QC_REPORT_TEMPLATE` | 类注éåçãæéæ©å±ãï¼æ¯å®æ¹ççæ©å±ç¹ï¼`getByType` 对æªç¥ç±»åæ `IllegalArgumentException`ï¼ä¸å å°±æ¯ 500 | | è¿å¸¦ | `QcReportTemplateServiceImpl.deleteTemplate` | å `deleteAttachmentsByRecord(QC_REPORT_TEMPLATE, id)`ï¼å¦å模æ¿å é¤åéä»¶ä¸ç£çæä»¶æ°¸ä¹ å¤å¿ | | **åç«¯è£ é å¨**ï¼æ¬è½®æå¤§ç¼ºå£ï¼ | `mom-pro2-before/src/components/quality/core/assembler.ts` | **æ¤åè¯ä¹å±æ¯ãåªåçã**ï¼åªè½ä»ç»å¸æ¶éåºæ¥ï¼ä¸è½åè£ åå»ãè¿ä¸ªæä»¶æ¯ `collectSchema` ççº¯å½æ°éè¿ç® | | åç«¯ç§¯æ¨æ¸ å | `.../core/catalog.ts` | 注å表 â åç» LLM çæ´¾çè§å¾ï¼å¥æ icon/buildContent/validateï¼ï¼çº¯å½æ° | | å端æ¥å ¥ | `api/mes/qc/report/ai/index.ts` / `designer/modules/ai-import-modal.vue` / `designer/index.vue` / `use-designer.ts` | API å®ä¹ãå¼¹çªãé¡¶æ æé®ã`loadCanvasFromDraft` | | èè°æ¹æ¡ | `docs/qc_report_ai_import_frontend_integration.md` | æ `frontend-docs.md` 强å¶è¦æ±äº§åºï¼ä¸å«ä»»ä½å端代ç çæ®µ | ### 13.2 å «å¤å»æçåè | åè | çç± | |---|---| | **ç§¯æ¨æ¸ åç±å端é请æ±ä¼ ï¼å端ä¸ç¡¬ç¼ç ** | ç»ä»¶æ³¨å表çå¯ä¸çç¸æ¥æºå¨å端ãå端æä¸ä»½å°±æ¯**第äºä¸ªçç¸æ¥æº**ï¼ä¸æ¯æåçé£ç§ï¼å端å ç»ä»¶/æ¹å¿ å¡«åæ®µæ¶å端ç«å»çæãåç«¯å¯æ¬éé»è¿æï¼æ¨¡åéå³ä¼ç¼åºæ³¨åè¡¨éæ ¹æ¬ä¸åå¨ç `type`ã项ç®å·²å¨ `ReportTemplateSchema.QualityComponentNode` 注ééæç¡®å对è¿ä»¶äº | | **è¾¹çæ²¡æè¢«åå¼±** | æ¸ åè¿ prompt åçå¯ä¸åºå£æ¯ãè¢«æ¨¡åææ JSON å符串ããçæ£æå ³çæ¯**åç«¯è£ é å¨å¯¹ç活注åè¡¨æ¥ `getQualityComponent`**ï¼æªæ³¨åç `type` ä¼è¢«è·³è¿ï¼**æ°¸è¿äº§ä¸åºä»»æ HTML**ãå°±ç®æäººä¼ªé å« `type:'CustomHtml'` çæ¸ åï¼ä¹åªä¼è¢« skip | | **POI ä¸ç¨ Tika** | è¡¨æ ¼çè¡åç»ææ¯æ ¸å¿ä¿¡æ¯ï¼Tika 伿平æçº¯ææ¬ï¼ä¸ `tika-parsers-standard-package` 伿è¿å ç¾ MB è§£æå¨ï¼qcreport æ²¡å¿ è¦è | | **忥è¿åï¼ä¸å弿¥ä»»å¡ + è¿åº¦è½®è¯¢ + ä»»å¡è¡¨** | å·²åç¨æ·æ²å®çæ¹æ¡ãä»£ä»·æ¯æ¥å£æé¿æ 180 ç§ï¼æä»¥åç«¯é£æ¡ axios åç¬æ¾å®½è¶ æ¶ | | **é误ç¨çæ£ç `ServiceException` + åé误ç ** | ERP / CRM å 便é误å¡è¿ `respVO.rawText` ä¸ HTTP 200ï¼è®©å端é ãrawText ææ²¡æå¼ãå æ¢å¤±è´¥ï¼`crm_sale_quotation_ocr_integration.md` é£å¼ 表就æ¯è¿ç§çº¦å®çåç³ï¼ãæ¬æ¹æ¡å»æå离 | | **å¤é¡µé页è¯å«åæ `(type, props)` å»éåå¹¶** | 页çãæ é¢ã表头å¨å¤é¡µéå¤åºç°æ¶åªç第ä¸ä»½ãä¸é 5 页/æä»¶ã8 页/è¯·æ± | | **ç»å¯¹ä¸å¤å¶ ERP/CRM ç `tempFile.delete()` 缺é·** | `ErpPurchaseInvoiceAiServiceImpl` ä¸ `CrmSaleQuotationAiServiceImpl` å¨ `finally` éå çæ¯ `getPublicFile` è¿åç**çå®åå¨ blob æä»¶æ¬èº«**ï¼å³å ç¨æ·ççå®ä¸ä¼ æ°æ®ãæ¬è½®ä¸ç¢°å®ä»¬ï¼ä½ä¹ç»ä¸å¤å¶ã建议åç¬å¼ä¿®å¤é¡¹ï¼ä¸å¤åæï¼ | | **å®å ¨è§£æä¸åºæ¶ç´æ¥æéï¼ååºé䏿¾ `rawText`** | å¡ä¸ä¸ªæä¸º null çåæ®µåªä¼è®©åæ¥äººä»¥ä¸ºãè§£æå¤±è´¥æ¶åç«¯è½æ¿å°æ¨¡ååæããæ¨¡ååæåè¿äºæå¡ç«¯æ¥å¿ | ### 13.3 宿µè¯æ® | ç»è®º | è¯æ® | |---|---| | **ç¼è¯éè¿** | `mvn compile -pl yudao-module-qcreport -am -q` exit 0 | | **93 项æµè¯å ¨ç»¿** | `mvn -pl yudao-module-qcreport test` â **93 项 0 失败**ï¼51 â 93ï¼æ¬è½®æ°å¢ **42 项**ï¼éé å¨ 13 + å½ä¸ 11 + è§£æ 9 + è°ç¨è®¡å 3 + æç¤ºè¯ 6ï¼ï¼éæ¡æ¥èª `target/surefire-reports/*.txt` | | **è£ é å¨ä¸ `collectSchema` äºä¸ºéè¿ç®ï¼æºå¨è¯æï¼** | `.qc-conformance/assemble-ts.ts`ï¼`npx tsx` ç´è·ï¼**9 ç»ã约 60 æ¡æè¨å ¨è¿**ï¼12 个ç»ä»¶å ¨é¨è£ é æåï¼æ¯ä¸ªé¡¶å±èç¹é½å¸¦ `data-quality-type` ä¸**é¡¶å±ä¸å« `data-qc-repeat`/`repeat-row`**ï¼**`attributesToProps â assemble === resolveQualityProps`ï¼12/12 å¾è¿æ æï¼**ï¼æ¯ä¸ªå段ç `data-qc-<key>` **ç¡®å®åè¿äºç»å¸**ï¼é²æ¢é ä¸¤è¾¹åæ¶åé»è®¤å¼èå绿ï¼ï¼boolean `false/true` â `'false'/'true'`ï¼`Divider.thickness` æ°å 3 â `'3'` â æ°å 3ï¼enum æ°å弿¶ææå符串 `'3'`ï¼ä¸æ ¡éªã`buildContent` ç `String()`/`Number()` æ¯è¾å£å¾ä¸è´ï¼ï¼`CustomHtml` + 空 type è¿ `skipped` ä¸ **`alert(1)` 没æ¸è¿ç»å¸æ°æ®**ï¼ç»æå屿§åè½é»è®¤å¼å¹¶åäº§ä¸æ¡ problemï¼å¿ 填缺失ï¼`itemsPath: ''`ï¼äº§ problemãéå¿ å¡«ï¼`title: ''`ï¼ä¸äº§ï¼ç©º/undefined è¾å ¥ä»è¿åé空 `pages`ï¼æ¸ å 12 项ãåªå« `category/fields/label/type` å个 keyã**æ `buildContent`/`icon`/`validate`/`defaults` æ³æ¼** | | **åç«¯é¶æ°å¢ç±»åé误** | `vue-tsc --noEmit`ï¼å¸¦ `NODE_OPTIONS=--max-old-space-size=8192`ï¼ï¼æ¹å **316** â æ¹å **316**ï¼typecheck æ¥å¿é `catalog.ts` / `assembler.ts` / `components/quality` **é¶å½ä¸** | | **è®¾è®¡å¨ UI æ¥å ¥ + å ¨é¾è·¯å¾è¿** | éå¯å已走å®**宿´é¾è·¯**ï¼ä¸ååªå°å端边çï¼ï¼è§ 13.5 ââ 11 å¼ æªå¾ `ai-02-designer.png` ï½ `ai-11-published.png`ï¼å«å·æ°å**è¯ä¹ç¾åéåä¸è´**ä¸å叿å | | **AI 模åè¶ æ¶æ¶å£æªç ´åæ¢æè°ç¨** | åªæ¹äº 1 个è°ç¨ç¹ï¼`AiModelServiceImpl:101`ï¼æä¸æ£æ `AiModelDO`ï¼ï¼ERP/CRM/MES ä¸å¤ AI è°ç¨èµ°åä¸å·¥åï¼ä¸èµ·è·å¾è¶ æ¶ä¸ `maxTokens` | | **ä¸å¡å¯è§åçä¸ä»½ mmd å¢éè¯æ³ææ** | æ¬æºæ `mmdc`ãå端 `node_modules` é乿²¡æ mermaidï¼æ ç¨æ 头 Chromium è½½å ¥ **mermaid 11ï¼ä¸ `index.html` åä¸ä¸ª jsDelivr CDN çæ¬ï¼** 对æ¹å¨åçä¸ä»½æä»¶è· `mermaid.parse()`ï¼`05-quality-flow` / `09-data-flow` / `11-ai-business-flow` **å ¨é¨ `ok: true`**ãè¿ä¸æ¥è¡¥ä¸äºæ¤ååªåãç¼è¾ + `sync_index.js` éåãèæ²¡åè¯æ³æ ¡éªçç¼ºå£ | **éªè¯è¿ç¨ä¸çº æ£çåå¤è¯¯å¤**ï¼è®°ä¸æ¥å å¾å人é踩ï¼ï¼ 1. **`è稿 0 / èç¹ 0`**ï¼`listQualityComponents()` è¿å 0 ââ æ³¨å表**åªææ¾å¼è°ç¨ `registerAllQualityComponents()` ææå 容**ï¼è®¾è®¡å¨å¨ `init` æ¶åï¼ï¼å¯¼å ¥ barrel ä¸ä¼æ³¨åä»»ä½ä¸è¥¿ã两å¤ä¿®å¤ï¼ç» `buildQualityComponentCatalog()` å 空注å表**ç´æ¥æé**çå®å«ï¼æèªå·±çæµè¯èæ¬å°±è¸©äºè¿ä¸ªåï¼è¯´æè¿ä¸ªåºæ¯æ¯çå®çï¼ï¼å¹¶å¨å¯¹æèæ¬éè¡¥ä¸æ³¨åè°ç¨ã 2. **`number 3 è¿å为æ°å 3` æ¥ `string 3`**ï¼æè¯¯ä»¥ä¸º `Heading.level` æ¯æ°ååæ®µãå®é 宿¯ `type: 'enum'` ä¸**é项弿¯æ°å** ââ æä»¥å¾è¿æå符串 `'3'` æ¯**æ£ç¡®ç**ï¼æ ¡éªç¨ `String()` æ¯å¯¹ã`buildContent` ç¨ `Number()`ï¼ã**è£ é 卿¯å¯¹çï¼æçæµè¯æ¯éçã** å·²ææ°åå¾è¿æ¹ç¨ `Divider.thickness`ï¼ç¡®è®¤æ¯çæ£ç `type: 'number'`ï¼ï¼enum æ¹ä¸ºæè¨ `String(back.level) === '3'`ã 3. **`åå ç´ ä¸å½±åå ¶ä½ï¼4 项ä¸è£ è¿ 3 项` å®é æ¯ 2**ï¼ææ°éäºã`QualityTable` / `CustomHtml` / `Heading` / `{type:''}` å项é两项被 skipï¼æä»¥è£ è¿ **2** 项 ââ `skipped.length === 2` æ©å·²ç¡®è®¤è¿ä¸ç¹ã顺带修æäºä¸å¤è¶çåå¼å¯¼è´ç `TypeError`ã 4. **`å¿ å¡«ç¼ºå¤±äº§ problem` å¾å° `[]`**ï¼æç¨äº `QualityTable.props.title`ï¼å®**䏿¯å¿ å¡«**ãæ¹ç¨ `itemsPath`ï¼å¯ä¸å¿ å¡«åæ®µï¼ï¼å¹¶è¡¥äºä¸æ¡ãéå¿ å¡«ç空ä¸äº§ problemãçå¯¹ç §æè¨ã ### 13.4 çå® HTTP èè° ââ â **54/54 å ¨ç»¿**ï¼å端éå¯åé¦è·ï¼ èæ¬ `D:/qcl-tmp/ai-import-e2e.mjs`ï¼é£æ ¼ç §æ¢æ `real-canvas.mjs`/`pdf-e2e.mjs`ï¼ç»å½å token â multipart ä¸ä¼ æ¿ `blobId` â `bind` å½å±å°æ¨¡æ¿ â `POST /qc-report/ai-import/draft` â æè¨ï¼ã **两轮**ï¼é¦è½® 46/47ï¼å¯ä¸é£æ¡ FAIL æ¯**æèªå·±ææè¨åéäº**ï¼æãcatalog 为空ã彿 008 å¯è¾¾è·¯å¾ï¼å®é 被 VO ä¸ç `@NotEmpty` å æ¦æ HTTP 400ï¼å±æ¡æ¶æ ¡éªï¼åçï¼ï¼æ¹åæä¸¤æ¡çæ£å¯è¾¾ç 008 è·¯å¾ï¼41 é¡¹è¶ ä¸éã缺 `type`ï¼å**第äºè½® 54/54**ã | ç¨ä¾ | å ³é®æè¨ | 宿µ | |---|---|---| | çµåç PDF | `code=0`ãç»ä»¶é空ã`type` â 12 å ç½® | â | | æ«æç PDFï¼3 é¡µï¼ | åä¸ + **å¤é¡µå»éçæ** | â ï¼è§ä¸ï¼ | | åé¡µæ«æå¾ç | åä¸ | â | | Word å«è¡¨æ ¼ | åä¸ï¼è¡¨æ ¼è¡åæªæå¹³ | â | | Excel æ£éªé¡¹ç® | åä¸ | â | | çº¯ææ¬ / CSV | åä¸ | â | | æ§ç `.doc` | `1_070_104_003` 䏿æ¡å«ãå¦å为 .docxã | â | | åæ°ç±»é误 6 æ¡ | 001 / 005 / 008Ã2 / 009 / 400 | â 6/6 | **ç宿¨¡åè°ç¨å»¶æ¶**ï¼å®æµ `durationMs` è½å¨ **2 786 ï½ 13 642 ms**ï¼éæä»¶é¡µæ°ä¸ééï¼ææ¬ vs è§è§ï¼æµ®å¨ï¼ä¸èæ¬ä¾§ wall clock å»å ââ 说æç¡®å®æ¯æå°äº DashScopeï¼ä¸æ¯ç¼åæçè·¯ã **å¤é¡µå»éï¼`scan-3page.pdf`ï¼ç warnings åæ**ï¼ä¸¤æ¡é½å¨ï¼ ``` æä»¶ãscan-3page.pdfãæ²¡æææ¬å±ï¼å ± 3 页ï¼ï¼å·²ææ«æä»¶é页è¯å«ï¼ä¼æ¯è¾æ ¢ å¤é¡µè¯å«å ±å¾å° 15 项ç»ä»¶ï¼å ¶ä¸æ 6 项å¨å ¶å®é¡µå·²åºç°ï¼é¡µçãæ é¢ã表头çéå¤å 容ï¼ï¼å·²åå¹¶ ``` å³ **15 â 9**ï¼å»éæ `(type, props)` çæï¼ä¸æ¯åªå卿³¨ééçæ¿è¯ºã **ãAI 产åºåªä½èç¨¿ãæ DB è¯æ®**ï¼å ¨é¨ AI è°ç¨è·å®ï¼`qc_report_template_version` 鿍¡æ¿ 5 **ä»ç¶åªæ v1.0 / v1.1 两è¡**ï¼æ²¡æä»»ä½ä¸è¡æ¯ AI ç´æ¥åè¿å»çãè½åºåªåçå¨äººå·¥ç¹ãä¿åãæ¶ã **`type` è¶çç为 0**ï¼æ¯ä¸ªç¨ä¾é½æè¿åç `type` éåä¸ 12 个å ç½® type æ±å·®éï¼**æ ä¸è¶ç**ï¼`summary` å为éé¡ºä¸æï¼ä¾ï¼ãæ¬æä»¶ä¸ºåºè´§æ£éªæ¥åï¼å 嫿¥åå¤´ãæ£éªé¡¹ç®è¡¨æ ¼ä¸ç»è®ºãï¼ã ### 13.5 设计å¨ãçæå°ç»å¸ â ä¿å â å·æ°ãå¾è¿éªæ¶ ââ â æ¬è½®çæ£çéªæ¶ç¹ ç¨ Playwright MCP ææ´æ¡é¾è·¯èµ°å®å¹¶éæ¥æªå¾ï¼`.playwright-mcp/ai-02-designer.png` ï½ `ai-11-published.png`ï¼ï¼ | æ¥éª¤ | ç»æ | |---|---| | è¿è®¾è®¡å¨ â ç¹é¡¶æ ãAI å¯¼å ¥ã | æé®ä¸å¼¹çªæ£å¸¸ï¼`ai-02`/`ai-03`ï¼ | | éå ¥ç宿«æä»¶ â ãå¼å§è¯å«ã | 5 个ç»ä»¶ï¼**4.5 s**ï¼`ai-04`/`ai-05`ï¼ | | ãçæå°ç»å¸ã | ç»ä»¶æåºåºç°ï¼å±æ§é¢æ¿è¢« `restoreQualityTraits` é建ï¼`ai-06`ï¼ | | **ç¹ãä¿åãâ ä¿å v1.1** | è½åºæåï¼`ai-07`/`ai-08`ï¼ | | **å·æ°é¡µé¢ â åçæ¬ååå** | **è¯ä¹ç¾åéåä¸è´**ï¼`ai-09`ï¼ | | åå¸ v1.1 | toastãçæ¬ v1.1 å·²åå¸ãï¼`ai-10`/`ai-11`ï¼ | > **为ä»ä¹ç¨ãå½ä¸åè¯ä¹ç¾åãè䏿¯è£¸å符串æ¯å¯¹**ï¼GrapesJS çèç¹ `id` æ¯æ¬¡ä¼è¯éæ°çæï¼ä¸¤ä»½å 容ç¸åç Schema å符串ä¸å®ä¸åãæ¯å¯¹åä½å**é¡¶å± `[data-gjs-type="wrapper"] > [data-quality-type]` èç¹åå ¶ `data-qc-*` 屿§éå**ï¼è¿ææ¯ãç»å¸å 容ãçæ£ç¡®è¡¨ç¤ºãå·æ°åå该ç¾å**å®å ¨ä¸è´**ï¼å³è¯æ `collectSchema` â æ°çæ¬è¡ â `loadSchema` å¾è¿æ æï¼åæ¶æè£ é å¨ä¸ `restoreQualityTraits` ä¸èµ·éªäºã 两å¤é带确认ï¼AI çåºççº¸å¼ è¢«å¹¶å ¥ `pageSetting` å¹¶**èªå¨åå°çºµå**ï¼`ReportHeader.reportNo` / `Result.conclusion` ç **`{{...}}` ç»å®å ä½ç¬¦åæ ·ä¿ç**ï¼æ²¡è¢«æ¿æ¢ææ¨¡åçå°çåé¢å¼ã ### 13.6 PDF 页çä¿®å¤å¤éª ââ â éç¯ Â§åäº éç第 1 æ¡ å®ä¾ 30ï¼`QR20260919-0003`ï¼å¯¼åºï¼**1 209 ms**ã**59 809 åè**ã`browserStatus: READY`ãç¨ PDFBox æ½ç¬¬ 1 é¡µçº¯ææ¬ + æ¸²ææ PNG åéç¡®è®¤ï¼ - 第 1 é¡µææ¬**é¦è¡æ¯æ¥åæ£æ**ãæ¥ææ£éªæ¥åãã**æ«è¡æ¯æä»¬èªå·±ç页è**ã第 1 页 / å ± 1 页ãï¼ - **éç¯æ²¡æ** Chromium èªå¸¦çãæå°æ¥æ + ææ¡£æ é¢ã页çï¼ - 渲æå¾ `D:/qcl-tmp/header-check-30-p1.png` èç¼å¤æ ¸ä¸è´ã å³ `PdfPrintOptions.HEADER_TEMPLATE = "<div></div>"` + `.setHeaderTemplate(...)` ç¡®å®çæã ### 13.7 æ¬è½®æ´é²ç 1 个çå®ç¼ºé·ï¼å·²ä¿®ï¼+ 1 个çå®éæ£ï¼æªä¿®ï¼å¦å®è®°å½ï¼ **â 缺é·ï¼å·²ä¿®ï¼ââæ¹æ¡ææ¡£éç `application` å¼ä¸å¨å端æä¸¾å ã** 馿¬¡ `bind` ç´æ¥ **HTTP 500 ç³»ç»å¼å¸¸**ï¼æ¥å¿ï¼ ``` java.lang.IllegalArgumentException: æ æçæä»¶ç¨éç±»å: ai_import_source at cn.iocoder.yudao.module.system.enums.storage.StorageApplicationTypeEnum.getByType(:30) ``` æ ¹å ä¸å¨å端ãä¹ä¸å¨æµè¯ï¼**`StorageApplicationTypeEnum` åªæ `file` / `image` / `avatar` ä¸ä¸ªå¼**ï¼`getByType` 对æªç¥å¼ç´æ¥æï¼ï¼è**æ¹åæ¹æ¡éåçæ¯ `ai_import_source`**ãè¿ä¸ªåé¢å¼æ²¡å¯¹çæä¸¾æ ¸è¿ï¼åªæçè· HTTP æä¼æ´é² ââ åæµå typecheck 齿¦ä¸ä½å®ã 已修 **3 å¤**ï¼`ai-import-modal.vue`ã`docs/qc_report_ai_import_frontend_integration.md`ï¼å¹¶å äºä¸è¡ â 说ææä¸¾éå¶ï¼ãèè°èæ¬ã`recordType` 仿¯æä»¬èªå·±ç `qc_report_template`ï¼ä¸åå½±åã **â¡ éæ£ï¼æªä¿®ï¼ââ对å䏿¨¡æ¿éå¤å¯¼å ¥ä¼çä¸å¤å¿ blob + ç£çæä»¶ã** `StorageFileUtil.saveStorageAttachmentByRecordTypeAndRecordId` çæ¢æè¯ä¹æ¯ã**åªå 餿§çéä»¶å ³èè®°å½ï¼ä¿ç blob è®°å½ï¼æä»¶æ¬èº«ä¸å é¤ï¼**ãã对 AI å¯¼å ¥è¿æ¡æµï¼å«ä¹æ¯ï¼**å䏿¨¡æ¿ç¬¬äºæ¬¡å¯¼å ¥æºæä»¶æ¶ï¼ä¸ä¸ä»½æºæä»¶åæå¤å¿ blobï¼ä¸ç£çæä»¶ä¸ä¼å **ï¼ééå¤å¯¼å ¥æ çå¢é¿ã è¿å¸¦ä¸ä¸ªæ¸ çä¸çåï¼`deleteAttachments(ids)` 乿¸ 䏿ã已被å䏿¬¡ bind 软å ãçéä»¶è¡ ââ å®å é¨èµ° `storageAttachmentMapper.selectByIds(...)`ï¼**带é»è¾å é¤è¿æ»¤**ï¼åä¸å°é£äºè¡èªç¶ä¹å°±åä¸å°å¯¹åº blobãæ¬è½®çä¸´æ¶æ°æ®æåæ¯æå·¥ `UPDATE system_storage_blob SET deleted=1 ...` + `rm` ç£çæä»¶æå¹²åçã **å¤ç½®å»ºè®®ï¼çå¾ ä¸ä¸è½®å®å¤ºï¼**ï¼è¦ä¹è®© AI å¯¼å ¥ãæ¿æ¢åé¡ºå¸¦æ¸ ç被顶æç blobãï¼è¦ä¹æç¡®ææºæä»¶å½**æ£å¼å½æ¡£ç´¯ç§¯**ï¼é£å°±ä¸ç®å¤å¿ï¼ä½è¦æé é¢ä¸èªæ¸ çï¼ã两æ¡è·¯é½éè¦äº§åå£å¾ï¼ä¸å±æ¬è½®èå´ã ### 13.8 â å·²éªè¯ / æªéªè¯ï¼æ¬èçéç¹ï¼ **å·²æ¿å°å®æµè¯æ®ãå¯ä»¥è¯´ãå¯ç¨ãç**ï¼ - å端çå® HTTP é¾è·¯ **54/54**ï¼8 æ ¼å¼ç¨ä¾ + `.doc` ææ¶ + 6 æ¡å ¥åé误ï¼ï¼å«ç宿¨¡åå»¶æ¶ï¼ - **å¤é¡µå»é**ã**`type` é¶è¶ç**ã**AI ä¸è½åº** ä¸é¡¹åæå®è¯ï¼ - è®¾è®¡å¨ **UI å ¨é¾è·¯ + å·æ°å¾è¿æ æ + åå¸**ï¼ - PDF 页çä¿®å¤å¤éªï¼ - ç¼è¯éè¿ã**93 项念**ãè£ é å¨å¯¹æ **约 60 æ¡æè¨**ãå端 **é¶æ°å¢ç±»åé误**ï¼è§ 13.3ï¼ã **仿ªéªè¯çï¼ä¸è½è¯´ãå¯ç¨ãï¼**ï¼ - **`yudao.qcreport.ai-import.enabled=false â 1_070_104_000`** ââ æªå®æµï¼éä¸´æ¶æ¹é ç½® + éå¯ï¼ã - **è¶ æ¶åæ¯ `1_070_104_011`** ââ æªå®æµã`yudao.ai.timeout` ç 60 s ä¸åç«¯é£æ¡ 180 s æ¯**两éç¬ç«çé¸**ï¼é½æ²¡è§¦åè¿ãæçäºçéªæ³æ¯æ `yudao.ai.timeout` ä¸´æ¶æ¹æ `1s`ï¼**没å**ã - **è¶ é¡µï¼`FILE_TOO_MANY_PAGES`ï¼/ åè®¡è¶ é¡µï¼`TOO_MANY_PAGES_TOTAL`ï¼/ è¶ å¤§å°ï¼`FILE_TOO_LARGE`ï¼ä¸æ¡** ââ æªå®æµãåæéãè¶ æä»¶æ°ã已被 `@Size` æ¦æ 400 并宿µå°ï¼ä½å¦å¤ä¸æ¡æ²¡é è¿è¶ é fixtureã - **ãæ¨¡æ¿å 餿¸ éä»¶ãçè¿å¸¦æ¹å¨æªå®æµ** ââ 代ç å¨ï¼`deleteTemplate` éå äº `deleteAttachmentsByRecord`ï¼ï¼**没è·è¿**ã - **没å `@Disabled` ççå®èè°æµè¯**ï¼è®¡åéæå°å¯ä»¥åä¸ä¸ªé»è®¤ä¸è·çï¼ââ æ¬è½®**没å**ã - **AI çè¯å«ãåç¡®çãæªè¢«åº¦é** ââ æä»¬éªçæ¯ãé¾è·¯éãç»æåæ³ã`type` ä¸è¶çãï¼**䏿¯**ãè®¤åºæ¥çç»ä»¶ä¸åä»¶é项å»åããè¿ä»½å¤æéè¦äººå·¥å¯¹ç §ç宿¥åï¼æ¬è½®åªåå°äººå·¥çäºæªå¾è§å¾åçã - **Linux é¨ç½²ä¸æªéª** ââ AI å¯¼å ¥ä¸ä¾èµæµè§å¨/åä½ï¼ä¸ Phase 3 ä¸åï¼ï¼é¢ææ é£é©ï¼ä½**æªéª**ã > **åç¡®ç说æ³**ï¼AI å¯¼å ¥**å端 + å端 + çå®é¾è·¯å ¨é**ï¼æ ¸å¿å£å¾ï¼åªä½è稿ãå¤é¡µå»éã`type` ä¸è¶çï¼é½æå®æµè¯æ®ã > ä½**两个é ç½®å¼å ³åæ¯ã䏿¡è¾å ¥ä¸éãæ¨¡æ¿å 餿¸ çãè¯å«åç¡®ç**ä»å±æªéªè¯ ââ > ä¸è¦æã54/54 éè¿ã说æãææéè¯¯åæ¯é½éªè¿ãï¼é£äºæ°ååªè¦çäºå·²æé çç¨ä¾ã ### 13.9 ä¿çæ°æ®ä¸åæ»è¯´æ - **ææä¿ç**ï¼æ¨¡æ¿ 5ï¼`QC_OQC_UI` åºè´§æ£éªæ¥å模æ¿ï¼ç **v1.1**ï¼schema 22 541 å符ï¼**å·²åå¸**ä¸ `current_version = v1.1` ââ è¿æ¯ UI å¾è¿éªæ¶ç产ç©ï¼çç让åè½å¯æ¼ç¤ºãå®åå æ¯ä¸ä»½**空ç»å¸è稿**ï¼æä»¥**å®å ¨å¯åæ»**ï¼æ v1.1 åç¨ / å æï¼`current_version` åå v1.0 å³å¯ï¼ã**å¦éè¿å以便éåéªæ¶ï¼è¯´ä¸å£°å³å¯ã** - **å·²æ¸ çå¹²å**ï¼å®ä¾ 9 è¡å ¨é¨è½¯å / **0 åæ´»**ï¼`system_storage_blob` 19 è¡ / **0 åæ´»**ï¼`system_storage_attachment` 17 è¡ / **0 åæ´»**ï¼`D:/uploads/2026/0918/` ä¸ `D:/uploads/2026/0919/` **å为空**ã - **ææä¸å **ï¼`qc_report_render_record` 10 è¡ ââ `deleteInstance` ææä¸å 渲æè®°å½ï¼å®¡è®¡ççï¼ã - **䏿¡å·²éç¼è¯ãå¾ éå¯çæçææ¡å¾®è°**ï¼`ErrorCodeConstants.AI_IMPORT_FILE_LEGACY_OFFICE` çæ¨¡æ¿é `.{}` åå¤äºä¸ä¸ªç©ºæ ¼ï¼åä¸å¥éç `.docx`/`.xlsx` 齿²¡æï¼ï¼æ¸²ææãæ§ç Office æ ¼å¼ **.doc**ãï¼å·²å»æç©ºæ ¼ã`mvn compile -pl yudao-module-qcreport -am -q` **EXIT=0**ï¼**éå¯å端åçæ**ï¼æªçæä¸å½±ååè½ã ### 13.10 ä¸ä¸æ¥ï¼æä¼å çº§ï¼ 1. **éå¯å端**ï¼è®© `.doc` ææ¡é£å¤å¾®è°çæï¼ââ `mvn compile -pl yudao-module-qcreport -am -q` **å·²è·ãEXIT=0**ï¼åªå©æå·¥éå¯ã 2. **è¡¥ä¸ä¸ªé ç½®/éå¶åæ¯ç宿µ**ï¼é½è¦ä¸´æ¶æ¹é ç½® + éå¯ï¼ï¼`ai-import.enabled: false` â `1_070_104_000`ï¼`yudao.ai.timeout: 1s` â `1_070_104_011`ï¼è¶ å¤§å° fixture â `1_070_104_004`ã 3. **宿µãå æ¨¡æ¿é¡ºå¸¦æ¸ éä»¶ä¸ blobã**ï¼å»ºä¸ä¸ªä¸´æ¶æ¨¡æ¿ â å¯¼å ¥ä¸æ¬¡æºæä»¶ â å æ¨¡æ¿ â å读ä¸å¼ 表 + ç£çç®å½ã 4. **å®å¤º 13.7 â¡ çå¤å¿ blob å¤ç½®**ï¼æ¸ ç vs 彿¡£ç´¯ç§¯ï¼ï¼å®å®å卿ã 5. **ä¿® §12.3.3 çé¿è¡¨å页缺é·**ï¼æ `PdfPrintOptions.PRINT_CSS` ç `table` ä» `break-inside: avoid` ååéææï¼å坼䏿¬¡ 60 è¡ fixture 确认第 1 页ä¸åçç½ã 6. **æéç å ¥ `system_menu`**ï¼`qc-report:template:ai-import` ä¸ `qc-report:instance:*` ç®åé è¶ ç®¡ç´éï¼æ£å¼å¯ç¨åè¦è½èåè¡ï¼å±åå§åèæ¬åæ´ï¼æ `sql-config-export-init.md` èµ°ï¼ã --- ## ååãæ¶å£ä¸ä»¶ï¼2026-09-19ï¼ââ §13.10 å¾ å 2/3/5/6 ä¸çå项è½å° > æ¬èæ §13.10 éãå·²å代ç 使²¡éªè¿ãåãåæ ¹æ²¡åãçå 项䏿¬¡æ§æ¸ æï¼å¹¶è¡¥ä¸å¹³å°ç**åºä»¶éç¯å端**ã > ä¸å¥è¯ç»è®ºï¼**é¿è¡¨å页缺é·å·²ä¿®ä¸æä¸æ¡å¯å¤ç°ç夿®**ï¼**AI å¯¼å ¥çå¤å¿ blob éæ£å·²ä¿®ä¸æ 15 æ¡æè¨**ï¼ > **æ¥åå®ä¾ä»ãåªæ 8 个å端æ¥å£ãåæã页é¢ä¸è½ç¹ã**ï¼æéç ä¹è½åºäºã ### 14.1 #43 é¿è¡¨åé¡µç¼ºé· ââ â 已修ï¼ä¸çä¸äºä¸æ¡æºå¨å¯å¤ç夿® **æ¹çæ¯ä»ä¹**ï¼`PdfPrintOptions.PRINT_CSS` é **æ `table` ä» `break-inside: avoid` ååææ**ï¼ä¿ç `tr/td/th/img`ã ```java // æ¹åï¼tr, td, th, img, table { break-inside: avoid; ... } // æ¹åï¼ tr, td, th, img { break-inside: avoid; page-break-inside: avoid; } ``` **为ä»ä¹**ï¼`break-inside: avoid` å å¨ `table` 䏿¯ãæ´å¼ 表ä¸è®¸è·¨é¡µãã60 è¡æ£éªé¡¹ç表å¨ä¸é¡µæ¾ä¸ä¸æ¶ï¼Chromium 伿**æ´å¼ 表**æ¬å°ä¸ä¸é¡µï¼ç¬¬ 1 页åªå©é¡µçä¸ä¸å¤§ç空ç½ãé¿è¡¨è·¨é¡µçæ£è§£æ¯ `thead { display: table-header-group }` 让表头æ¯é¡µéå¤ + è¡å 䏿页ï¼è¿ä¸¤æ¡æ¬æ¥å°±å¨ãæ¹åç宿µç°è±¡è®°å½å¨ §12.3.3ã **夿®ï¼è¿æ¯æ¬æ¬¡ææä»·å¼çé¨åï¼**ï¼é ä¸ä»½ 60 è¡å¾ªç¯è¡¨æ ¼ + å åµå¾ççä¸´æ¶æ¨¡æ¿å¹¶åºä»¶å¯¼åºï¼`D:/qcl-tmp/pdf-visual-fixture.mjs`ï¼ï¼åç¨ `PageFillProbe.java` æé¡µç»è®¡ææ¬éä¸å ³é®è¯ã项ç®ãçåºç°æ¬¡æ°ï¼ | ææ | æ¹åï¼Â§12.3.3 è®°å½ï¼ | æ¹åï¼æ¬æ¬¡å®æµï¼ | |---|---|---| | 第 1 é¡µå ³é®è¯å½ä¸æ° | **0**ï¼`VERDICT=FIRST_PAGE_BLANK`ï¼ | **36**ï¼`VERDICT=FIRST_PAGE_HAS_ROWS`ï¼ | | å页 | 1 页ï¼è¡¨è¢«æ´ä½æ¨å°ä¸åå¨ç第 2 é¡µå¯¼è´æº¢åºï¼ | 2 页ï¼page1 36 è¡ / page2 26 è¡ | `pdf-visual-fixture.mjs` æ¬è½® **11/11 PASS**ï¼60 项åºä»¶å `<tr>` æ° = 61ï¼60 æ°æ® + 1 表头ï¼ãäº§ç© HTML ä¿ç `data:image` å åµå¾ã`@page` æ¯ `210mm 297mm`ãPDF 2 页ãMediaBox `594.96 à 841.92`ï¼A4 ç«çï¼ãå« `/Subtype /Image`ãå« `/FontFile2`ã > ä¿® `pdf-visual-fixture.mjs` çä¸å¤**æè¨èªèº«åé**ï¼åªä½çæè¨ååæ» `/0 0 595\.\d+ 841\.\d+/`ï¼è Chromium åºç A4 æ¯ `594.95996 à 841.91998`ââå·® 0.05 pt æ¯æµ®ç¹èå ¥ï¼ä¸æ¯çº¸åä¸å¯¹ãå·²æ¹æå¸¦å®¹å·®çæ°å¼æ¯è¾ã**è¿ä¸æ¯å端缺é·ã** ### 14.2 #44 AI å¯¼å ¥å¤å¿ blob 鿣 ââ â 已修ï¼15/15 æè¨ §13.7 â¡ æç鿣ï¼å¤ç½®å£å¾å®ä¸º **ãæ¿æ¢ãè䏿¯ã累积ã**ï¼ä¸æ¬¡å¯¼å ¥å¯¹åºä¸ä»½æºæä»¶ï¼æ°çè¿æ¥ãæ§çåºå»ãæºæä»¶åªæ¯è¯å«ç¨çè¾å ¥ï¼æ¨¡æ¿çæ£çäº§ç©æ¯çæ¬ Schemaï¼æ²¡æå½æ¡£å®ä»¬çå¿ è¦ã **æ¹çæ¯ä»ä¹**ï¼`ai-import-modal.vue` å¨ `bind` **ä¹å**å 䏿¥ `removeExistingSourceFiles(templateId)` ââ å `listAttachments(recordType=qc_report_template, recordId)` æ¿å°æ§éä»¶ idï¼å `deleteAttachments(ids)`ï¼ç¶åæä¸ä¼ âç»å®æ°æä»¶ã **为ä»ä¹å¿ é¡»åç¬å **ï¼`bind` çå®ç°æ¯ãåªå æ§ç**éä»¶å ³èè®°å½**ï¼ä¿ç **blob è®°å½**ï¼æä»¶æ¬èº«ä¸å é¤ï¼ããæä»¥ææç»å®é¡ºææ¸ çæ¯éçââæ§æä»¶å ä¸äºè¡ä¹å ä¸äºçï¼éå¤å¯¼å ¥å 次ç£çä¸å°±å¤å 份忲¡äººå¼ç¨çæä»¶ã **宿µï¼`D:/qcl-tmp/verify-44-blob-lifecycle.mjs`ï¼15/15 PASSï¼**ï¼å¤å»åç«¯åæ¥æ¶åºï¼ç´æ¥æ¯å¯¹ DB ä¸ç£çï¼ | æè¨ | ç»æ | |---|---| | ä¸ä¼ A â å æ§ï¼æ¤æ¶ä¸ºç©ºï¼â ç»å® Aï¼éä»¶å表 1 æ¡ãç£çæ A | â | | åä¸ä¼ B â å æ§ï¼æ¤æ¶æ¯ Aï¼â ç»å® B | â `code=0` | | **A çç£çæä»¶éä¹å é¤ï¼#44 çæ ¸å¿ï¼** | â `...round-a.txt exists=false` | | æ¿æ¢åéä»¶å表åªå© Bï¼B çç£çæä»¶ä»å¨ | â | | å æ¨¡æ¿ â éä»¶åè¡¨æ¸ ç©ºãB çç£çæä»¶ä¹è¢«æ¸ æ | â | æå䏿¡åæ¶æ §13.10 å¾ å 3ã**宿µå 模æ¿é¡ºå¸¦æ¸ éä»¶ä¸ blob**ãä¸èµ·éªäºï¼`deleteTemplate` éç `deleteAttachmentsByRecord(QC_REPORT_TEMPLATE, id)` ç¡®å®çæï¼`live attachment for template 15 = 0`ï¼ä¸¤ä¸ª blob è¡å `deleted=1`ï¼ç£çæ æ®çï¼ã ### 14.3 #45 å端æ¥åå®ä¾é¡µ ââ â å·²è½å°ï¼æµè§å¨å ¨é¾è·¯éªæ¶éè¿ **è¿æ¯ §13.10 éæ²¡æçä¸é¡¹**ï¼æ¤åå¹³å°ç 8 个å®ä¾æ¥å£**å端ä¸ä¸ªé½æ²¡æ¥**ï¼ãåºä»¶éç¯ãå¨é¡µé¢ä¸ç¹ä¸å°ãæ¬è½®è¡¥é½ã | å± | æä»¶ | ç¶æ | |---|---|---| | API | `mom-pro2-before/src/api/mes/qc/report/instance/index.ts` | æ°å»ºï¼æ¤åå®å ¨ä¸åå¨ï¼ | | å表页 | `.../views/mes/qc/report/instance/index.vue` + `data.ts` | æ°å»ºï¼è·¯ç± `/qc/report-instance` | | 详æ å¼¹çª | `.../instance/modules/detail.vue` | æ°å»ºï¼æ¥åé¢è§ï¼iframeï¼/ æ°æ®å¿«ç § / å¯¼åº PDF | | æé | `system_menu` 6 è¡ | èåãæ¥åå®ä¾ã+ æ¥è¯¢/åºä»¶/导åºPDF/å é¤ 4 个æé® + `qc-report:template:ai-import` | | èè°æ¹æ¡ | `docs/qc_report_instance_frontend_integration.md` | å¢éæ´æ°ï¼å PDF è½®å·²å»ºï¼ | **æéé¾å ¨ç¨è¿½è¿ä¸é**ï¼è¿å³å®äºè¦ä¸è¦éå¯å端ï¼ï¼å端 `@ss.hasPermission` å¯¹è¶ ç®¡çè·¯æ¾è¡ï¼å端 `hasAccessByCodes` æ¯æ¿ `accessCodes` å**精确éåå¹é **ï¼èå®ç± `/get-permission-info` ç°åºä» `menuService.getMenuList(menuIds)` æå»ºï¼`getMenuList()` **䏿¯ `@Cacheable`**ãâ **åªæ `system_menu` è¡ï¼å·æ°é¡µé¢å³å¯çæï¼ä¸éè¦éå¯å端ã** 已宿µï¼æ°èå项åºç°å¨ä¾§æ ã`/qc/report-instance` è·¯ç±å¯è¿ãæé®æ£å¸¸æ¸²æã **æµè§å¨éªæ¶ï¼Playwright MCPï¼6 å¼ æªå¾ï¼**ï¼ | éªè¯ç¹ | ç»æ | |---|---| | å表渲æ | 5 è¡ï¼æ¨¡æ¿å / ä¸å¡ç±»åï¼`mes_qc_oqc`âåºè´§æ£éªï¼/ çæäººï¼ç¼å·âæµç§°ï¼**齿£ç¡®è§£æ**ï¼æ `æ¨¡æ¿ #id` æè£¸ç¼å·æ®ç | | 详æ å¼¹çª | Descriptions é½å ¨ï¼æ¨¡æ¿çæ¬ç»¿è² Tagãç¶æèµ° `DictTag` | | **æ¥åé¢è§é¡µç¾** | iframeï¼`sandbox=""`ï¼**çæ¸²åºæ¥åæ¬ä½**ï¼å«å åµå¾çç红/绿æ¸åï¼60 è¡è¡¨æ ¼å¯è§ | | æ°æ®å¿«ç §é¡µç¾ | å»ç»ç context JSON æ£å¸¸å±ç¤º + è¯´ææ§ Alert | | **å¯¼åº PDF æé®** | toastãå·²çæ VERIFYâ¦.pdfï¼44.25 KBï¼èæ¶ 1615 毫ç§ï¼ãï¼å¼¹çªå åºç°ãå¨çº¿é¢è§ / ä¸è½½ PDFã | | **éå¤å¯¼åºä¸å æä»¶** | åå®ä¾è¿ç¹ 3 次åï¼`system_storage_attachment` åªæ **1 æ¡ææè¡**ï¼åä¸¤æ¡ `deleted=1`ï¼ã`system_storage_blob` 对åºè¡åæ ·è½¯å ã**ç£çç®å½éåªçå½åè¿ä¸ä»½ PDF** | **åç«¯é¶æ°å¢ç±»åé误**ï¼`vue-tsc --noEmit`ï¼å¸¦ 8 GB å åæ°ï¼æ»è®¡ **316 项**ï¼ä¸æ¢æåºçº¿å®å ¨ä¸è´ï¼ï¼å ¶ä¸ `qc/report` ç¸å ³æä»¶ **0 项**ã ### 14.4 åå§åèæ¬ ââ ç»è®ºï¼**æ¬æ¬¡æ éæ¹å¨** 6 æ¡èåè¡åªæè¿äºè¿è¡åº `ruoyi-vue-pro`ãæ `sql-config-export-init.md` æ¬è¯¥éæ°çæ `config_export_all_<æ¥æ>.sql`ï¼ä½å®é æ åµä¸åï¼ > **该æä»¶çãé¶æ®µäºã䏿¯åé¢é INSERTï¼èæ¯æ§è¡æçè·¨åºæ·è´** ââ > `INSERT INTO system_menu SELECT * FROM \`ruoyi-vue-pro\`.system_menu WHERE deleted = b'0'`ã > åªè¦æºåºæè¿å è¡ï¼å¨æ°åºæ§è¡æ¶å°±ä¼è¢«å¸¦è¿å»ï¼**䏿件å 容æ å ³**ï¼é¶æ®µä¸çåé¢é INSERT æéè¦éæ°å¯¼åºï¼ã æä»¥**æä»¶æ¬èº«å·²ç»æ¯ææ°ç**ã已卿件头ãè¡¥ä¸è¯´æãå äºç¬¬ ⤠æ¡ç»è®°è¿ä»¶äºï¼å¹¶åæãæ¤å¤æ éæ¹å¨ãç»è®°åªä¸ºè¯´æè¿çæä»¶ç¡®å®è¦çäºè¯¥åæ´ãï¼é¿å 忥è 误以为æ¼äºã ### 14.5 æ¬è½®åç°ç**两å¤åéæ°æ®ä¸ä¸è´**ï¼ä¸æ¯æ¬è½®ä»£ç å¼å ¥ï¼å¦å®ä¸æ¥ï¼ ç¨ `pdf-e2e.mjs` å¤è·æ¶åºç° 2 æ¡ FAILï¼17/19ï¼ï¼éæ¡è¿½å°çæ¯**æ°æ®é®é¢èé缺é·**ï¼ | ç°è±¡ | æ ¹å | |---|---| | æè¨ãçº¸é¢æ¯ A4 ç«çã失败ï¼å®å¾ `MediaBox [0 0 841.92 594.96]`ï¼**横å**ï¼ | æ¨¡æ¿ 5 ç `qc_report_template` è¡åçæ¯ A4/**portrait**ï¼ä½å®**çæ¬ schema éç `page.orientation` æ¯ landscape`**ã**渲æå¨ä»¥ schema 为å**ï¼çº¸é¢é ç½®ççç¸å¨çæ¬éï¼ä¸å¨æ¨¡æ¿è¡ä¸ï¼â è¡ä¸ schema ä¸ä¸è´ï¼æ¸²ææ schema èµ° | | æè¨ã60 é¡¹åºææå¤é¡µã失败ï¼å®å¾ 1 页 | æ¨¡æ¿ 5 **v1.0 çç»å¸æ¯ç©ºç**ï¼`{"components":[],â¦}`ï¼schema ä» 959 å符ï¼ï¼60 è¡è¡¨æ ¼æ ¹æ¬æ²¡æ¸²è¿å»ãè¿æ¡æè¨æ¯ v1.0 è¿æå 容æ¶åçï¼æ°æ®è¢«æ¸ åæè¨å°±æ¬ç©ºäº | **å¦ä¸æ¡å¼å¾è®°çè§åæ§äºå®**ï¼`uk_report_no` **ä¸åºå软å é¤**ã䏿¡å®ä¾è¢«å é¤åå®çæ¥åç¼å·ä»è¢«å ç¨ï¼å次åºä»¶åå·ä¼æ¥ `1_070_102_001`ãæ¬è½®é fixture æ¶å°±æå°è¿ä¸æ¬¡ï¼ç¡¬ç¼ç ç `PG20260918001` ä¸å·²è½¯å çå®ä¾ 25 å²çªï¼ï¼æ¹æå¸¦æ¶é´æ³çç¼å·æéè¿ãåèè°èæ¬æ¶**ä¸è¦ç¡¬ç¼ç æ¥åç¼å·**ã ### 14.6 â å·²éªè¯ / æªéªè¯ï¼æ¿æ¥ §13.8ï¼ **æ¬è½®æ°æ¿å°å®æµè¯æ®ç**ï¼ - é¿è¡¨å页修å¤ï¼`PageFillProbe` ç `FIRST_PAGE_HAS_ROWS`ï¼å«æ¹ååå¯¹ç §ï¼ï¼ - AI å¯¼å ¥æºæä»¶æ¿æ¢ç blob/ç£ççå½å¨æï¼15 æ¡æè¨ï¼ç´æ¥ DB + ç£çï¼ï¼ - å æ¨¡æ¿æ¸ éä»¶ä¸ blobï¼åæ¥éªè¯ï¼ï¼ - æ¥åå®ä¾é¡µçå表 / 详æ / iframe é¢è§ / å¿«ç § / å¯¼åº PDF / éå¤å¯¼åºä¸å æä»¶ï¼6 å¼ æªå¾ï¼ï¼ - æéç å ¥ `system_menu` ååç«¯å³æ¶çæï¼æ ééå¯ï¼ï¼ - åç«¯ç±»åæ£æ¥é¶æ°å¢é误ï¼316 â 316ï¼ã **仿ªéªè¯çï¼æ¿æ¥ §13.8ï¼æ¬è½®**没æ**æ¨è¿ï¼**ï¼ - **`yudao.qcreport.ai-import.enabled=false â 1_070_104_000`** ââ 仿ªå®æµã - **è¶ æ¶åæ¯ `1_070_104_011`** ââ 仿ªå®æµã - **è¶ é¡µ / åè®¡è¶ é¡µ / è¶ å¤§å°ä¸æ¡ï¼`FILE_TOO_MANY_PAGES` / `TOO_MANY_PAGES_TOTAL` / `FILE_TOO_LARGE`ï¼** ââ 仿ªå®æµï¼ç¼ºè¶ é fixtureã - **`yudao.qcreport.pdf.enabled=false â 1_070_103_008`** ââ 仿ªå®æµã - **AI è¯å«åç¡®ç** ââ 仿ªåº¦éã - **Linux é¨ç½²** ââ 仿ªéªã > è¿äºé¡¹é½éè¦**ä¸´æ¶æ¹é ç½® + éå¯å端**ï¼éä¸å¨ä¸ä¸è½®ä¸æ¬¡åå®ï¼è§ 14.7ï¼ã ### 14.7 ä¸ä¸æ¥ 1. **䏿¬¡æ§è¡¥å®é ç½®åæ¯å®æµ**ï¼æ¹ä¸æ¬¡é ç½® + éå¯ä¸æ¬¡ï¼æåæ¡ä¸èµ·ææï¼ï¼`ai-import.enabled=false` â `104_000`ï¼`yudao.ai.timeout=1s` â `104_011`ï¼è¶ å¤§å° fixture â `104_004`ï¼`pdf.enabled=false` â `103_008`ã 2. **å®å¤ºå¹¶ä¸æ¥æ¨¡æ¿ 5 çãè¡ vs schema 纸åä¸ä¸è´ãä¸ãv1.0 空ç»å¸ã**ï¼è§ 14.5ï¼ââ å±åéæ°æ®ï¼é人工确认æ¯ä¿®æ°æ®è¿æ¯æ¹ `pdf-e2e.mjs` çæè¨ã 3. **`MesQcReportApi` + åç§è´¨æ£åå½ä¸å**ï¼æãè´¨æ£å â åºæ¥åãçå ¥å£æ¥ä¸ï¼å±æ¶å表页ç `qc-report:instance:create` æéç æçæ£ç¨å¾ä¸ï¼ç°å·²é¢ç½®å ¥åºï¼ã 4. **åç¬å¼ä¿®å¤é¡¹ï¼ERP/CRM/MES ä¸å¤ `tempFile.delete()` 缺é·**ï¼åæä¸å¤ï¼å çæ¯ç¨æ·çå®ä¸ä¼ ç blob æä»¶æ¬èº«ï¼ã 5. å ¶ä½ç»´æä¸åï¼å ¥ç« HTML ååãäºç»´ç çæã`qc_report_data_source`/`qc_report_rule` ä¸¤å¼ æ Java 对åºç表ã --- ## åäºãéå¶ä¸é ç½®åæ¯å®æµï¼2026-09-19ï¼ Â§14.6 åçäºé¡¹æªéªè¯ï¼æ¬è½®å ¨é¨ææã**䏿¹ä»»ä½ä¸å¡ä»£ç ï¼åªåéªè¯**ï¼æä»¥æ¬è没æäº¤ä»ç©ï¼åªæè¯æ®ä¸ä¸ä¸ªæ°æ´é²çé®é¢ã ### 15.1 æ¹æ³ï¼æéªè¯èæ¬åï¼è䏿¯æç¹ 䏿¡èæ¬æ¾å¨ `D:/qcl-tmp/`ï¼ä¸å ¥åºï¼ï¼ | èæ¬ | è¦ç | æ¯å¦ééå¯ | |---|---|---| | `verify-46-limit-branches.mjs` | è¾å ¥æ ¡éªç±» 10 ä¾ | å¦ | | `verify-46-config-a.mjs` | 两个 `enabled=false` å¼å ³ | æ¯ï¼æ¹é ç½®ï¼ | | `verify-46-config-b.mjs` | AI è°ç¨è¶ æ¶ | æ¯ï¼æ¹é ç½®ï¼ | å¤é¡µ PDF fixtureï¼`p6/p5a/p5b.pdf`ï¼ç¨ Playwright ç°é ã**页æ°ä¸ç±èæ¬èªç§°ãç±å端èªå·±æ¥åºæ¥**ï¼"å ± 6 页"/"å计 10 页"ï¼ï¼é¿å èªè¯ã ### 15.2 宿µç»æï¼11 æ¡é误ç + 1 æ¡ä¸ä¼ 壿¦æª | åºæ¯ | 颿ç | ç»æ | |---|---|---| | blobId ä¸åå¨ | `1_070_104_001` | â | | `.exe` | `1_070_104_002` | â | | `.doc` æ§æ ¼å¼ | `1_070_104_003` | â | | 21MB æä»¶ | `1_070_104_004` | â | | åªæç©ºç½ç txt / æåç xlsx | `1_070_104_005` | â | | ç»ä»¶æ¸ å 41 项 / æé¡¹ç¼º type | `1_070_104_008` | â | | `schemaVersion=1.0` | `1_070_104_009` | â | | 6 页 PDFï¼åæä»¶è¶ 5 é¡µï¼ | `1_070_104_007` | â | | 两个 5 页 PDFï¼å计 10 > 8ï¼ | `1_070_104_014` | â | | `ai-import.enabled=false` | `1_070_104_000` | â | | `yudao.ai.timeout=1s` | `1_070_104_011` | â | | `pdf.enabled=false` | `1_070_103_008` | â | | 4 个æä»¶ | â | â è§ 15.3 é®é¢ä¸ | 两æ¡ãæªå¯ç¨ãæç¤ºé½ç¹åäºå ·ä½é 置项ï¼`yudao.qcreport.ai-import.enabled` / `yudao.qcreport.pdf.enabled`ï¼ï¼ç¨æ·è½ç §çæ¹ã`ai-import.enabled=false` é£ä¾**æ æä¼ ä¸åå¨ç blobId**ï¼ä»æ¿å° `104_000` èé `104_001` ââ é¡ºå¸¦è¯æå¼å ³æ£æ¥æå¨æä»¶è¯»åä¹åã **0 åèæä»¶èµ°ä¸å°ä¸å¡å±**ï¼storage ä¸ä¼ å£ç´æ¥ä»¥ `ä¸ä¼ æä»¶ä¸è½ä¸ºç©º`ï¼code `1002029000`ï¼æç»ï¼æä»¥ `OfficeImportAdapter` é `content.length == 0` 飿¡åæ¯ç» HTTP ä¸å¯è¾¾ï¼`text.isBlank()` 飿¡ææ¯å®é çæçï¼ç¨åªæç©ºç½ç txt éªå°ï¼ã屿£å¸¸ç纵深设é²ï¼ä» è®°å½ã ### 15.3 ä¸ä¸ªæµåºæ¥çé®é¢ **é®é¢ä¸ï¼`max-files` è¿ä¸ªé 置项åªè½å¾ä¸è°ï¼ä¸è½å¾ä¸è°ã** `QcReportAiImportReqVO.blobIds` ä¸åæ»äº `@Size(max = 3)`ï¼Bean Validation å äºæå¡æ§è¡ã忿䏤å±ï¼ 1. `AI_IMPORT_TOO_MANY_FILES`ï¼`104_006`ï¼å¨é»è®¤é ç½®ä¸**ä¸å¯è¾¾** ââ ä¼ 4 个æä»¶æ¿å°çæ¯ HTTP 400ã䏿¬¡æå¤è¯å« 3 个æä»¶ãï¼è䏿¯è¿ä¸ªé误ç ãæå¡éç `validateFileCount` ä¸ VO 注解éçæ¯åä¸ä»¶äºã 2. æ YAML éç `max-files` è°å° 5 ä¹**ä¸ä¼çæ**ï¼ä»è¢« `@Size(max=3)` å¡å¨ 3ï¼ï¼åªæè°å° 3 以䏿ææä¹ã 代ç éæ²¡æä»»ä½æ³¨é说æè¿ä¸¤ä¸ªæ°åæ¯è¦åçï¼åæ¥çç»´æ¤è 大æ¦çä¼ä»¥ä¸ºæ¹ YAML å°±è½æ¾å¼ã **é®é¢äºï¼æåç Office æä»¶ç»åºçæ¯ãæ«æä»¶å辨çãç建议ã** â **已修ï¼è§ §15.7** ä¼ ä¸ä¸ªæåç `.xlsx`ï¼è¿å `104_005`ï¼ > æä»¶ãbroken.xlsxãæªè§£æåºä»»ä½å¯ç¨å 容ï¼å ± 1 页ï¼ã**è¥æ¯æ«æä»¶ï¼è¯·ç¡®è®¤å辨çä¸ä½äº 200 DPIãæåæ 严é徿æé®æ¡**ï¼æ¨èæ¹ç¨çµåç PDF æ Word/Excel çå®åå æ¯**æä»¶æåæä¸æ¯ææç Excel**ï¼ä¸æ«æå辨çå PDF 毫æ å ³ç³»ï¼`readXlsx`/`readDocx` ç catch åç»ä¸æ `FILE_EMPTY`ï¼ãææç¤ºæä½çç¨æ·ä¼å»è°å辨çãæ¢ PDFï¼æä¹è¯é½ä¸ä¼å¥½ãè¿è¿å `.claude/rules/error-message-precision.md` çä¸è¦ç´ ï¼å ·ä½ç»´åº¦ / åæ¹å¼ / 坿§è¡å¨ä½ï¼ââ 维度æéäºï¼å¨ä½ä¹å°±éäºã**建议å ä¸ä¸ªä¸å±é误ç **ï¼å¦ `AI_IMPORT_OFFICE_CORRUPT`ï¼ï¼ææ¡æå"æä»¶å¯è½å·²æåï¼è¯·ç¨ Excel/Word éæ°æå¼å¹¶å¦å为 .xlsx/.docx åéè¯ï¼è¥åæä»¶æå¯ç ä¿æ¤è¯·å è§£é¤"ã **é®é¢ä¸ï¼æéè¦ï¼ï¼`yudao.ai.timeout` 䏿¯ç«¯å°ç«¯èæ¶ä¸éã** é `1s`ï¼å®æµç«¯å°ç«¯ **7967ms**ï¼æ¥å¿éçå¼å¸¸æ¯ `OpenAIIoException â InterruptedIOException: timeout â SocketTimeoutException`ï¼okhttp3 HTTP/2ï¼ã`isTimeout()` 认åºäºå®ï¼æä»¥æ å°å° `104_011` æ¯å¯¹ç ââ **ä½ 1 ç§åæäº 8 ç§**ã 读 `openai-java-core` æºç ï¼`RetryingHttpClient.kt` / `ClientOptions.kt`ï¼ç¡®è®¤äºä¸ä¸ªä¹æ°ï¼ 1. **SDK é»è®¤ `maxRetries = 2`**ï¼å³æå¤ 3 次å°è¯ã`shouldRetry()` æç¡®æ `IOException` ä¸ `OpenAIIoException` å为å¯éè¯ ââ **è¶ æ¶æ¬èº«å°±ä¼è¢«éè¯**ã 2. éé¿ `min(0.5 à 2^(n-1), 8s)` åä¹ jitterï¼ä¸¤æ¬¡é´é约 1.1ï½1.5sã 3. `OpenAiChatOptions.timeout` è®¾çæ¯**读/æµè¶ æ¶**ï¼ä¸è¦ç okhttp ç `connectTimeout`ï¼é»è®¤ 10sï¼ãæ¡æé¶æ®µä¸åè¿ä¸ªé¢ç®ã â ç产é ç `60s`ï¼**ç¨æ·å¯è§çæåçå¾ çº¦ 3 åé**ï¼60Ã3 + éé¿ + éè¿ï¼ï¼ä¸æ¯ 60 ç§ãçé¢æç¤ºåçæ¯"é¢è®¡ 10â60 ç§"ï¼å·® 3 åãææ¡æ¬èº«æ²¡éªäººï¼å®è¯»çæ¯çå® `elapsed`ï¼å¦å®åäº"å·²ç 7 ç§"ï¼ï¼ä½**çå¾ æ¶é¿ä¸ç¨æ·é¢æè±è**ã è¿ä¸æ¯å½å飿¡ `timeout` ä¿®å¤æ²¡çæï¼èæ¯ä¿®å¤çæåæ´é²åºç第äºå±ï¼æ per-attempt è¶ æ¶å½æäºæ´ä½è¶ æ¶ãå¯éå¤ç½®ï¼åæåèï¼å¾ å®å¤ºï¼ï¼ç»è¿ä¸ªæ¨¡åå ³æéè¯ï¼`maxRetries(0)`ï¼ä½ä¼å¤±å»å¯¹æµæ§ 429 çå¼¹æ§ï¼ï¼æä¿æéè¯ãæ YAML 弿 `颿 ÷ 3` è®¾ï¼ææ¥åç°ç¶ä» æçé¢ææ¡æ¹æ"æå约 3 åé"ã ### 15.4 é带çå®è¯ï¼éä»¶ bind 伿°¸ä¹ å¤å¿åæ§ blob è· `verify-46-limit-branches.mjs` æ¶ææ¼æäºå端 `removeExistingSourceFiles` é£ä¸æ¥ï¼ç´æ¥è¿ç» `bind`ï¼ç»æ **7 个 blobï¼å«ä¸ä¸ª 21MB çï¼è¡ä¸ç£çæä»¶å ¨é¨å¤±å»å¼ç¨**ã é¾è·¯ï¼`saveStorageAttachmentByRecordTypeAndRecordId` åªè½¯å **æ§çéä»¶å ³èè¡**ï¼ä¸ç¢° blobãè½®å°ä¸ä¸ä¸ª `stage()` å bind æ¶ï¼ä¸ä¸æ¹éä»¶è¡è¢«è½¯å ï¼èå®ä»¬æåç blob åæ²¡æä»»ä½æªå é¤çéä»¶è¡å¼ç¨ ââ æ²¡æä»»ä½æºå¶ä¼åæ¶å®ã **è¿åå®äº `memory/qc-report-platform.md` é飿¡è¦åï¼å¹¶è¡¥ä¸å®çæ¨è®ºï¼é²å¤å¿çé£éé¸é¨å¨è°ç¨æ¹ï¼å端 `removeExistingSourceFiles`ï¼ï¼åå¨å±æ¬èº«æ²¡æä»»ä½å åºã** å端ç®åå对äºï¼ä½ä»»ä½ç»è¿å端çè°ç¨æ¹ï¼å«èè°èæ¬ï¼é½ä¼é黿¼æä»¶ãæ®çå·²ç¨äº§åèªèº«ç级èï¼ç»å°ä¸´æ¶æ¨¡æ¿ â å æ¨¡æ¿ï¼æ¸ å¹²åï¼DB ä¸ç£ç齿 ¸å¯¹è¿ã ### 15.5 â 仿ªéªè¯ï¼è¯å®å£°æï¼ - **`104_010 AI_UNAVAILABLE`**ï¼éè¦æ `ai_api_key`/`ai_model` æ¹åæè½è§¦åï¼ä¼å½±å ERP/CRM/MES çå ¨é¨ AI è°ç¨ï¼é£é©å¤§äºæ¶çã宿¯ `104_011` åä¸ä¸ª try/catch çå åºåæ¯ï¼å¼å¸¸æ¶å£è·¯å¾å·²è¢«è¶ æ¶ç¨ä¾è¯æå¯ç¨ã - **`104_012 RESPONSE_UNPARSEABLE` / `104_013 DRAFT_EMPTY`**ï¼éè¦æ¨¡åè¿åç¸å½¢æç©ºç»æï¼æ æ³ç¡®å®æ§è§¦åã两è é½æçº¯åæµè¦çï¼`QcReportAiDraftParser` ç½å¤´å符串 / `Normalizer` ç©ºç»æï¼ï¼ç¼ºçåªæ¯ç«¯å°ç«¯é£ä¸æ¥ã - **AI è¯å«åç¡®ç**ï¼ä»æªåº¦éï¼éè¦äººå·¥æ æ³¨æ ·æ¬ï¼æ¯ç¬ç«è¯¾é¢ï¼ã - **Linux é¨ç½²**ï¼ä»æªéªã ### 15.6 æ¶å°¾ç¶æ é 置已è¿åï¼`application-local.yaml` ç `pdf.enabled`ã`ai-import.enabled` åå° `true`ï¼`yudao.ai.timeout` åå° `60s`ï¼ä¸å¤ä¸´æ¶æ¹å¨é½çäº `ã#46 临æ¶ã` æ è®°ï¼å·²å ¨é¨æ¤éï¼ãæµè¯æé´å»ºç 5 ä¸ªä¸´æ¶æ¨¡æ¿ï¼id 16~20ï¼ä¸ 1 个å®ä¾ï¼id 37ï¼è¿åå ¶éä»¶ãblob è¡ãç£çæä»¶å ¨é¨å é¤ã ### 15.7 §15.3 两个å³å®çè½å°ï¼2026-09-19 ç»ï¼ §15.3 çä¸ä¸ªé®é¢éï¼é®é¢ä¸ï¼`@Size(max=3)` ä¸ YAML è¦åï¼ä¸é®é¢ä¸ï¼`timeout` æ¯ per-attemptï¼å·²æç¨æ·ææ¿å¤ç½®ï¼é®é¢äºçä¿®æ³å¦ä¸ã **å³å®ä¸ï¼è¶ æ¶ä¸æ¹ä»£ç ï¼åªæ¹ç颿æ¡ã**ï¼é®é¢ä¸ï¼ ç¨æ·å£å¾æ¯"䏿¹ä»£ç ï¼åªæçé¢ææ¡æ¹æãæå约 3 åéã"ãæ¹å¨åªæä¸å¤ï¼ `mom-pro2-before/src/views/mes/qc/report/template/designer/modules/ai-import-modal.vue` ççå¾ æç¤ºï¼ç± > æ£å¨è¯å«ï¼é¢è®¡ 10â60 ç§ï¼æä»¶é¡µæ°ææ°éè¾å¤æ¶æ´ä¹ ï¼è¯·å¿å ³é页é¢ã æ¹ä¸º > æ£å¨è¯å«ï¼é¢è®¡ 10â60 ç§ãæ«æä»¶æé¡µé页è¯å«ï¼æ¯é¡µåç¬ä¸æ¬¡è°ç¨ï¼é¡µæ°å¤æ¶ä¼ææ¾æ´ä¹ ï¼æé¿çå¾ çº¦ 3 åéï¼è¶ æ¶å请åå°æä»¶æ°éæé¡µæ°åè¯ã请å¿å ³é页é¢ã **没æ**å¨ `src/api/mes/qc/report/ai/index.ts` é飿¡ axios ç `timeout: 180_000` ââ 宿°å¥½ä¸"ç¨æ·å¯è§ç 3 åé"åå¼ï¼æ¯è¯å®çã > â **§15.3 é®é¢ä¸çè§æ¨¡éè¦ä¿®æ£ï¼æ±æ¥å£å¾ï¼**ï¼3 å鿝**忬¡ LLM è°ç¨**çæåå¼ãè `QcReportLlmCallPlanner` å¯¹æ«æä»¶æ¯**æ¯é¡µä¸æ¬¡è°ç¨**ï¼å请æ±ä¸é 8 页 ââ å端æåå¯ä»¥è¿ç»ç§ 8 è½®ã约 24 åéãå端 180 ç§å ææï¼æä»¥**ç¨æ·å¯è§çå°±æ¯ 3 åé**ï¼ææ¡æ²¡éªäººï¼ï¼ä½**æµè§å¨æ¾å¼ä¹åå端ä»å¨ç»§ç»è·å¹¶ç»§ç»è®¡è´¹**ãè¿æ¯æ¬è½®æªå¤ç½®çæ®çï¼åºåç¬å¼ä¸é¡¹ã **å³å®äºï¼ä¸ºæåç Office æä»¶å ä¸å±é误ç ã**ï¼é®é¢äºï¼ `1_070_104_015 AI_IMPORT_OFFICE_CORRUPT`ï¼ > æä»¶ãXãæ æ³ä½ä¸º .Y æå¼ï¼å 容已æåæåå¯ç ä¿æ¤ã请å ç¨ Office æå¼è¯¥æä»¶ï¼ç¡®è®¤è½æ£å¸¸æ¾ç¤ºåãå¦å为ã.docx / .xlsx åéæ°å¯¼å ¥ï¼è¥æä»¶ææå¼å¯ç ï¼è¯·å è§£é¤å¯ç ä¿æ¤ æ¹å¨ä¸å¤ï¼ | æä»¶ | æ¹å¨ | |---|---| | `enums/ErrorCodeConstants.java` | æ°å¢ `AI_IMPORT_OFFICE_CORRUPT = 1_070_104_015`ï¼è¯¥æ®µåæ¢äº `_014`ï¼ | | `service/aiimport/document/OfficeImportAdapter.java` | `readDocx` / `readXlsx` 两个 catch åç±æ `FILE_EMPTY` æ¹ä¸ºææ°ç | | `docs/qc_report_ai_import_frontend_integration.md` | é误ç 表å¢è¡ï¼`_005` ç触ååºæ¯æ¶çªï¼ä¸å¡è§å表å¢ã空ç½ä¸æåå弿¥ãä¸è¡ | **è¾¹ç廿忏 **ï¼`text.isBlank()` 飿¡åæ¯ä»æ `FILE_EMPTY`ãè¯ä¹æ¯ââ"æ©å±ååæ³ä½å 容为**空**"ï¼å¦åªæç©ºç½å符ç `.docx`ï¼èµ° `_005`ï¼"æ©å±å忳使件**æä¸å¼**"ï¼æåãå å¯ï¼èµ° `_015`ã两è ç»ç¨æ·çå¨ä½å®å ¨ä¸åï¼åè æ¢æä»¶ï¼åè ä¿®å¤æä»¶ï¼ï¼ä¸è½åæä¸æ¡ã `mvn compile -pl yudao-module-qcreport -am -q` å·²éè¿ï¼`MVN_EXIT=0`ï¼ãæ°ç ç端å°ç«¯å¤éªè§ §15.8ã ### 15.8 `1_070_104_015` 端å°ç«¯å¤éªï¼**4/4 PASS**ï¼2026-09-19 ç»ï¼ éå¯å端åè· `D:/qcl-tmp/verify-104-015.mjs`ï¼åæ¡ç¨ä¾å ¨ç»¿ï¼å®æ®å¦ä¸ï¼ | ç¨ä¾ | ææç | 宿µç | æ¥æï¼æªæï¼ | |---|---|---|---| | æåç `.xlsx`ï¼æ PK 头ãå 容䏿¯ zipï¼ | `1_070_104_015` | â `1070104015` | æä»¶ãbroken.xlsxãæ æ³ä½ä¸º .xlsx æå¼ï¼å 容已æåæåå¯ç ä¿æ¤â¦**å¦å为** .docx / .xlsx åéæ°å¯¼å ¥ | | æåç `.docx`ï¼æ PK 头ãå 容䏿¯ zipï¼ | `1_070_104_015` | â `1070104015` | åä¸ï¼æ©å±åæ¿æ¢ä¸º .docx | | æ ¹æ¬ä¸æ¯è¡¨æ ¼ç `.xlsx`ï¼æ PK å¤´ï¼ | `1_070_104_015` | â `1070104015` | åä¸ | | åªæç©ºç½ç `.txt`ï¼åå½ï¼ | `1_070_104_005` | â `1070104005` | æä»¶ãblank.txtãæªè§£æåºä»»ä½å¯ç¨å 容ï¼å ± 1 页ï¼â¦ | **ä¸¤ç§æåå½¢æé½å½å° `_015`**ï¼æ zip 头ä½å 容å / è¿ zip 头齿²¡æï¼ï¼è¯´æ catch è¦çé¢ä¸é¢æä¸è´ï¼ ç©ºç½æä»¶ä»èµ° `_005`ï¼**è¾¹çæ²¡æè¢«è¿æ¬¡æ¹å¨æ¾å®½**ãæ¯ä¸ªç¨ä¾é ä¸ä¸ªä¸æ¬¡æ§æ¨¡æ¿å¹¶å¨æè¨åå½åºå é¤ï¼ äºåæ ¸å¯¹ `qc_report_template.id >= 10` å ± 21 è¡**å ¨é¨ `deleted=1`**ã`qc_report_instance` å ¨é¨ `deleted=1`ã `mes_qc_oqc` æ `ZZ_E2E_%` ââ æ æ´»å¨æ®çã ### 15.9 â 仿ªéªè¯ï¼è¯å®å£°æï¼ - **åç«¯ææ¡æ¹å¨æªç» typecheck**ï¼`ai-import-modal.vue` åªæ¹äºæ¨¡æ¿éçå符串ï¼ç论æ ç±»åå½±åï¼ä½æªè· `vue-tsc` 确认ï¼åºçº¿ 316 æ¡åéé误ï¼ã - åç«¯ææ¡ä¸ `timeout: 180_000` çä¸è´æ§åªæ¯**äººå·¥æ ¸å¯¹åå¼**ï¼æªåæµè§å¨å®æµã - §15.3 é®é¢ä¸çæ®çä»å¨ï¼æµè§å¨ 180 ç§æ¾å¼åï¼å端ä»ç»§ç»è·å®å¹¶ç»§ç»è®¡è´¹ï¼ï¼æ¬è½®æªå¤ç½®ã ## åå ãè´¨æ£å â æ¥å对æ¥ï¼2026-09-19 ç»ï¼ââ â 端å°ç«¯ **19/19** å ¨ç»¿ + å端ãåºæ¥åãå ¥å£å·²æ¥å ¥å¹¶ UI éªæ¶éè¿ï¼åå端ä¸å¹¶è½å°ï¼ ç¨æ·åè¯ï¼**ãæ¥åæ¨¡æ¿è®¾è®¡è¿æ¯æ²¡æåè´¨æ£å ³èå§ï¼ç°å¨ä¸ç¥éæä¹å¯¼åºæ°æ®ã**ãæ¬èè§£å³çå°±æ¯è¿å¥ï¼ 让 MES ç IQC/IPQC/OQC/RQC å类质æ£åè½ä¸é®åºææ¥åï¼å端**ä¸åèªå·±æ¼æ°æ®**ã ### 16.1 å 个å£å¾ï¼ç¨æ·ææ¿ï¼æ¬è½®æ®æ¤å®ç°ï¼ æ¹æ¡ææ¡£ §12 åäºå ä¸ªå¾ å®é®é¢ï¼ç¨æ·çç夿¯ï¼`1.a, 2.çææ¶å¼¹æç¤º, 3.å ¨é¨è¿, 4.ä¼, 5.åªè½éæ©å¯¹åºç±»å模æ¿, 6.å 许`ã | # | é®é¢ | ç»è®º | è½å°æ¹å¼ | |---|---|---|---| | 1 | è´¨æ£åçãç¹é / ä¸åæ ¼éè´§ / ä¸åæ ¼æ¥åºãåå¼å¤å®ï¼æ¥åçãåæ ¼/ä¸åæ ¼ãä¸æè£ ä¸ä¸ | **Aï¼å¦åä¸¤ä¸ªåæ®µ** | `ReportFields` å `qcResult` / `qcResultText`ï¼ä¸å¼æå¤å®**å¹¶åäºä¸è¦ç** | | 2 | 没æè§æ ¼ä¸ä¸éç项æä¹å | **çææ¶å¼¹æç¤º**ï¼ä¸åå¡«æ°æ® | ååºå¸¦ `undecidableCount`ï¼åç«¯å¿ é¡»æç¤º | | 3 | æ£éªææ æ¯å¦è¦è¿æ»¤ | **å ¨é¨è¿**ï¼ä¸å `judge_flag` è¿æ»¤ãä¸å å | æ å°å¨ä¸åææ çé | | 4 | ä¸ä¸ªææ 夿¡å®æµå¼ï¼å¤æ ·åï¼æä¹å | **ä¼ï¼ä¸æ¡ï¼æ ·å Ã ææ ï¼ä¸æ¡æ¥å项** | æ ·åæ° > 1 æ¶æãæ ·å Xãæè¿ `remark`ï¼ä¸ä¸¢æ°æ® | | 5 | 模æ¿ç±»åè¦ä¸è¦æ ¡éª | **åªè½é对åºç±»å模æ¿** | å端 `105_005` æç»ï¼ä¸¤è¾¹ä¸æåé½ç»ï¼ï¼å端æ `reportType` è¿æ»¤ | | 6 | åä¸å¼ åæ®è½å¦åºå¤ä»½æ¥å | **å 许**ï¼åå²ççéè¦ï¼ | ä¸åéå¤çææ¦æª | ### 16.2 交ä»ç© **æ°å¢ï¼MES ä¾§ï¼`yudao-module-mes-api` / `yudao-module-mes`ï¼** | æä»¶ | 说æ | |---|---| | `api/qc/MesQcReportApi.java` | `getQcReportData(qcType, qcId)`ï¼åç±»åæ®ç»ä¸å ¥å£ | | `api/qc/dto/MesQcReportRespDTO.java` | å头 + `items` + `groupItemNames` + `missingIndicatorCount` + `finished` + `statusName` | | `api/qc/dto/MesQcReportItemRespDTO.java` | åæ¡æ£éªé¡¹ï¼å« `itemsIndex` çï¼ | | `api/qc/MesQcReportApiImpl.java` | è£ é è´¨æ£ååå±ç»æï¼å头 / å¤å®ä¾æ®è¡ / æ ·å / 宿µå¼æç»ï¼ | **æ°å¢ï¼qcreport ä¾§ï¼** | æä»¶ | 说æ | |---|---| | `enums/QcReportSourceTypeEnum.java` | åç±»åæ®çæä¸¾ï¼å¸¦ `businessType` 表åä¸ `matchesTemplateReportType` | | `service/instance/MesQcReportContextMapper.java` | **çº¯å½æ°**æ å°å¨ï¼`MesQcReportRespDTO` â `ReportContext` + `warnings` | | `service/instance/GenerateFromQcResult.java` | åºä»¶ç»æ record | | `controller/admin/instance/vo/QcReportInstanceGenerateFromQc{Req,Resp}VO.java` | åºå ¥å | | `service/instance/MesQcReportContextMapperTest.java` | **12 æ¡çº¯åæµ** | **æ¹ï¼qcreport ä¾§ï¼** | æä»¶ | æ¹å¨ | |---|---| | `pom.xml` | å `yudao-module-mes-api` ä¾èµ | | `enums/ErrorCodeConstants.java` | æ°å¢ `1_070_105_000`~`_006` å æ¡ | | `engine/context/ReportFields.java` | å `qcResult` / `qcResultText` 䏤忮µï¼`copy()` åæ¥ï¼ | | `engine/context/ReportContext.java` | `reportMap()` å¢å 两个 key | | `engine/context/ReportContextCodec.java` | `reportOf()` å¢å ä¸¤ä¸ªåæ®µ ââ **æ¼äºè¿å¤ `regenerate` ä¼éé»ä¸¢å段** | | `service/instance/QcReportInstanceService(+Impl).java` | `generateFromQc` + åä¸ªæ ¡éªå¨ | | `controller/admin/instance/QcReportInstanceController.java` | `POST /generate-from-qc` | ### 16.3 忡廿åè **åèä¸ï¼ä¸æ°å¼è½åºè·¯å¾ï¼å é¨å°±è° `generate`ã** `generateFromQc` æ ¡éªå®å°±ç»è£ ä¸ä¸ª `QcReportInstanceGenerateReqVO` è°æ¢æç `generate`ã äºæ¯çæ¬å叿 ¡éªãç¼å·çæä¸æå·éè¯ãæ°æ®å¿«ç §å»ç»ãå¤å®å¼æ**å ¨é¨ç §æ§çæï¼ä¸æ¡é½æ²¡ç»å¼**ã å¦å䏿¡è½åºé¾è·¯çäºæäºéé¸é¨å¤å¶ä¸éï¼è坿¬ä¸å®ä¼æ¼ç§»ã **åèäºï¼æ ¡éªæä¸å¡ä¼å çº§åªæ¥ç¬¬ä¸å¤ã** ç±»å â åæ®åå¨ â 已宿 â å·²å¤å® â æ¨¡æ¿ç±»å â ææ£éªé¡¹ã䏿¬¡æå¤æ¡ä¼è®©ç¨æ·æä¸ä½éç¹ ï¼`error-message-precision.md` æç¡®è¦æ±ï¼ãå æ¡ææ¡é½åå°äºãå ·ä½ç»´åº¦ + åæ¹å®é å¼ + 坿§è¡å¨ä½ãï¼ ä¸**两边å¼é½æ¯ä¸å¡ä¸æå**ï¼ãæ¥å模æ¿çæ¥åç±»åæ¯ãåºè´§æ£éªãï¼ä¸æ¬æ¬¡è´¨æ£åçç±»åãæ¥ææ£éªãä¸ä¸è´ãï¼ï¼ 䏿¯è£¸ IDã **åèä¸ï¼`105_004`ï¼æ¨¡æ¿ä¸åå¨ï¼ä¸ `105_007`ï¼æ¨¡æ¿æ å·²åå¸çæ¬ï¼ä¸å®ç°ã** æ¹æ¡ææ¡£ §7 åäºè¿ä¸¤æ¡ï¼å®é æ¹ä¸º**å¤ç¨æ¢æç** `TEMPLATE_NOT_EXISTS` / `TEMPLATE_STATUS_DISABLED` / `TEMPLATE_NO_PUBLISHED_VERSION`ãé£å æ¡ææ¡æ¬æ¥å°±æ¯ç²¾ç¡®çï¼ åé ä¸éåªä¼å¤ä¸¤æ¡ä¼æ¼ç§»ç坿¬ã**è¿æ¯ä¸æ¹æ¡ææ¡£çæ¾å¼å离ï¼å·²å¨èè°æ¹æ¡éåæã** **åèåï¼`sampleStrategy` åæ°ä¸åã** æ¹æ¡ææ¡£åæ¬è®¾æ³è®©è°ç¨æ¹éã夿 ·åæä¹å¤çãï¼ç¨æ·å£å¾ 4ï¼ãä¼ãï¼ç´æ¥å®æ»äºä¸ä»¶äºï¼ éæ©ææ²¡å¿ è¦æ´é²ç»å端ï¼åæ°å æã ### 16.4 `undecidableCount` æä¹ç®ï¼ä¸éç®å¤å®æ¡ä»¶ï¼ æ°**å¿«ç §é `result` 为空串**çé¡¹ï¼ ```java ReportContext frozen = ReportContextCodec.fromMap(result.instance().getDataSnapshot()); int undecidable = 0; for (InspectionItem item : frozen.getInspectionItems()) { if (StrUtil.isBlank(item.getResult())) { undecidable++; } } ``` ã`result` ä¸ºç©ºãæ£æ¯å¼æå¯¹ãå¾ å¤å®ãçå®ä¹ï¼`ReportEvaluator` æ¿ä¸å° verdict æ¶ `applyResult(null)`ï¼ï¼ æä»¥å«ä¹å¤©ç¶ä¸è´ï¼è¿é¡ºå¸¦æãå¡å¨è§åæ¥éä¸ãç项ç®è¿å»ã**ä¸å¦åä¸éå¤å®æ¡ä»¶** ââ 飿 ·æ¢å¯è½ä¸å¼æå说åè¯ï¼å伿¼æè§åæ¥éè¿ä¸ç±»ã ### 16.5 宿µè¯æ® **å·²éªè¯** | 项 | ç»æ | |---|---| | å ¨é¡¹ç®ç¼è¯ `mvn compile -q` | â éè¿ï¼è·¨æ¹äº `yudao-module-ai` / `-system` / `-mes-api` / `-mes` / `-qcreport`ï¼ | | `mvn -pl yudao-module-qcreport test` | â **Tests run: 106, Failures: 0, Errors: 0** â BUILD SUCCESS | | å ¶ä¸ `MesQcReportContextMapperTest` | â 12/12 | | å ¶ä¸ `FrontendConformanceTest`ï¼å»ç»å¯¹æï¼ | â 3/3ï¼**è¯æ `ReportFields` å åæ®µæ²¡æç ´åå端å»ç»äº§ç©**ï¼ | | `ReportContextCodecTest` | â 7/7 | | åéç¨ä¾ï¼å¤å® 23 / AI å¯¼å ¥ 42 / PDF 9 / åºä»¶ 7 â¦ï¼ | â å ¨é¨ä»ç»¿ | `MesQcReportContextMapperTest` ç 12 æ¡è¦çï¼å头æ å°ï¼å« IQC å `vendorBatch` 使¹æ¬¡å·ã æ¥æ `2026-09-17 14:30:05` æ ¼å¼ã`qcResult="2"`/`qcResultText="ç¹é"`ï¼ï¼å¼æå段ä¸è¢«æ å°å¨æ±¡æï¼ 缺ä¸ä¸éæ¶**ä¿æ null èé 0**ï¼æ¾å¼æ³¨éï¼åæ 0 çè¯å®æµå¼ 0.09 ä¼è¢«å¤æãè¶ ä¸éä¸åæ ¼ãï¼ï¼ åæ ·åä¸å `remark`ã夿 ·ååãæ ·å S1ããè¶ 3 ä¸ªæ ·åç空 + warningï¼æªå½å®æµå¼ warningï¼ åç»æ é¢ä¸å·²å ææ ç warningï¼æ¥ææ ¼å¼**ä¸ locale æ å ³**ï¼ç¨ `th-TH-u-ca-buddhist-nu-thai` éªï¼ã **æªéªè¯ï¼è¯å®å£°æï¼** - ä¸ Â§16.9 å䏿¡ï¼**åªéªäº IQC ä¸è·¯**ï¼IPQC / OQC / RQC ä¸é¡µçæé®ä¸å¼¹çªæªéä¸ç¹è¿ï¼å页代ç åæï¼ å·®å¼åªæ `QC_TYPE` 常éä¸è¡æä½éç¹ï¼ã ### 16.6 端å°ç«¯ HTTP èè° ââ â **19/19 PASS** ç¨æ·éå¯å端åè· `node D:/qcl-tmp/verify-qc-generate.mjs`ï¼**19 项æè¨å ¨é¨éè¿**ï¼ | # | ç¨ä¾ | ç»æ | |---|---|---| | 1 | `qcType=9` | â `105_000`ï¼æ¥æå«ãåªæ¯æ IQCã | | 2 | `qcId=999999` | â `105_001`ï¼æ¥æå«è¯¥ ID | | 3 | çå®åæ® 1ï¼èç¨¿ï¼ | â `105_002`ï¼æ¥æå«ãè稿ã | | 4 | ç½® status=4 + check_result=NULL | â `105_003`ï¼æ¥æå«ãå°æªå¡«åæ£éªå¤å®ã | | 5 | æ¥æå + åºè´§æ¨¡æ¿ | â `105_005`ï¼æ¥æ**åæ¶**å«ãåºè´§æ£éªãä¸ãæ¥ææ£éªã | | 6 | IQC happy path | â `itemCount=14` / `undecidableCount=14`ï¼å«ã3 ä¸ªææ â¦åªä½åç»æ é¢ãï¼DB `business_type/business_id = 'mes_qc_iqc\t1'`ï¼å¿«ç § `qcResult='1'`/`qcResultText='åæ ¼'`/`resultText='å¾ å¤å®'` | | 7 | OQC å¤¹å ·ï¼æ¨¡æ¿ 5 带 item 级è§åï¼ | â 3 é¡¹å ¨å¤ï¼`undecidableCount=0`ï¼ï¼å¿«ç §å¸¦ç宿¹æ¬¡/客æ·/passRateï¼æ¸²æ HTML å«å®æµå¼ | | 7b | OQC å¤¹å · + **æ è§åä¸´æ¶æ¨¡æ¿** | â `itemCount=4` / `undecidableCount=1`ï¼**æææ åå®ä½**ï¼è²åº¦=PASS(30)ãå¯åº¦=PASS(850)ãæå ç=FAIL(1.3993)ãå¤è§=å¾ å¤å® ââ **è¿æ¡æ¯æ¬è½®æå ³é®çè¯æ®ï¼è§æ ¼ä¸ä¸éåæ¯ç¡®å®å¨ç¨çå® MES æ°æ®ç®** | | 8 | 忥 `businessType=mes_qc_iqc&businessId=1` | â æ¥å¾å°ç¨ä¾ 6 çå®ä¾ | | 9 | åªæåç»æ 颿æ ç OQC å¤¹å · | â `105_006`ï¼æ¥æå«ãåç»æ é¢ã | | 10 | 对ç¨ä¾ 7 çå®ä¾å¯¼ PDF | â `code=0` ä¸ `byteSize>1000` | | 11 | æ¸ çåæ æ®æ¸£ | â å®ä¾ / å¤¹å · / ä¸´æ¶æ¨¡æ¿ / éä»¶å ¨é¨å½é¶ï¼**å·²æ `deleted=0` ç»è®¡**ï¼ | **è°è¯è¿ç¨ä¸ç 5 个ã失败ãå ¨æ¯èæ¬æè¨åéï¼ä¸æ¯äº§å缺é·**ï¼éæ¡æ¥åºè¯ä¼ªåä¿®æ£ï¼ 1. ãè·³è¿åç»æ 颿æç¤ºãç regex åå¾å¤ªæ»ï¼`/3 ä¸ªææ åªä½åç»æ é¢/`ï¼ï¼å®é æ¥æä¸é´éäºãå¨è´¨æ£åéãï¼ æ¹æ `/3 ä¸ªææ .*åªä½åç»æ é¢/`ã 2. **ç¨ä¾ 7 çæææ¬èº«éäº**ï¼æ¨¡æ¿ 5 v1.1 é带äºä¸æ¡ item 级è§å ï¼`item.actualValue >= item.lowerLimit AND item.actualValue <= item.upperLimit`ï¼ï¼ å®**ä¼å äº**è§æ ¼ä¸ä¸éåæ¯ï¼æä»¥ 3 é¡¹å ¨è¢«å¤äºãæè¨æ¹å为å®çæ£è¯æçäºï¼å¹¶**æ°å¢ç¨ä¾ 7b** ç¨ æ è§åçä¸´æ¶æ¨¡æ¿å»çæ£é离ãè§æ ¼ä¸ä¸éãè¿æ¡åæ¯ã 3. ãæ¸²æäº§ç©å¸¦ç宿°æ®ã失败ï¼`renderHtml` éæ `850`ï¼ä½æ²¡ææ¹æ¬¡å·/客æ·åãæ½åºå ä½ç¬¦æ¸ åå确认 æ¨¡æ¿ 5 v1.1 çç»å¸**åªç»äº `{{report.reportNo}}` ä¸é¡¹çº§å段**ï¼åæ ¹æ²¡ç» `{{report.batchNo}}` / `{{report.customerName}}` ââ æ¯æ¨¡æ¿å 容é®é¢ï¼ä¸æ¯å端æ¼ä¼ ï¼æ æè¿ä¸¤é¡¹æè¨ç§»å°å¿«ç §ï¼å¿«ç §éç¡®å®æï¼ã 4. ãæ æ®æ¸£ãå¤±è´¥ï¼æèªå·±çæ¥è¯¢æ¼äº `AND deleted=0`ï¼ä¸¤æ¡è½¯å å®ä¾è¢«ç®ææ®æ¸£ï¼è¿æ£æ¯æ¬é¡¹ç® ææ¡£éè®°è¿çåï¼ãè¡¥ä¸æ¡ä»¶åå½é¶ã 5. é¡ºå¸¦æ ¸å® `storage_blob_id=20` 飿¡éä»¶æ¯**ä¸ä¸è½® AI å¯¼å ¥æè®¾è®¡ç»å®å°æ¨¡æ¿ 5 çæºæä»¶**ï¼ä¸æ¯æ¬è½®æ®çã ### 16.7 å端æ¥å ¥ï¼ç¨æ·ææãåå端ä¸èµ·æ¹ãåè½å°ï¼ | æä»¶ | æ¹å¨ | |---|---| | `src/components/quality/engine/context.ts` | `ReportContextReport` å `qcResult` / `qcResultText`ï¼`emptyReport()` 补空串é»è®¤å¼ï¼å·²ç¡®è®¤ `emptyReport()` æ¯å¯ä¸æé ç¹ï¼æ¹å¿ å¡«å®å ¨ï¼ | | `src/api/mes/qc/report/instance/index.ts` | å `GenerateFromQcReq` / `GenerateFromQcResult` 两个 interface + `generateInstanceFromQc()` | | `src/views/mes/qc/components/report-generate-modal.vue` | **æ°å¢**ï¼å个质æ£åé¡µå ±ç¨ï¼é模æ¿ï¼æ `reportType` è¿æ»¤ + åªçæçæçæ¬çï¼â çæ â å°±å°å±ç¤ºç»æä¸åè¦ â ãæ¥çæ¥åå表ã | | `src/views/mes/qc/{iqc,ipqc,oqc,rqc}/index.vue` | åå 5 å¤ï¼import å¼¹çªã`QC_TYPE` 常é + `useVbenModal`ã`handleReport`ã`<ReportModal />`ãè¡æä½ãåºæ¥åãï¼`ifShow: status === 已宿`ï¼ | | `src/views/mes/qc/report/instance/data.ts` | æç´¢è¡¨åå ãä¸å¡åæ®ãåæ®µï¼`businessId`ï¼**å端æ¯ç²¾ç¡®å¹é **ï¼placeholder å·²åæï¼ | | `src/views/mes/qc/report/instance/index.vue` | `onMounted` è¯»è·¯ç± query é¢å¡« `businessType` / `businessId` | **ä¸¤ä¸ªå»æçå¤ç** - **å¼¹çªæ é¢ä¸åè´¨æ£ç±»å䏿å**ï¼è¯¥åæ³ä¿çï¼ï¼`mes_qc_rqc` è¿ä¸è·¯æ¾æªè¾ä¸ä¸è´ââå端æä¸¾ã表注éã èåæéå项åãéè´§æ£éªãï¼èåç¶é¡¹ä¸ `mes_qc_type` åå ¸åãéææ£éªãã**æ¬è½®å·²ç»ä¸ä¸ºãéè´§æ£éªã**ï¼è§ §17.3ï¼ã - **`catch {}` éä¸åå¼¹æç¤º**ï¼åç«¯å¤±è´¥æ¥æå·²ç±è¯·æ±æ¦æªå¨ï¼`preset-interceptors.ts` æé â `api/request.ts` ç `errorMessageResponseInterceptor`ï¼èªå¨ toastï¼ä¸å¡ä»£ç å弹䏿¬¡ä¼åºç°ä¸¤æ¡ä¸æ ·çæ¥éã **æéç å ¥åº**ï¼`system_menu` æ°å¢ `id=1075416`ãæ¥åå®ä¾è´¨æ£ååºä»¶ãï¼ `permission=qc-report:instance:generate`ï¼`type=3`ï¼`sort=5`ï¼`parent_id=1075410`ï¼æ¥åå®ä¾ï¼ã **ä¸éè¦éæ°çæåå¹¶èæ¬** ââ `config_export_all_20260918.sql` é¶æ®µäºæ¯æ§è¡æ¶è·¨åº `INSERT INTO system_menu SELECT * FROM ruoyi-vue-pro.system_menu`ï¼æ°è¡ä¼èªç¶è¢«å¸¦è¿å»ã ### 16.9 å端 UI éªæ¶ï¼Playwrightï¼IFC ä¸è·¯èµ°éï¼ ç¨ Playwright MCP 对 `http://localhost:5666` çç¹äºä¸éï¼ç»è®ºï¼ | éªæ¶ç¹ | ç»æ | |---|---| | è稿å**䏿¾ç¤º**ãåºæ¥åã | â å¯ä¸é£å¼ IQC å `status=0` æ¶ï¼è¡æä½åªæ ä¿®æ¹/å é¤/详æ ï¼æ ãåºæ¥åãââ`ifShow: status === 已宿` çæ | | 已宿å**åªæ¾ç¤º**ãåºæ¥åã | â ä¸´æ¶æ `status=4` åï¼ä¿®æ¹/å 餿¶å¤±ããåºæ¥åãåºç°ï¼ä¸¤è æ¡ä»¶äºæ¥ï¼ç¬¦åè®¾è®¡ï¼ | | å¼¹çªæ é¢ | â `åºæ¥å - IQC_20260917001`ï¼**ä¸å¸¦ç±»å䏿å**ï¼è§é¿ RQC æªè¾åæ§ï¼ | | 模æ¿**é¢è¿æ»¤** | â åºé 3 å¼ æ¨¡æ¿ï¼åªååº 1 å¼ å¯éé¡¹ãæ¥ææ£éªæ¥åæ¨¡æ¿ v1.0ãââæ¨¡æ¿ 3ï¼åç±»åä½ `current_version` 为空ï¼ä¸æ¨¡æ¿ 5ï¼OQC ç±»åï¼é½è¢«æ£ç¡®æé¤ | | æªéæ¨¡æ¿æ¶ | â ãçææ¥åãç½®ç° | | ç»æå°±å°å±ç¤º | â `æ¥åå·²çæï¼QR20260919-0012` + `模æ¿çæ¬ v1.1ï¼åå ¥æ£éªé¡¹ 14 æ¡` + **é»è²åè¦ãæ¬æ¬¡æ 14 项缺å°è§æ ¼ä¸ä¸éä¸è§åï¼å¤å®ä¸ºãå¾ å¤å®ãã** + ãæ°æ®åèæç¤ºï¼1ï¼ã+ãä¸è½åºã说æ ââ **è¿æ£æ¯å£å¾ 2 è¦æ±çææ** | | 跳转 + çé | â è·³å° `/qc/report-instance?businessType=mes_qc_iqc&businessId=1`ï¼æç´¢è¡¨åé¢å¡«ãæ¥ææ£éªã+ ä¸å¡åæ® `1`ï¼å表**å ± 1 æ¡è®°å½**ï¼å°±æ¯ååºçé£ä»½ï¼ | **è¿è½®éªæ¶æå°ä¸ä¸ªå端çå®ç¼ºé·ï¼å·²ä¿®ï¼**ï¼æåæç¨ `formApi.setFieldValue` é¢å¡«æç´¢æ¡ä»¶ï¼ 读 `use-vxe-grid.vue` æç¡®è®¤è¡¨æ ¼åæç´¢æ¡ä»¶æ¯è¯» `formApi.getLatestSubmissionValues()` ï¼`extends.ts` ç `extendProxyOption` å è£ äº ajax queryï¼åªå**å·²æäº¤**çå¼ï¼ï¼ **åª set ä¸ submit çè¯æ¡ä»¶å¡«è¿è¡¨åä½åè¡¨ä»æ¥å ¨é**ââçäºæ²¡çã å·²æ¹ä¸º `setValues` + `submitForm()`ï¼å¹¶å äºæ³¨é说æä¸ºä»ä¹å¿ é¡»è·ç submitã **顺带åç°ä¸å¤åéæ°æ®ä¸ä¸è´ï¼ä¸æ¯æ¬è½®å¼å ¥ï¼æ¬è½®å·²ä¿® â §17.2ï¼**ï¼æ¨¡æ¿ 1 `QC_IQC_DEMO` ç `current_version = 'v1.0'`ï¼ä½çæ¬ v1.0 æ¯**å·²åç¨ï¼status=2ï¼**ãçæ£å·²åå¸çæ¯ v1.1ã äºæ¯**ç¨æ¨¡æ¿ 1 åºæ¥åå¿ ç¶å¤±è´¥**ï¼`1_070_101_003 模æ¿çæ¬æªåå¸ï¼æ æ³çææ¥å`ï¼ï¼ èå¼¹çªå伿å®åæå¯é项ï¼å端åªå¤ `currentVersion` éç©ºï¼æ¨¡æ¿å页æ¥å£ä¸è¿åè¯¥çæ¬çç¶æï¼**å端æ ä»å¤æ**ï¼ã å½è½®åªä¸´æ¶æ¹æ v1.1 èµ°å®éªæ¶ï¼**å·²è¿å为 v1.0**ï¼æªæ èªæ¹æ°æ®ã éªæ¶è¿ç¨ä¸´æ¶æ¹å¨å ¨é¨å·²è¿åï¼IQC å 1 æ¢å¤ `status=0` / `check_result=NULL`ï¼ æ¨¡æ¿ 1 æ¢å¤ `current_version='v1.0'`ï¼çæçå®ä¾ `QR20260919-0012` 已硬å é¤ï¼`qc_report_instance` è®¡æ° 0ï¼ã ### 16.10 ä¸ä¸æ¥ 1. ä¸ä¸è½®éçï¼ä¸å±æ¬èï¼ï¼`blobIds @Size(max=3)` ä¸ YAML `max-files` çè¦åã `@Disabled` çå® AI èè°æµè¯ãï¼ãå端ä»å¨ç§ AI è°ç¨ãå·²å¨ Â§17.3 æ¶å£ãï¼ 2. ~~æ¨¡æ¿ 1 ç `current_version` æ°æ®ä¸ä¸è´~~ â å·²å¨ Â§17.2 ä¿®å¤ã 3. ~~RQC æªè¾åæ§~~ â å·²å¨ Â§17.3 ç»ä¸ä¸ºãéè´§æ£éªãã 4. IPQC / OQC / RQC ä¸é¡µç UI æªéä¸ç¹è¿ï¼åæä»£ç ï¼é£é©ä½ï¼ã --- ## åä¸ãä¼åè½®ï¼å¼¹çªè¿æ»¤ / 误å ä¸ä¼ æä»¶ / AI æ»é¢ç® / 两å¤å¾ è£å³é¡¹ å项䏿¬¡æ§åå®ï¼åä¸ºå¯¹æ¢æè½åç补强æç¼ºé·ä¿®å¤ï¼æ æ°å¢ä¸å¡å¯¹è±¡ã ### 17.1 åºæ¥åå¼¹çªè¡¥ãå½åçæ¬å·²åå¸ãæ ¡éª **é®é¢**ï¼å¼¹çªåªæ `reportType` + æ¨¡æ¿ `status=å¯ç¨` æ¥æ¨¡æ¿å页ï¼åå¨åç«¯å¤ `currentVersion` é空ã ä½ãå½åçæ¬æ¯å¦å·²åå¸ãæ¯**çæ¬è¡¨**ä¸çç¶æï¼å页æ¥å£ä¸è¿åï¼è**åç¨ä¸ä¸ªçæ¬ä¸ä¼æ¸ 空模æ¿ç `currentVersion`**ï¼ äºæ¯ `currentVersion` æå¼ãé£ä¸ªçæ¬å·²åç¨ç模æ¿ç §æ ·è¢«ååºï¼éä¸åå¿ ç¶æ¥ `1_070_101_003`ã æ¨¡æ¿ 1 å°±æ¯è¿ç§ï¼è§ §16.9ï¼ã **æ¹æ³**ï¼æ°å¢ `GET /qc-report/template/selectable?reportType=`ï¼æé `qc-report:template:query`ï¼ï¼ å¨**å端**䏿¬¡æ§åä¸éè¿æ»¤ï¼æ¨¡æ¿ `report_type` ä¸è´ â æ¨¡æ¿ `status=å¯ç¨` â `current_version` æåççæ¬ `status=å·²åå¸`ã ä¸¤æ¡ SQL è§£å³ï¼æ N+1ï¼å æ¥æ¨¡æ¿ï¼åç¨ `templateId IN (...)` æ¹éæ¥å·²åå¸çæ¬ï¼å¨å åéæ `templateId@version` æ±äº¤éï¼ã å端æ¹ç¨è¿ä¸ªæ¥å£ï¼ä¸åèªå·±æ¨å¯¼è§åã **为ä»ä¹ä¸æçæ¬ç¶æå¡è¿æ¨¡æ¿å页çååº**ï¼é£è¦å¨å表é对æ¯è¡åæ¥ä¸æ¬¡çæ¬è¡¨ï¼N+1ï¼ï¼ èä¸ãçæ¬å¿ é¡»å·²åå¸ãè¿æ¡è§åä¼è·å°å端å»ï¼æä¸ºç¬¬äºä¸ªçç¸æ¥æºã **顺带记å½ä¸ä¸ªä»£ç å±çæ´ï¼æ¬è½®æªæ¹ï¼éå³çï¼**ï¼`QcReportTemplateVersionServiceImpl.disableVersion` åç¨çæ¬æ¶**䏿¸ 空 `template.currentVersion`**ï¼æä»¥ãåç¨å½åçæ¬ãè¿ä¸ªæä½æ¬èº«å°±è½é åºä¸é¢é£ç§ä¸ä¸è´æ°æ®ã æ¬è½®åªæ¡ä½äºãç¨æ·çå°å¹¶éä¸å®ãï¼æ²¡å µä½ã产çå®ããè¦ä¸è¦å¨åç¨å½åçæ¬æ¶ç´æ¥æç» ï¼æç¤ºãè¯·å æå½åçæ¬åæ»å°å¦ä¸ä¸ªå·²åå¸çæ¬ãï¼æ¯ä¸å¡å£å¾é®é¢ï¼æªæ èªå³å®ã ### 17.2 æ¨¡æ¿ 1 æ°æ®ä¿®å¤ `qc_report_template` id=1ï¼`current_version` ç± `v1.0`ï¼status=2 å·²åç¨ï¼æ¹ä¸º `v1.1`ï¼status=1 å·²åå¸ï¼ã æ¹åæ¨¡æ¿ 1 æ¢è½è¢«æ°æ¥å£ååºãä¹ççè½åºæ¥åãè¿æ¯æ°æ®ä¿®å¤ï¼ä¸æ¹ä»£ç ã ### 17.3 RQC æªè¾ç»ä¸ä¸ºãéè´§æ£éªã åå çåæ§é¢ï¼ | ä½ç½® | åæ | |---|---| | `MesQcTypeEnum.RQC.name`ã`QcReportSourceTypeEnum.RQC.name`ã`MesQcReportContextMapper` ç¸å ³ææ¡ | éè´§æ£éª | | `mes_qc_rqc` 表注éã`system_menu` id=5671~5675ï¼æéåé¡¹ï¼ | éè´§æ£éª | | `system_menu` id=5670ï¼èåç¶é¡¹ï¼ã`system_dict_data` id=3124 ç label | éææ£éª | | å端 `instance/data.ts` ç `BUSINESS_TYPE_LABEL`ã`pendinginspect/index.vue` ä¸¤å¤ | éææ£éª | **ç»ä¸å°ãéè´§æ£éªã**ï¼çç±ï¼â 宿¯å¤æ°æ´¾ï¼å端 Java æä¸¾é¶å¤åºç°ãéææ£éªãï¼ï¼ â¡ RQC çå¯¹è±¡åæ¶è¦çç产éæãéå®éè´§ãå®åéè´§ï¼ãéè´§æ£éªãæ¯è½è¦çä¸è ç说æ³ï¼ãéææ£éªãåªè¦çå ¶ä¸ï¼ ⢠`system_dict_data` id=3124 ç `remark` æ¬æ¥å°±åçãéè´§è´¨éæ£éªãï¼è¯´æãéæãæ¯ label åç¬åå²çã **æ¹å¨ç¹**ï¼`system_menu` id=5670 ç `name`ã`system_dict_data` id=3124 ç `label`ã å端 `instance/data.ts` + `pendinginspect/index.vue`ï¼`name` å置串ä¸è¡æä½ labelï¼ã `docs/sql/config_export_all_20260918.sql` é id=3124 çåé¢éã > èååæ¹å¨**ä¸éè¦éæ°çæåå¹¶èæ¬**ï¼é¶æ®µäºæ¯æ§è¡æ¶è·¨åº `INSERT INTO system_menu SELECT * FROM ...`ã > åå ¸æ¯é¶æ®µä¸çåé¢éï¼æä»¥æå·¥æ¹äºé£ä¸è¡ä»¥ä¿æä¸è´ï¼åªæ¹ label ä¸ä¸ªåé¢éï¼ä¸å¿ æ´ä»½é导ï¼ã ### 17.4 AI å¯¼å ¥å 请æ±çº§æ»èæ¶é¢ç® **é®é¢**ï¼`max-files` / `max-pages-per-file` / `max-pages-per-request` åªçº¦æè°ç¨**次æ°**ï¼ä¸çº¦æ**æ¶é¿**ã æ¨¡åæéæ¶æ¯æ¬¡è°ç¨é½è½è满 `yudao.ai.timeout`ï¼60sï¼ï¼8 次å å è½æä¸æ¬¡è¯¯æä½ææ 8 åéçä»è´¹è°ç¨ï¼ èå端 axios 180s å°±æäºï¼ç¨æ·å¨æçº¿åå®å ¨ä¸ç¥éå端è¿å¨è·ã **æ¹æ³**ï¼æ°å¢ `yudao.qcreport.ai-import.max-duration-seconds`ï¼é»è®¤ 180ï¼ä¸¤ä¸ª profile 忥ï¼ã å¨**两次模åè°ç¨ä¹é´**æ£æ¥å·²èæ¶ï¼è¶ äºå°±åæ¢åç»è°ç¨ï¼ - å·²è¯å«å°çé¨å**ç §å¸¸è¿å**ï¼å¹¶å¨ `warnings` éæåã已达å°åæ¬¡å¯¼å ¥èæ¶ä¸éï¼180 ç§ï¼å·²ç¨æ¶ X ç§ï¼ï¼ åç»å 容æªåè¯å«ãââè¿æ¯æ¾å¼æªæï¼ä¸æ¯é黿ªæï¼ä¸æ¨¡åæ¢æå£å¾ä¸è´ã - 妿ä¸ä¸ªç»ä»¶é½æ²¡è¯å«åºæ¥ï¼æ¹ææ°é误ç `AI_IMPORT_AI_BUDGET_EXCEEDED`ï¼`1_070_104_016`ï¼ï¼ è䏿¯ `AI_IMPORT_DRAFT_EMPTY`ââ两è ç¨æ·è¦éåçå¨ä½å®å ¨ä¸åï¼åè åæ¹ï¼åè æ¹ç¨æå·¥è®¾è®¡ï¼ã - è¿æ¯ä¸ªæä»¶ä¹å乿£æ¥ä¸æ¬¡ï¼è¶ äºå°±ä¸å读ä¸ä¸ä»½ãä¸å渲æå®ç页ï¼çæç½è±ç CPU ä¸å åï¼ã **为ä»ä¹åªè½å¨ä¸¤æ¬¡è°ç¨ä¹é´å ³é¨**ï¼`AiChatApi` 䏿¥åè¶ æ¶åæ°ï¼å次è°ç¨æ æ³ä¸éææï¼ æä»¥æåä¼å¤åºä¸ä¸ª `yudao.ai.timeout`ãå æ¤**å端 axios è¶ æ¶ä» 180s æå° 300s** ï¼180 é¢ç® + 60 忬¡è¶ æ¶ = 240 æåï¼ç 60s ä½éï¼ââåå 180s åèæ¯å端æåæ 嵿´çï¼ ç¨æ·ä¼å çå°æµè§å¨æå¼è䏿¯åç«¯é£æ¡è®²æ¸ åå çæç¤ºãå 180 ç§ä½ä¸ºé¢ç®ï¼ å¸¸è§ 8 次以å çè°ç¨é½è·å¾å®ï¼åªææ¨¡åææ¾åæ ¢æ¶æè§¦åã ### 17.5 ä¿®å¤ ERP / CRM 两å¤è¯¯å ç¨æ·ä¸ä¼ æä»¶ **é®é¢**ï¼`ErpPurchaseInvoiceAiServiceImpl` ä¸ `CrmSaleQuotationAiServiceImpl` å¨ `finally` é `tempFile.delete()`ï¼èé£ä¸ª `File` æ¥èª `SystemStorageBlobService.getPublicFile(...)` ââ **宿¯ç£çä¸çå®åå¨çé£ä»½ blobï¼ä¸æ¯ä¸´æ¶å¯æ¬**ãçäºæ¯æ¬¡ AI è¯å«å®å°±æç¨æ·ä¸ä¼ çåå§å票/æ¥ä»·æä»¶å äºã **æ¹æ³**ï¼å»ææ´ä¸ª `finally` å é¤åï¼åéå `tempFile` æ¹ä¸º `blobFile` å¹¶å ä¸è¡æ³¨é说æãè¿ä¸æ¯ä¸´æ¶å¯æ¬ï¼è¯»å®ä¸è½å ãã å½åè¯¯å¯¼æ£æ¯è¿ä¸ªç¼ºé·çæå ï¼çç容æåç¯ã > è®°çãä¸å¤ãå®ä¸º**两å¤**ï¼`MesQcAiServiceImpl` æ¯çº¯ææ¬å¤å®å»ºè®®ï¼å ¥åæ¯ VOï¼ä¸ç¢°æä»¶ï¼ï¼æ²¡æè¿ä¸ªç¼ºé·ã ### 17.6 æ¬è½®æªå - `disableVersion` åç¨å½åçæ¬æ¶çæ¦æªï¼è§ §17.1 æ«ï¼ââéä¸å¡å£å¾ã - AI å¯¼å ¥ç `blobIds @Size(max=3)` ä¸ YAML `max-files` çè¦åã - `@Disabled` ççå® AI èè°æµè¯ã - IPQC / OQC / RQC ä¸é¡µç UI éä¸ç¹éªï¼åæä»£ç ï¼ã ## åå «ãä¼åè½®ï¼äºï¼ï¼ç»å¸åå ¥ä¾§æ ¡éª / åç¨å½åçæ¬æ¦æª / 空ç»å¸å¤æ® / ä¸å¤æ¶å°¾ï¼2026-09-19 ç»ï¼ å项忝 §17.6 éç项çè½å°ææ¬è½®æ°åç°ç缺é·ä¿®å¤ï¼æ æ°å¢ä¸å¡å¯¹è±¡ãæ å»ºè¡¨ãæ æ¸²æå¼ææ¹å¨ã **å ¨é¨ä¸ºå端æ¹å¨ï¼æ¬è½®æªå¨ä»»ä½å端æä»¶ã** ### 18.1 ç»å¸åå ¥ä¾§å®å ¨æ ¡éªï¼æ°å¢ `CanvasSafety`ï¼ **é®é¢**ï¼æ¥åäº§ç©æ¯æ¼å符串æ¼åºæ¥çï¼æ¨¡æ¿ç»å¸æ¯ç¨æ·å¯ç¼è¾æ°æ®ãæ¥ `HtmlRenderer` åç¡®è®¤æ³¨å ¥é¢**æ¯é¢æ³çå¤ä¸¤å¤**ï¼ ä¸å ±ä¸å¤å 容ä¼**ä¸ç»è½¬ä¹**è¿å ¥äº§ç©ï¼ | æ¼æ¥ç¹ | ä½ç½® | åæ | |---|---|---| | èç¹ `tagName` ç´æ¥æ¼æ `<tag>` | `resolveTag` | å¯å `<script>`ï¼å `"img src=x onerror=alert(1)"` è¿è½æ´æ®µå¡è¿ä¸ä¸ªäºä»¶å±æ§ | | 屿§**å**ç´æ¥æ¼ä¸ï¼åªæå±æ§**å¼**转ä¹ï¼ | `renderAttributes` | ä¸ä¸ªå«å¼å·ç屿§åå°±è½æåé¢çå å®¹é¡¶ææ°å±æ§ | | `styles` åæå符串æ¶**åæ ·**å¡è¿ `<style>` | `buildCss` | åºç° `</style>` 峿åéåï¼åé¢åæç HTML | > å计åçâ£åªæäº `tagName` 䏿¡ã读渲æå¨åæå®é æ³¨å ¥é¢æ©æä¸æ¡ââå°å µä¸¤æ¡ï¼ç½ååå°±çäºæ²¡åã **æ¹æ³**ï¼æ°å¢ `engine/render/CanvasSafety.validate(grapes) â List<String>`ï¼å¨ `validateSchema` éè°ç¨ ï¼`createVersion` ä¸ `updateVersion` 两æ¡å ¥å£å ±ç¨å䏿®µæ ¡éªï¼ä¸åæ ¼**ä¸å¾ä¸è½åº**ï¼ï¼ - æ ç¾èµ°**ç½åå**ï¼å¸¸è§æçä¸ææ¬æ ç¾ï¼ï¼ä¸ç¨é»ååââé»ååæ°¸è¿æ¼ï¼èæ¥åæçç¨å¾å°çæ ç¾æ¯å¯æä¸¾çï¼ - 屿§åæåç¬¦éæ ¡éª + `on` åç¼å¤äºä»¶å±æ§ï¼ - `styles` å¿ é¡»æ¯è§åæ°ç»ï¼ä¸ `selectors` / `mediaText` / æ ·å¼å±æ§åä¸å¼éåºç° `<` 峿ç»ï¼ - 䏿¬¡æå¤æ¥ **8 æ¡**ï¼æ¯æ¡å¸¦ãä½ç½® + è¿è§å 容ãï¼å¤æ¡ä»¥ `ï¼` è¿æ¥ã **为ä»ä¹å¨ä¿åæ¶å¡ï¼è䏿¯å¨æ¸²ææ¶å¡**ï¼æ¸²æé¾è·¯ä¸ç `HtmlRenderer` ä¸å端 `engine/render.ts` å¿ é¡»**éåä¸è´**ï¼ æ¹å®å°±è¦éç®å¯¹æäº§ç©ã并让å鿥åç `regenerate` ä¸åéåå¤ç°ããå ¥ç«å 容æ¯å¦å¯ä¿¡ãæ¬æ¥å°±æ¯åå ¥ä¾§çé®é¢ï¼ 卿°æ®è¿åºåæ¦æï¼æ¸²æä¾§å¯ä»¥ç»§ç»ä¸å端å®å ¨å¯¹ç§°ã代价æ¯**å¯¹æ ¡éªä¸çº¿åå·²å ¥åºçèæ°æ®æ²¡æå åº**ã **ä¸è¯¯æçä¸ç±»**ï¼å®ç°éæç¡®æé¤ï¼åæåæµï¼ï¼`textnode` ä¸ç `tagName`ï¼æ¸²æå¨ä¸è¯»ï¼ã GrapesJS æå¨èç¹ä¸ç `docEl`/`head` å æ°æ®ï¼æ¸²æå¨ä¸è¯»ï¼æ ¡éªåªæ²¿ `components` èµ°ï¼ãåéçå®ç»å¸ã **æ°å¢æµè¯** `CanvasSafetyTest`ï¼17 项ï¼å« 6 个çå®åéç»å¸çåæ°åç¨ä¾ï¼ã å ¶ä¸ `acceptsRealStoredCanvases` æ¯æåºéå ¨é¨çæ¬å¯¼æå¤¹å ·ï¼`src/test/resources/qcreport/canvas/`ï¼ï¼ ç¨æ¥å®ä½ãç½ååä¸è®¸æ¶å¾è¿ç´§ãââ宿¯ãæç»èå 容ãé£å æ¡æ´éè¦ï¼å 为误æçåææ¯ç¨æ·çªç¶åä¸äºæ¨¡æ¿ã > åè¿æ¹ç¨ä¾æ¶è¸©è¿ä¸æ¬¡åï¼æåæ `components` æå¨ `grapes` æ ¹ä¸ï¼èæ ¡éªå¨ï¼æ£ç¡®å°ï¼ä¸æ¸²æå¨ä¸è´ï¼åªæ²¿ > `pages[0].frames[0].component` èµ°ï¼äºæ¯ãæç»ãç±»æè¨å ¨é¨**åéè¿**ï¼ä»ä¹é½æ²¡æ£æ¥ï¼ãå·²æ¹ä¸ºå ¨é¨æçå®ç»å¸å½¢ç¶åã > å®å ¨æµè¯çåéè¿æ¯ä¸åæ´å±é©ã ### 18.2 åç¨ãå½åçæ¬ãç´æ¥æç»ï¼`TEMPLATE_VERSION_CURRENT_CANNOT_DISABLE` = `1_070_101_010`ï¼ **é®é¢**ï¼Â§17.1 æ«è®°çé£ä¸ªã产çå®ãçæ´ï¼ï¼`disableVersion` åç¨çæ¬æ¶ä¸æ¸ 空 `template.currentVersion`ã åç¨å½åçæ¬ä¼é åºèªç¸çç¾ç模æ¿ââ`current_version` è¿æçå®ãå®å´å·²ä¸å¯ç¨ï¼è¯¥æ¨¡æ¿èªæ¤åºä¸äºæ¥åï¼ ä¸å¤±è´¥åçå¨**åºä»¶é£ä¸å»**ï¼`1_070_101_003`ï¼ï¼ç¨æ·ä¸ä¼æ³å°èµ·å æ¯ä¹å飿¬¡åç¨ã **æ¹æ³**ï¼`disableVersion` éæ¯å¯¹æ¨¡æ¿ `current_version` ä¸è¯¥çæ¬ `version`ï¼ç¸çç´æ¥æç»ã **ä¸é¡ºææ¿ç¨æ·æ¹ `current_version`**ââãæ¹åªä¸ªçæ¬ãæ¯ç¨æ·çå³å®ï¼ä¸è¯¥ç±ãåç¨ãè¿ä¸ªå¨ä½ææä»£å³ï¼ ææ¡éç»åºä¸¤æ¡åºè·¯ï¼å åæ»å°å¦ä¸ä¸ªå·²åå¸çæ¬ / æ¹ä¸ºåç¨æ´ä¸ªæ¨¡æ¿ï¼ã å£å¾ï¼åªæ¯å符串ï¼**ä¸ççæ¬æ¯å¦å·²åå¸**ï¼èç¨¿åæ ·å¯è½è¢«æå°ï¼ï¼åç¨éå½åçæ¬çè¡ä¸ºä¸åã ### 18.3 `hasCanvas` 夿®å¼ºå + `/selectable` 补第ä¸éè¿æ»¤ï¼æ¬è½®èªè¡è¿½å ï¼éåå®èå´ï¼ **åç°**ï¼å¯¼åºåºä¸å ¨é¨çæ¬åæ ¸å¯¹ï¼æ¨¡æ¿ 1 çå½åçæ¬ï¼id=2, v1.1, å·²åå¸ï¼ç `grapes` æ¯ `{"assets":[],"styles":[]}`ââ**é空ï¼å´ä¸ä¸ªç»ä»¶é½æ²¡æ**ãè `hasCanvas` 彿¶ç夿®æ£æ¯ãgrapes è¿ä¸ª Map é空ãï¼ äºæ¯å®è¢«å¤ä¸ºæå 容ï¼ç»§ç»èµ°æ¸²æï¼`HtmlRenderer` å `pages[0].frames[0].component` åå° nullï¼ **æ body 渲ææç©ºä¸²ã䏿¥é**ï¼ç¨æ·æ¿å°ä¸å¼ **ç½çº¸ PDF**ãè¿æ¯æ¥éæ´ç³ï¼ç¨æ·ä¸ç¥éè¦æ¹ä»ä¹ã **æ¹æ³**ï¼æ `hasCanvas` ç夿®æ¹æä¸æ¸²æå¨è¯»ç»å¸çé£ä¸è¡**ä¸¥æ ¼ä¸è´**ï¼ ``` Paths.readPath(grapesOf(schema), "pages[0].frames[0].component") instanceof Map<?, ?> ``` å®åæ¶å®ç两å¤ï¼`getSelectableTemplates`ï¼è¦ä¸è¦æè¿ä¸ªæ¨¡æ¿æè¿åºæ¥åå¼¹çªï¼ä¸ `resolveVersion`ï¼åºä»¶åè¦ä¸è¦ç´æ¥æç»ï¼æ¥æ¢æç `1_070_101_008`ï¼ãä¸ä¸ªå¤æ®åç¹å®ä¹ï¼ä¸¤å¤å£å¾ä¸ä¼æ¼ã **å½±åé¢å·²å®æµ**ï¼å ¨åº 6 ä¸ªçæ¬ä¸ï¼åªæ id=1ãid=2ï¼é½æ¯æ¨¡æ¿ 1 ç空ç½ç»å¸ï¼ç±ãéè¿ãåãæç»ãï¼ 4 ä¸ªçæ£ç»äºä¸è¥¿ççæ¬ä¸åå½±åãæ¥è¡¨å®ä¾å ¨åºåªæ 1 æ¡ï¼æ¨¡æ¿ 3 v1.0ï¼ï¼ä¸è¢«æ³¢åã **`/selectable` éä¹è¡¥ä¸ç¬¬ä¸éè¿æ»¤**ï¼åéçæ¬è¡æ¬æ¥å°±æ´è¡è¯»åºæ¥ï¼å« `schema_json`ï¼ï¼ æ¿å®å¤ `hasCanvas` æ¯ç½æ¡çï¼**没æé¢å¤æ¥åº**ï¼æ¹æ exists åæ¥è¯¢åèå¤ä¸æ¬¡å¾è¿ã > è¿ä¸æ¡è¶ åºäºåå®çå项èå´ï¼å±è¯»ä»£ç æ¶åç°å¹¶é¡ºæå µæç缺é·ï¼å¨æ¤åå说æã ### 18.4 廿 AI å¯¼å ¥ `blobIds` ä¸åæ»ç `@Size(max=3)` **é®é¢**ï¼å ¥å VO 䏿ç `@Size(max = 3)`ï¼èä¸éçæ£ç± `yudao.qcreport.ai-import.max-files` å³å®ã 两个æ°åå¹¶åçç»æåªä¼æ¯**注解æ´ä¸¥ãé 置失å»å¯è°æ§**ï¼é ç½®åªæ¢å¾ä¸è°ï¼ï¼ä¸ä¸åæ¥æ¶ç¨æ·çå°çä¸éä¸çæçä¸é对ä¸ä¸ã **æ¹æ³**ï¼å ææ³¨è§£ï¼ææå¡å±ç `validateFileCount` 声æä¸º**å¯ä¸é¸é¨**ã 已确认å®çè°ç¨ä½ç½®å¨ `generateDraft` æå段ã**ä»»ä½ blob 读åä¹å**ï¼æä»¥æ¾å¤§æä»¶æ°ä¸å¸¦æ¥é¢å¤çç£ç/å åå¼éï¼ æ³¨è§£é£å±æ¦æªå¹¶æ å¿ è¦ã ### 18.5 模æ¿è¡çº¸åå¯¹é½ + `pdf-e2e.mjs` ä¿®æ£ **æ°æ®ä¾§**ï¼æ ¸å¯¹åï¼ä¸å¼ 模æ¿è¡éåªææ¨¡æ¿ 1 ç `orientation`ï¼portraitï¼ä¸å ¶å½åçæ¬ï¼landscapeï¼ä¸ä¸è´ï¼ æ¨¡æ¿ 3ãæ¨¡æ¿ 5 æ¬æ¥ä¸è´ï¼Â§17 æ¶ä»¥ä¸ºçãæ¨¡æ¿ 5 ä¸ä¸è´ãæ¯å ¶ v1.0 çï¼è v1.0 䏿¯å½åçæ¬ï¼ã å·²ææ¨¡æ¿ 1 ç `page_size`/`orientation` 对é½å°å½åçæ¬ï¼å ¨è¡¨ä¸è¡ç°å¨é½ä¸å ¶å½åçæ¬ä¸è´ã > 夿³¨ï¼`qc_report_template.page_size` / `orientation` **å ¨é¡¹ç®æ è¯»åæ¹**ï¼åªæ DO ä¸ RespVO 声æï¼ï¼ > 渲æä¸å¾å Schema éç `page`ãå®ä»¬åªæ¯æ¨¡æ¿å表页çå±ç¤ºå æ°æ®ââæä»¥è¿æ¬¡å¯¹é½åªå½±åå表æ¾ç¤ºï¼ > 䏿¹ä»»ä½æ¸²æç»æãæ¨¡æ¿ 1 çä¸¤ä¸ªçæ¬ç»å¸é½æ¯ç©ºçï¼è§ §18.3ï¼ï¼å®å¨ `/selectable` éå·²è¢«è¿æ»¤æï¼ > å æ¤è¿æ¬¡å¯¹é½å¯¹å®æ¯ãæ¾ç¤ºä¸è´ãèéãå¯ç¨ãã **èæ¬ä¾§**ï¼`D:/qcl-tmp/pdf-e2e.mjs`ï¼ä»åºå¤ï¼ï¼ 1. ä¸ååæ» `version: 'v1.0'`ï¼æ¹ä¸ºå æ¥æ¨¡æ¿ç `currentVersion` å带ä¸ââ åæ»ççæ¬å·ä¼å¨æ¨¡æ¿åæ°çæ¬åè®©èæ¬**ææå»éªä¸ä¸ªè¿æçåå²çæ¬**ï¼èæä»¬ä»¥ä¸ºå®è¿å¨éªå½å模æ¿ã 2. æè¨ `3.4` åå åæ»ãA4 纵å 595.28x841.89ptããä½ `orientation` **æ¯çæç** ï¼`PageSizes.resolve` 横åä¼ç¿»è½¬å®½é«ï¼ï¼ç®æ çæ¬æ¹ä¸ºå½åçæ¬å该æè¨å¿ ç¶è¯¯æ¥ã æ¹ä¸ºä»å®ä¾ HTML ç `@page { size: Wmm Hmm }` åçå¼ãæ¢ç®æ pt åä¸ PDF ç MediaBox æ¯å¯¹ââ èªæ´½ä¸ä¸é纸åååè失æã 3. æ°å¢æè¨ `0.5.1`ï¼æ¨¡æ¿è¡ç纸åä¸å½åçæ¬ Schema ç纸åä¸è´ãè¿æ£æ¯ §18.5 æ°æ®ä¾§é£æ¡ä¸åéçæºå¨æ£æ¥ï¼ 以ååææ¼ç§»ä¼è¢«è¿æ¡æä½ã ### 18.6 éªè¯ç¶æï¼å·²å / æªååå¼å£°æï¼ **å·²éªè¯**ï¼ - `mvn compile -q -o` å ¨é¡¹ç®ç¼è¯éè¿ï¼æ¬è½®æ¹äº qcreport ä¸ aiãsystem ä¸ä¸ªæ¨¡åï¼ã - `mvn -pl yudao-module-qcreport test -o` â **`Tests run: 132, Failures: 0, Errors: 0`** ï¼å 106 项 + æ°å¢ `CanvasSafetyTest` 17 项 + æ°å¢ `QualityReportEngineTest` 9 项ï¼ï¼å ¨ç»¿ã - å ¨åº 6 ä¸ªçæ¬è·ã强åå / 强ååã夿®å¯¹ç §ï¼ç¡®è®¤å½±åé¢**åªææ¨¡æ¿ 1 ç两个空ç»å¸**ã - 模æ¿è¡çº¸å对é½åå读ä¸è¡ï¼ç¡®è®¤å ¨é¨ä¸å½åçæ¬ä¸è´ã **HTTP 宿µå·²å®æï¼2026-09-19ï¼ç¨æ·ä¸¤æ¬¡éå¯å端åï¼**ï¼ - `/d/qcl-tmp/verify-round2.mjs` â **21/21 PASS**ï¼A ç» 6 + B ç» 7 + C ç» 8ï¼ã - **A ç» `/selectable`**ï¼å è¯ææ¨¡æ¿ 1 卿®éå表éåå¨ã`status=0`ï¼å¯ç¨ï¼ã`current_version=v1.1` ç¶æ `1`ï¼å·²åå¸ï¼ã `grapes` é¡¶å±é®åªæ `assets,styles`ï¼**éç©ºä½æ pages**ï¼ââå³å两éè¿æ»¤å®è¿å¾å»ï¼ å¯ä¸è½æ¡å®çå°±æ¯ç¬¬ä¸éï¼éå `/selectable?reportType=1` è¿å `[3]`ï¼**æ¨¡æ¿ 1 æ¶å¤±**ï¼ è `reportType=3` ä»è¿åæ¨¡æ¿ 5ï¼ç¬¬ä¸é没æä¸åå误伤ï¼ï¼`reportType=2` è¿å空æ°ç»è鿥éã - **B ç» `disableVersion`**ï¼å¨ä¸´æ¶æ¨¡æ¿ä¸åï¼ä¸ç¢°ç宿°æ®ï¼ï¼åç¨éå½åçæ¬æåä¸ `status` è½å° 2ï¼ åç¨å½åçæ¬è¿å `1070101010`ï¼ææ¡åæ¶å¸¦åºçæ¬å·ä¸æ¨¡æ¿åï¼å¹¶ç»åºãåæ»åèµ° / åç¨æ´ä¸ªæ¨¡æ¿ã两æ¡åºè·¯ï¼ å¦å 䏿¡**ã被æåå½åçæ¬ç¶ææªè¢«æ¹å¨ã**ï¼è¯ææç»æ¯ç没ååºï¼è䏿¯å ååæã - **C ç» `CanvasSafety`**ï¼å ç§æ³¨å ¥å½¢æï¼`<script>` æ ç¾ã`tagName` 夹 `onerror`ã`onclick` äºä»¶å±æ§ã 屿§å夹å¼å·ã`styles` æ´æ®µ CSS åç¬¦ä¸²ãæ ·å¼è§åå¼å« `<`ï¼**éä¸è¿å `1070101009` 䏿æ¡åèªç¹æåå **ï¼ æ£å¸¸ç»å¸ï¼å«è¡¨æ ¼/å¾ç/hr/ææ¬èç¹ï¼æ¾è¡ï¼`update` è·¯å¾åæ ·è¢«æ¦ï¼ä¸åªæ `create` æé¸é¨ï¼ã - `/d/qcl-tmp/pdf-e2e.mjs` éè· â **20/20 PASS**ãæ¬è½®æ°å /æ¹åçä¸¤æ¡æè¨é½è¿äºï¼ `0.5.1 模æ¿è¡ç纸åä¸å½åçæ¬ Schema ç纸åä¸è´ | 模æ¿è¡=A4/portrait çæ¬ v1.1=A4/portrait`ï¼ `3.4 纸é¢å ä½ä¸æ¸²ææ¶å£°æç @page ä¸è´ | @page=210x297mm ææâ595.28x841.89pt å®é =594.96x841.92pt` ï¼ä¸ååæ»çºµåï¼æ¨ªåçæ¬ä¹ä¸ä¼è¯¯æ¥ï¼ã60 é¡¹é¿æ¥å 3 页ã䏿åä½å·²åµå ¥ã - `/d/qcl-tmp/verify-budget.mjs` â **6/6 PASS**ï¼`max-duration-seconds` ä¸´æ¶æ¹ 0 + éå¯åè·ï¼è·å®å·²è¿å为 180ï¼ï¼ `1070104016` èé `DRAFT_EMPTY`ï¼ææ¡å¸¦åºçæé¢ç®ï¼0 ç§ï¼ä¸å®é èæ¶ï¼å¹¶ç»åºãåå°æä»¶/æ¥æ¨¡åã两æ¡å¨ä½ï¼ 端å°ç«¯ **29â84 ms** è¯æ**䏿¬¡æ¨¡åè°ç¨é½æ²¡å**ï¼çè°ä¼æ¯æ°å毫ç§ï¼ï¼ å¯¹ç §ç»è¯æå ¥åæ ¡éªï¼`schemaVersion` â `1070104009`ï¼æå¨é¢ç®æ£æ¥ä¹åã - **ä¸ä¸ªåªå¨æç«¯é ç½®ä¸æåºç°çææ¡ççµï¼æªä¿®ï¼å¤å®ä¸ºä¸å¼å¾ä¿®ï¼**ï¼é¢ç®ä¸º 0 æ¶å¾ªç¯å¨è¯»å°ç¬¬ä¸ä¸ªæä»¶**ä¹å**就跳åºï¼ `fileNames` 为空ï¼äºæ¯ææ¡åºç°ã卿件ãã䏿ªè¯å«åºä»»ä½å¯ç¨ç»ä»¶ããçå®é ç½®ï¼180 ç§ï¼ä¸å¿ ç¶å·²ç»è¯»è¿è³å°ä¸ä¸ªæä»¶ï¼ è¿ä¸ªç©ºä¸²å½¢æé ä¸åº 0 å°±å¤ç°ä¸äºï¼å±æµè¯ä¸ç¨å½¢æã - **ä¸¤å¤ FAIL å ¨æ¯æè¨åéã䏿¯äº§å缺é·**ï¼`1_070_104_016` çæ°å¼æ¯ **`1070104016`**ï¼ æç¬¬ä¸çæ `1_070_104_016` ç´åæ `10701010416`ï¼å¤ä¸ä½ï¼ã**æ®µå·æ¼æ¥è§åï¼`1|070|104|016` â 廿åé符**ï¼ åæè¨åå çæ¥å£çå®è¿åå¼ï¼å«ç §æé误ç 常ééçä¸å线形å¼ã **æµè¯æ°æ®å·²æ¸ å¹²å**ï¼ä¸´æ¶æ¨¡æ¿ 32 åå ¶ 3 ä¸ªçæ¬ç¡¬å ï¼`pdf-e2e` é çå®ä¾ 46/47ãéä»¶ 69~72ãblob 63~66 è¿åç£çæä»¶ä¸å¹¶æ¸ æï¼é¢ç®ç¨ä¾ç 2 个å ä½ blobï¼68/69ï¼ä¸ç£çæä»¶åæ ·æ¸ æã å读ä¸å¼å·¥åä¸è´ï¼`qc_report_template` 3 è¡ã`version` 6 è¡ãå®ä¾åªå© 44/45ãblob ä¸é 67ãéä»¶ä¸é 73ï¼ã **ç¨æ·èªå·±çæ°æ®ä¸å¾æªå¨**ï¼blob 67ï¼`ç¾äºæ¨¡ç.docx`ï¼ä¸éä»¶ 73ï¼ç»å¨æ¨¡æ¿ 1 ä¸ï¼ãå®ä¾ 44/45 åä¿æåæ ·ã ### 18.7 æ¬è½®æªå - åéèç»å¸ç**åæº¯æ ¸æ¥**ï¼`CanvasSafety` åªå¨ä¿åæ¶æ§è¡ï¼ä¸åæ«åå²çæ¬ï¼ã - 设计å¨ãé¢è§ãæé®æ¥ `instance/preview`ï¼æå¡ç«¯æ¸²æï¼ââç¨æ·å·²æç¡®éæ©**ä¿æç°ç¶**ã - äºç»´ç çæè½åï¼`GENERATING` / `FAILED` å®ä¾ç¶æï¼`qc_report_data_source` / `qc_report_rule` ä¸¤å¼ æ Java 对åºç表ã - `@Disabled` ççå® AI èè°æµè¯ï¼IPQC / OQC / RQC ä¸é¡µ UI éä¸ç¹éªã ## åä¹ãAI å¯¼å ¥ä¿®æ£ï¼é¡µç/页èè¢«è®¤æææ¬ç»ä»¶ï¼2026-09-19 ç»ï¼ **ç¨æ·æ¥çç°è±¡**ï¼ãai è¯å«åºç±»ä¼¼é¡µçæè 页èçæ¶å使ç¨å¯¹åºç»ä»¶ï¼ç°å¨å¥½å使ç¨çæ¯ææ¬ç»ä»¶ãã ææ¥å确认è忝**两å±ç¬ç«åå **ï¼åªä¿®ä¸å±ä¸å¤ãæ¬è½®**åå端ä¸å¹¶æ¹**ï¼ç¨æ·å·²ææå端ï¼ã æ æ°å¢ä¸å¡å¯¹è±¡ãæ å»ºè¡¨ãæ æ¸²æå¼ææ¹å¨ãæ 跨模å API ååã ### 19.1 第ä¸å±ï¼æç¤ºè¯é对ã12 个ç»ä»¶è¯¥éåªä¸ªã鶿坼 ç§¯æ¨æ¸ ååªå¸¦ `type` / `label` / `category` / `fields`ï¼éå¾ 5 æ¡ + æ½åè§å 4 æ¡**没æä¸æ¡è®²éå**ã èç»ä»¶ä¹é´å¤§éå¯äºç¸æ¿ä»£ï¼ | æä»¶éçå 容 | å¯é项 | |---|---| | æ¥åæ¬å¤´ï¼å ¬å¸å + æ¥åæ é¢ï¼ | `Heading`ãæ é¢ã/ `Text`ãææ¬ã/ `ReportHeader`ãæ¥å页çã | | è½æ¬¾ç¾ç½²è¡ï¼æ£éªå/å®¡æ ¸/æ¥æï¼ | `Text` / `ReportFooter`ãæ¥å页èã | | æ ·å/ç©æä¿¡æ¯å | `Table` / `Text` / `SampleInfo`ãæ ·åä¿¡æ¯ã | | æ£éªæ°æ®è¡¨ | `Table`ãåºç¡è¡¨æ ¼ã/ `QualityTable`ãæ£éªé¡¹ç®è¡¨æ ¼ã | | æ´ä½ç»è®º | `Heading` / `Text` / `Result`ãå¤å®ç»æã | 模ååªç䏿 `label`ï¼æ²¡æçç±ä¸æå段æç®åãæéç¨çé£ä¸ªââè¿å°±æ¯ãç¨äºææ¬ç»ä»¶ãçç´æ¥åå ã **ç¨æ·ææ¿çå£å¾**ï¼éåæå¼**æ¾å¨å端注å表çåæ®µé**ï¼ä¸ç»ä»¶å®ä¹åæºï¼å端æ¹ç»ä»¶æ¶èªå¨è·éï¼ï¼ **ä¸å端硬ç¼ç **ã䏿¦å端ç¹åç»ä»¶ï¼ç»ä»¶è¯ä¹å°±ååæç¬¬äºä¸ªçç¸æ¥æºï¼æ£æ¯æ¬é¡¹ç®ä¸ç´å¨é¿å çã **æ¹æ³**ï¼å端 `mom-pro2-before/`ï¼æ¬è½®å¯ä¸æ¹å端ç轮次ï¼ï¼ | è½ç¹ | æ¹å¨ | |---|---| | `components/quality/core/types.ts` | `QualityComponentDefinition` å å¯éæå `aiHint?: string` | | 12 个ç»ä»¶æä»¶ | åè¡¥ä¸æ¡ `aiHint`ï¼**ãç¨æ¥æ¾ä»ä¹ã+ãä¸è¦ç¨æ¥æ¾ä»ä¹ã**ãåè æ¯å ³é®ï¼æ¨¡å误ç¨çæ ¹å æ£æ¯ä¸ç¥éæä¸ªéç¨ç»ä»¶ä¸è¯¥ç¨å¨è¿é | | `components/quality/core/catalog.ts` | æ¸ åé¡¹å¸¦åº `hint?`ï¼æ²¿ç¨æ¢æã缺å¸å³çç¥ã飿 ¼ï¼ | | `api/mes/qc/report/ai/index.ts` | catalog ç TS ç±»åå `hint?` | **为ä»ä¹å®å ¨**ï¼å ¨ `src/` 没æä»»ä½ `Object.keys(definition)` / `for...in` / `{...definition}` / `JSON.stringify(definition)` / æ·±æ¯è¾ãç»å¸æé æ¯éåæ®µæ¾å¼æï¼å±æ§é¢æ¿åªä» `propertySchema` æ´¾çï¼ åºåå / è£ é / æ ¡éªä¸å¤é½åªéå `propertySchema`ï¼æ³¨åè¡¨æ¯ Map æ `type` ç´¢å¼ãå¯ä¸èå¨ç¹æ¯æ¸ åæ´¾çå±ã **å端åªè´è´£åºåå**ï¼`QcReportComponentSpecVO` å `hint`ï¼**ç»ä»¶çº§**ï¼ä¸å å¨åµå¥ç `Field` ä¸ï¼ï¼ æç¤ºè¯é追å 䏿¡æ½åè§åè¦æ±ãé忶以 hint 为åï¼æ¸ åéåå¨ä¸é¨ç»ä»¶å°±ä¸è¦éèç¨ Text / Heading è¿ç±»éç¨ç»ä»¶åæ°ãã **廿ä¸ç¹åä»»ä½ç»ä»¶**ââè§å 5 å ¨æä¸å«ç»ä»¶åï¼è¯ä¹ä»åªæ´»å¨å端ã ### 19.2 第äºå±ï¼`.docx` ç Word 页ç/页èåæ ¹æ²¡è¿æç¤ºè¯ `OfficeImportAdapter` èµ° `document.getBodyElements()`ï¼åªéå**æ£æ**ï¼å ¨æ¨¡å grep 确认 `getHeaderList()` / `getFooterList()` **䏿¬¡é½æ²¡è°ç¨è¿**ãäºæ¯é£ä»½æä»¶ç页ç ï¼å ¬å¸å / è±æåï¼ä¸é¡µèï¼å¶é å / åå / çµè¯ï¼**模å仿¥æ²¡çè§è¿**ï¼å®æ³ç¨ `ReportHeader` 乿 ä»ä¸æã **ç¨æ·ææ¿çå£å¾**ï¼docx ç Word 页ç页è**è¯»è¿æ¥å¹¶å `[页ç]` / `[页è]` æ è®°**ã **æ¹æ³**ï¼`OfficeImportAdapter`ï¼ï¼ 1. ææ£æéåæ½æ `appendBodyElements(StringBuilder, List<IBodyElement>)`ï¼æ£æ/页ç/页è**å ±ç¨ä¸ä»½è¡¨æ ¼é»è¾**ï¼é¿å 䏤奿¼ç§»ï¼ 2. æ°å¢ `collectHeaderFooter(...)`ï¼é个æ¶éå**æå½ä¸åææ¬å»é**ï¼Word æé¦é¡µ/奿°é¡µ/å¶æ°é¡µå¤å¥é¡µçï¼å 容常éåç¸åï¼ï¼ **空ç½å¿ é¡»è¿æ»¤**ââææ¡£æ²¡æé¡µçæ¶ POI ä»å¯è½è¿åä¸ä¸ªç©º headerï¼ä¸è¿æ»¤ä¼è®©æ¢æå¤¹å ·å空å¤åº `[页ç]` æ è®°ï¼ 3. `readDocx` æ¹ä¸ºè¿å `record DocxRead(String text, List<String> notes)`ï¼æ¼è£ 顺åºä¸º `[页ç] â æ£æ â [页è]`ï¼ä¸ææ¡£è§è§é¡ºåºä¸è´ï¼ä¸**ä» å½é空**ï¼ï¼ 4. å®é 读å°å 容æ¶å¾ `notes` å 䏿¡è½¯æç¤ºï¼ç» `QcReportImportServiceImpl` æ¢æç `warnings.addAll(extract.notes())` ç´æ¥è¿ååº `warnings`ï¼**å端æ é任使°ä»£ç å°±è½å±ç¤º**ã **åªæ docx éè¦**ï¼PDF / å¾çééé页çé¡µèæ¬æ¥å°±æ¯é¡µé¢åç´ æææ¬çä¸é¨åï¼æ éæ è®°ã ### 19.3 顺带修æç两个çå®ç¼ºé·ï¼ç¨æ·éæ¡ææ¿åè½å°ï¼ **(a) è¡¨æ ¼æ«è¡çè½æ¬¾ç¾ç½²è¡è¢«å½ææ°æ®è¡ä¸¢æã** æ½åè§å第 1 æ¡åçãå ·ä½æ°æ®è¡ä¸è¦åè¿æ¨¡æ¿ãï¼ èç¾ç½²è¡ï¼æ£éªå/å®¡æ ¸äºº/æ¥æï¼**å°±æå¨æ£éªè¡¨æ ¼çæåä¸è¡**ï¼äºæ¯è¢«æ´è¡ä¸¢æï¼è½æ¬¾å乿¾ä¸åæ¥ã æ¹æ³ï¼è§å 6 æ¾å¼æå®ä»ç¬¬ 1 æ¡éè±å åºæ¥ï¼å¹¶è¦æ±**ä¸å件页èåæä¸å¤**æ¾è¿ `ReportFooter`ï¼ åæ¶æå®åè¿ `ReportFooter` ä¸ `QualityTable` ä¸¤æ¡ `aiHint`ï¼ä¸¤ä¾§åèªå£°æè¾¹çï¼æ¨¡åä»ä»»ä¸ä¾§çé½ä¸è´ï¼ã **è¦ç¹åãå³ä½¿å®æå¨æ£éªè¡¨æ ¼çæåä¸è¡ä¹ä¸æ ·ãè¿ä¸ªä½ç½®**ââä¸ç¹åï¼æ¨¡åä»ä¼æç¬¬ 1 æ¡æå®å½æ°æ®è¡ã **(b) 模åç»ç¾ç½²è¡ç¼é äºä¸åå¨çç»å®é®ã** 修好 (a) ä¹å宿µè¾åºï¼ ``` ReportFooter.props.text = "æ£éªåï¼{{report.inspector}} å®¡æ ¸äºº Auditorï¼{{report.auditor}} æ¥æ Dateï¼{{report.date}}" ``` `{{report.auditor}}` / `{{report.date}}` **å¨ä¸ä¸æéæ ¹æ¬ä¸åå¨**ââæ¥ `ReportFields` ï¼æ¥åä¸ä¸æå段çæå¨æ¸ åï¼ç¡®è®¤åªæ `inspector` ä¸ `inspectDate`ï¼**没æ `auditor`ãæ²¡æ `date`**ã æ¥ `binding.ts` 确认åä¸å°çè·¯å¾æ¸²ææ**空å符串**ï¼æ¥åä¸ä¸ä¼åºç°ç¨æ·ç䏿ç `{{}}`ï¼ï¼ ä½ä¼**çä¸ç©ºç½å¹¶è§¦å䏿¡ãç»å®åä¸å°å¼ãåè¦**ã **ç¨æ·æä»¤**ï¼**ãå®¡æ ¸äººç空æå°åºæ¥å¡«ã**ãç±æ¤è½å°ä¸ºè§å 6 çåå + æ°å¢è§å 7ï¼ - ç¾ç½²è¡**ä¿çæ ç®åãå¼ç空**ï¼åæãæ£éªåï¼\_\_\_\_ãå®¡æ ¸äººï¼\_\_\_\_ãæ¥æï¼\_\_\_\_ãè¿æ ·çå¡«åä½ï¼ æå°åºæ¥ç±äººæå¡«ã**人å䏿¥ææ¯æ¯ä¸ä»½æ¥ååèªçæ°æ®**ï¼åæ»åä»¶ä¸çé£å 个人ä¼è®©æææ¥åå°æåä¸ä¸ªæ£éªåï¼ - è§å 7 æä»¤**ä¸å¾èªé ç»å®é®**ï¼æ¨¡åå¹¶ä¸ç¥éä¸ä¸æéæåªäºå段ï¼ç¼åºæ¥ç鮿¸²ææ¶åªä¼çç½å åè¦ã > è¿ä¸æ¡ä¸æå åç»ç¨æ·çè¿çé¢è§ä¸ä¸è´ï¼é¢è§éæ¯ãæ£éªåï¼æ¼å µé æ¨æ §ãè¿ç±»**åé¢äººå**ï¼ã > ç¨æ·çæä»¤è¦çäºé£ä¸ªç»èï¼æ¹ä¸ºç空ââä¸å®ä¸è§å 1 çåæï¼æ¨¡æ¿åªçç»æï¼æ¬å°±èªæ´½ï¼ > `ReportFooter` çé»è®¤å¼ä¹æ£æ¯ãæ£éªåï¼\_\_\_\_ãå®¡æ ¸äººï¼\_\_\_\_ãæ¥æï¼\_\_\_\_ãã > **çç卿¤ï¼é¿å 以åæäººä»¥ä¸ºåé¢äººåææ¯é¢æã** ### 19.4 ä¸å¤ä¸å¹¶è¯´æ¸ çå£å¾ï¼æ£ææ¬å¤´/è½æ¬¾ â æ¯é¡µéå¤ç页ç页è ç»å¸ä¸ç `ReportHeader` / `ReportFooter` æ¯**æ£ææµé**çæ¬å¤´åä¸è½æ¬¾åï¼ PDF æ¯é¡µéå¤ç页çé¡µèæ¯æå¡ç«¯ `PdfPrintOptions` å çï¼ç®ååªæé¡µç ï¼ã æä»¥ãWord 页ç â `ReportHeader`ãæ¯**æå®æªè¿æ£æé¡¶é¨**ï¼**䏿¯**è¿åæ¯é¡µéå¤çæå°çå¼ã å·²åè¿å端èè°ææ¡£ï¼é¿å åç«¯è¯¯ä»¥ä¸ºå¯¼å ¥åè½å¾å°æ¯é¡µéå¤ç页çã ### 19.5 æµè¯ä¸éªè¯ç¶æï¼å·²å / æªååå¼å£°æï¼ **å·²éªè¯ï¼ç¡®å®æ§ï¼**ï¼ - `mvn -pl yudao-module-qcreport test` â **`Tests run: 137, Failures: 0, Errors: 0`**ï¼ å ¨ç»¿ï¼132 项åé + æ¬è½®æ°å¢ï¼`QcReportTemplatePromptBuilderTest` 8 â 9ï¼ã - å端对æ `npx tsx .qc-conformance/assemble-ts.ts` â å ¨é¨éè¿ãæ¸ åæ°å¢ä¸æ¡æè¨ **ãæ¯ä¸ªç»ä»¶é½åäºé空 aiHintã**ï¼12/12ï¼ï¼å段éåæè¨åæ¥ä¸º `category/fields/hint/label/type`ã - **æç¤ºè¯æé 宿µ**ï¼åä»åºå¤æ¢é `D:/qcl-tmp/PromptProbe.java`ï¼ï¼æ¿**çå®å端导åºçæ¸ å**ååºååå è°çå® `QcReportTemplatePromptBuilder.buildSystemPrompt`ï¼æç¤ºè¯ 5816 åç¬¦ï¼ **12 æ¡ `hint` éæ¡å¨åºï¼ç¼ºå¤± 0ï¼**ï¼è§å 6/7 ææ¡éå¥å¨åºã - **docx æ½å宿µ**ï¼åä»åºå¤æ¢é `D:/qcl-tmp/HeaderFooterProbe.java`ï¼ï¼å¯¹ç宿件è°**ç产类** `OfficeImportAdapter`ï¼ç¡®è®¤é¡µç/页è被读åºå¹¶ä»¥ `[页ç]` / `[页è]` æ è®°å¹¶å ¥ï¼`notes` é空ã > è¿ä¸¤ä¸ªæ¢éçæä¹æ¯ï¼æãå端é»è¾å¯¹ä¸å¯¹ãä¸ã模åéå¾å¯¹ä¸å¯¹ã**å½»åºåå¼**ã > åè å¿ é¡»ç¡®å®æ§æè¨ï¼åè åªè½é çå®è°ç¨è§å¯ã **HTTP 宿µï¼çå®è°ç¨ï¼å¦å®æ±æ¥æ³¢å¨ï¼**ï¼ - è§å 6/7 è½å°**å**çç¶æå·²å®æµ 3 次ï¼2 æ¬¡èæ¬ + 1 次 UIï¼ï¼3 æ¬¡ç»æä¸è´ï¼ `ReportHeader â SampleInfo â QualityTable â Result â ReportFooter`ï¼**`Text` æªåºç°**ï¼ 5 é¡¹ç¡®å®æ§æè¨æ¯æ¬¡å ¨è¿ï¼å« `warnings` åºç°ãå件带æé¡µç/页èãï¼ã**é¡¶é¨ä¸åºé¨é½è½å°äºä¸é¨ç»ä»¶ä¸**ã - **è§å 6/7 è½å°åï¼ç¨æ·éå¯å端åï¼å¤éª 2 次ï¼ä¸¤æ¬¡ç»æä¸è´**ï¼å ³é®è¯æ® ââ `ReportFooter.props.text`ï¼ ç¬¬ 1 æ¬¡ï¼ ``` æ£éªå ï¼__________ å®¡æ ¸äºº Auditorï¼__________ æ¥æ Dateï¼__________ å¶é åï¼æ°ç大ç½ç´ 马éè¯å¶åæéå ¬å¸ ååï¼æ°çå屯å¸å·¥ä¸ååºéè¾è·¯555å· çµè¯å·ç ï¼0906-7516688 ``` 第 2 æ¬¡ï¼æªè¾ä¸æ¢è¡æå·®å¼ï¼å½¢æç¸åï¼ï¼ ``` æ£éªå ï¼__________ __________ å®¡æ ¸äºº Auditorï¼__________ æ¥æ Dateï¼__________ å¶é åï¼æ°ç大ç½ç´ 马éè¯å¶åæéå ¬å¸ ååï¼æ°çå屯å¸å·¥ä¸ååºéè¾è·¯555å· çµè¯å·ç ï¼0906-7516688 ``` ä¸¤æ¬¡é½æ»¡è¶³ä¸ä»¶äºï¼**â ç¾ç½²è¡æ ç®åå¨ãå¼ç空**ï¼è§å 6 ååçæï¼ï¼ **â¡ éç¯æ `{{report.auditor}}` / `{{report.date}}`**ï¼è§å 7 çæï¼å¯¹æ¯å¤éªå飿¬¡ç `å®¡æ ¸äºº Auditorï¼{{report.auditor}} æ¥æ Dateï¼{{report.date}}`ï¼ï¼ **⢠Word 页èä¸è¡ä¸ç¾ç½²è¡åæå¨åä¸ä¸ª `ReportFooter` é**ï¼ç¨æ·ææ¿çãA + B é½è¦è¿ãï¼ã 两次ç `type` åºåå为 `ReportHeader â SampleInfo â QualityTable â Result â ReportFooter`ï¼ **`Text` æªåºç°**ï¼5 é¡¹ç¡®å®æ§æè¨ **5/5 PASS**ï¼èæ¶ 11.7 s / 10.4 sã **æªéªè¯ / ä¸ä¸»å¼ **ï¼ - **æ¨¡åæ¯å¦æ¯æ¬¡é½éµå®è§å 6/7ï¼æ²¡æç¡®å®æ§è¯æ®**ãæ¨¡åè¾åºä¼æ³¢å¨ï¼å¤éªåªä½ãè§å¯è®°å½ãï¼ **ä¸åæãå·²éªè¯æ£ç¡®ã**ãçæ£çé¸é¨å¨åå ¥ä¾§ä¸åç«¯è£ é å¨ï¼æªæ³¨å type ä¼è¢«è·³è¿ï¼ã 两次å¤éªçæªè¾/æ¢è¡é½æåºå ¥ï¼ç¬¬ 2 次æãæ£éªåãåé¢ç第äºä¸ªå¡«åä½åç¬æ¢è¡äºï¼ï¼ æ£è¯´æè¿ç±»æ¹å¨åªè½é çå®è°ç¨è§å¯ï¼ä¸è½ç¨æè¨éæ»ã - **æªå UI éªæ¶ï¼ç¬¬äºè½®ï¼**ï¼æ¬è½® UI éªæ¶æ¯å¨è§å 6/7 è½å°**å**åçï¼ä¸´æ¶æ¨¡æ¿ 34ï¼ã è½å°ååªåäº HTTP å¤éªï¼**没æéè·è®¾è®¡å¨ UI é£ä¸è·¯**ãæ¹å¨åªå½±åæç¤ºè¯ææ¬ï¼ è£ é å¨ä¸è®¾è®¡å¨ä»£ç ä¸è¡æªå¨ï¼å æ¤å¤å®ä¸ºä½é£é©ï¼ä½**è¿ä¸æ¡æ¯ã没åãè䏿¯ãéè¿ã**ã - æªåãmock 模ååç½å¤´ JSON æè¨å®é对äºç»ä»¶ãè¿ç±»æµè¯ââ飿¯å¨æµ mockï¼ä¸æ¯æµäº§åã **æµè¯æ°æ®æ¸ çï¼å·²å读æ¯å¯¹ï¼**ï¼ä¸´æ¶æ¨¡æ¿ 34 / 35 / 36 å ¨é¨è½¯å ï¼å¤éªä¸ä¼ çå¤å¿ blob 76 / 77 èµ°ãå bind å°ä¸´æ¶æ¨¡æ¿ 36 åå éä»¶ãççº§èæ¸ æï¼**blob 没æç¬ç«å 餿¥å£**ï¼ï¼ `D:/uploads/2026/0919/` åå°ç¨æ·èªå·±ç 4 个æä»¶ãå读ä¸å¼å·¥åä¸è´ï¼ blobs `20/62/67/70`ãattachments `18/68/73/74`ã**ç¨æ·èªå·±ä¸ä¼ ç `ç¾äºæ¨¡ç.docx`ï¼blob 67/70ãéä»¶ 73/74ï¼å ¨ç¨æªå¨ã** ### 19.6 æ¬è½®æªå / éç - `docs/qc_report_ai_import_frontend_integration.md` 已忥ï¼`hint` åæ®µã`[页ç]`/`[页è]` å£å¾ã æ£ææ¬å¤´ä¸çå¼é¡µççåºå«ãç¾ç½²è¡ä¸¤æ¡è§åï¼ã**æªåä»»ä½å端代ç çæ®µ**ï¼æé¡¹ç®è§åï¼ã - é¢ç使ªå®æ½ï¼`.qc-conformance/dump-catalog.ts`ï¼æ¬è½®ä¸ºå¯¼åºç宿¸ å临æ¶å çèæ¬ï¼**æªè¢« git è·è¸ª**ï¼ ç¨éæ¯ææ¸ å dump æ JSON ä¾èè°èæ¬è¯»åï¼ä¿çä¸å¦å¾ å®ï¼ã - ç¨æ·å åæåºçå¦ä¸ä¸ªé®é¢ãå¦ä½é«åº¦è¿åè¿ç§å¤æçæ£éªé¡¹ã**仿ªé宿¹æ¡**ï¼æ¬è½®æªå¨ã ## äºåãè½æ¬¾æè¡ä¿®æ£ï¼ç¾ç½²è¡ä¸å¶é å被ææä¸çï¼2026-09-19 ç»ï¼ ### 20.1 ç°è±¡ä¸æ ¹å ç¨æ·å¨æ¸²æåºæ¥çæ¥å䏿åºã**æ¥æåå¶é åå¸å±æç¹ä¸è¡**ãï¼è½æ¬¾åºè¯¥æ¯å è¡ï¼å®é æ¯ `æ£éªåï¼__________ å®¡æ ¸äºº Auditorï¼__________ æ¥æ Dateï¼å¶é åï¼æ°ç大ç½ç´ 马éè¯å¶åæéå ¬å¸ååï¼â¦çµè¯å·ç ï¼â¦` **䏿´çè¿å¨ä¸èµ·**ï¼åªé 容å¨å®½åº¦ææä¸¤è¡ï¼æ¢è¡ä½ç½®ç±å®½åº¦å³å®ï¼ä¸æ¯å 容æ¬èº«çæç¹ï¼ã æ ¹å æ¯**渲æä¾§å¯¹æ¢è¡çå¤çï¼ä¸æ¯ AI è¾åº**ï¼AI ä¾§çè¾åºæ¯å¯¹çï¼ï¼è§å 6 è¦æ±æç¾ç½²è¡ä¸å件页è **åæåä¸ä¸ª `ReportFooter`**ï¼AI ç §åï¼å 段ä¹é´ç¨ `\n` åéãè `ReportFooter.buildContent` åªåäº `escapeHtml(properties.text)`ï¼ **`escapeHtml` åªå¤ç `& < > " '`ï¼å®å ¨ä¸ç¢°æ¢è¡**ï¼`core/html.ts`ï¼ï¼ HTML çç©ºç½æå è§åæ `\n` æææ®éç©ºæ ¼ï¼äºæ¯åè¡ç²è¿æä¸æ®µã **åä¸å¼ æªå¾éçèè² `Table cell` 䏿¯ç¼ºé·**ï¼é£æ¯ GrapesJS é䏿çç»ä»¶åå¾½æ ï¼GrapesJS æ `<td>` å½å为 "Table cell"ï¼ï¼å·²ç¡®è®¤å端æºç ä¸å ¨é¨å·²åç»å¸éé½ä¸åå¨è¿ä¸ªå符串ã ### 20.2 æ¹å¨ï¼ä¸å¤ï¼å ¨å¨åç«¯ï¼ | æä»¶ | æ¹å¨ | |---|---| | `src/components/quality/core/html.ts` | æ°å¢ `escapeHtmlMultiline`ï¼**å 转ä¹ãåæ `\r\n?|\n` ææ `<br>`** | | `src/components/quality/header/report-footer.ts` | `escapeHtml` â `escapeHtmlMultiline` | | `src/components/quality/base/text.ts` | åä¸ï¼AI ææ¶æç¾ç½²è¡æ¾è¿ `Text`ï¼åªä¿®é¡µèä¼çæ¼æ´ï¼ | **æè¡æ¾å¨è®¾è®¡æï¼`buildContent`ï¼è䏿¯æ¸²ææ**ï¼`HtmlRenderer` ä¸ `BASE_CSS` æ¯å»ç»ç ï¼`FrontendConformanceTest` ä¸åç«¯å¼æéå对æï¼ï¼**红线ä¸å¨**ã`buildContent` 产åºç `content` å°±æ¯ç»å¸å å®¹ï¼æ¸²æå¼æåªå `{{}}` åå¼ãåæ ·è¾åºï¼åå端两侧é½å·²è¯»ç ç¡®è®¤ï¼ `render.ts:renderText` / `HtmlRenderer.renderText` â `Bindings.resolveText`ï¼**åä¸å转ä¹**ï¼ï¼ æä»¥åè¿ `content` ç `<br>` è½ä¸è·¯æ´»å° HTML ä¸ PDFã `Heading` æªæ¹ï¼æ 颿¯çåè¡ï¼æ²¡æå¤è¡çè¯æ®ã`Result` / `SampleInfo` / `ReportHeader` æ¯æ¼æ¥å¼æé®å¼å¼ï¼ä¸åå½±åã ### 20.3 éªè¯ï¼å·²åï¼ - **å端 `mvn -pl yudao-module-qcreport test`ï¼137/137 å ¨ç»¿**ãç»å¸ fixture æ¯å»ç»çé¢æå»º JSONï¼ `buildContent` åªå¨è®¾è®¡æè·ï¼æ ä¸è´æ§å¯¹æä¸åå½±åã - **å端对æèæ¬æ°å¢ç¬¬ 10 è**ï¼`.qc-conformance/assemble-ts.ts`ï¼èæ¬æ¬ä½æªè¢« git è·è¸ªï¼ï¼ `ReportFooter` / `Text` åä¸¤æ¡æè¨ ââ `content` éåºç° `<br>` ä¸**䏿¯è¢«è½¬ä¹ç `<br>`**ã `data-qc-text` 屿§**ä¿çåå§ `\n`**ï¼å±æ§é¢æ¿ä¸ç»å®æçº¯ææ¬å¤çï¼æè¡åªåç卿¸²æäº§ç©è¿ä¸ä¾§ï¼ã è¿åæåèä¸èµ· **å ¨é¨éè¿**ã - **è£ é â æ¸²ææ¢é**ï¼ä¸´æ¶ tsx èæ¬ï¼ç¨å®å·²å ï¼ï¼å¤è¡è½æ¬¾ç» `assembleQualityCanvas` + `renderReport` åäº§åº 3 个 `<br>`ãåè¡æåå ¨å¨åºï¼`<script>` ä»è¢«è½¬ä¹æ `<script>`ï¼ **æ¢è¡å¤ç没æåå¼±æ³¨å ¥é²æ¤**ï¼åè¡è½æ¬¾éåä¸åï¼æ åå½ï¼ã - **æµè§å¨éªæ¶ï¼è®¾è®¡å¨ç»å¸ï¼**ï¼ä¸´æ¶å»ºæ¨¡æ¿ 37 / èç¨¿çæ¬ 22ï¼ç»å¸éç `ReportFooter` 带é£åè¡è½æ¬¾ï¼æå¼ `/qc/report-template/designer?id=37` 读ç»å¸ iframe ççå® DOMï¼ `innerText` ææ **4 è¡**ã`<br>` **3 个**ã`data-qc-text` é `\n` åæ ·ä¿çï¼ é¡¶é¨ `Heading`ãæ£ éª æ¥ åãä¹å¨ã**è¿ä¸å±å°±æ¯ç¨æ·çå°åºé®é¢çé£ä¸å±ï¼ç°å¨æ¯å¯¹çã** ### 20.4 æªå / å·²ç¥è¾¹çï¼å¦å®å£°æï¼ - **åéå·²ä¿åçç»å¸ä¸å溯**ã`content` åªå¨ `buildContent`ï¼æ°å»ºç»ä»¶ / AI è£ é ï¼æ¶çæï¼ 设计å¨**è½½å ¥**模æ¿ä¸ä¼éç® `content`ï¼`canvas-sync.ts` åªé建 traitsï¼ä¸éç®å 容ï¼ï¼ æä»¥**æ¹å¨ä¹å**å°±ä¿åè¿çå¤è¡è½æ¬¾ä»ä¼æ¯æå¨ä¸èµ·çã宿µ 7 个已åç»å¸çæ¬é **没æä»»ä½ä¸ä¸ªå«ãå¶é åã/ãååã**ï¼å³æ²¡æåéåå½±åï¼ä»å¼å¾æ¥åä¸èµ·æ¸ çèç»å¸æ¶ç»ä¸å¤çã - **屿§é¢æ¿ä»æ¯åè¡è¾å ¥**ï¼å¯ç»å®å段ç»ä¸ç¨èªå®ä¹æ§ä»¶ `qc-binding`ï¼é颿¯ `<input type="text">`ï¼`trait-binding.ts`ï¼ã**人æå¨è®¾è®¡å¨éæ¹è½æ¬¾ï¼ä»ç¶æä¸åºæ¢è¡ã** æ¬è½®**æªå¨**ï¼æå®æ¢æ `<textarea>` ä¼åæ¶æ¹å 12 个ç»ä»¶éå ¨é¨å¯ç»å®å段çè¾å ¥å½¢æï¼ å±äºè¦åç¬ææ¿çåèï¼ä¸å¤¹å¸¦å¨å¸å±ä¿®å¤éã**AI å¯¼å ¥åºæ¥çå¤è¡è½æ¬¾ä¸åæ¤é**ï¼å¼åæ ·ä¿çï¼ åªè¦ä¸å»ç¼è¾é£ä¸ªè¾å ¥æ¡ï¼ã - **æµè§å¨éªæ¶åªè¦ç设计å¨ç»å¸**ï¼**没æéè·ãçå® AI å¯¼å ¥ + æªå¾ãé£ä¸è·¯**ï¼ æ¬è½®å端æ¹å¨ä¸æ¹å AI çè¾åºææ¬ï¼ç¨è£ é 好çå䏿®µå¤è¡è½æ¬¾å³å¯ç¡®å®æ§å¤ç°ç¨æ·é£ä¸å±ï¼ å æ¤æ²¡æä¸ºä¸æ¬¡éç¡®å®æ§ç AI è°ç¨åè±ä¸æ¬¡é¢ç®ã**è¿ä¸æ¡æ¯ã没åãï¼ä¸æ¯ãéè¿ãã** - æ¬è½®**没æ**æ¹æç¤ºè¯ã**没æ**æ¹ `HtmlRenderer` / `BASE_CSS`ã**没æ**æ¹ SQL ä¸å»ºè¡¨ã ä¸å¡å¯è§åææ¡£ `docs/project-business/` æ¬è½®**æªæ¹**ï¼è¿æ¯æ¸²æå±å¸å±ä¿®å¤ï¼ ä¸å¡æ¨¡å / 对象 / æµç¨æ¥éª¤ / ç¶ææµè½¬ / æéåæ ååï¼å¤æ®ï¼AI çè¾åºè¯ä¹ä¸ä¸ä¸è½®å®å ¨ä¸è´ï¼ã ### 20.5 æµè¯æ°æ®æ¸ ç ä¸´æ¶æ¨¡æ¿ 37 ä¸èç¨¿çæ¬ 22 **å·²å é¤å¹¶å读确认**ï¼çæ¬åè¡¨ä¸ºç©ºãæ¨¡æ¿ `get` è¿å `data=null`ï¼ï¼ æ¬è½®**没æä¸ä¼ æä»¶**ï¼ä¸æ¶åéä»¶ä¸ `blob`ï¼ä¸´æ¶çè£ é èæ¬ä¸æªå¾å·²ä»å·¥ä½åºç§»é¤ã ## äºåä¸ã夿æ£éªé¡¹è¿åï¼åç»æ£éªé¡¹ï¼2026-09-19 ç»ï¼ > æ¹æ¡ä¸å®ç¨¿å£å¾ï¼`docs/qc_report_grouped_inspection_design.md`ï¼**å·²å®ç¨¿å¹¶å¼å·¥**ï¼ã > æ¿æ¥ §19.6 çç飿¡ãç¨æ·å åæåºçå¦ä¸ä¸ªé®é¢ãå¦ä½é«åº¦è¿åè¿ç§å¤æçæ£éªé¡¹ã仿ªé宿¹æ¡ãã ### 21.1 è¦ä¿®çæ¯ä»ä¹ åä»¶ï¼æ¥ææ£éªæ¥åï¼éçæ£éªé¡¹æ¯**两å±**çï¼`æ°´å` æ¯ä¸ä¸ªåç»é¡¹ï¼å®èªå·±åªæä¸æ¡å ¬å¼ `w = (m1 - m0) / m à 100%`ï¼å®æµå¼å¨å®ä¸é¢çå项 `è¯æ ·è´¨é m` / `æ°´åç»æ w` ä¸ã å¤æè´¨æ£åæ©å·²æè¿ä¸ªç»æåäºæ°æ®ï¼`mes_qc_indicator.parent_id` / `item_type` / `formula_text`ï¼ï¼ **åºæ¥åé£ä¸ä¾§æå®åå¹³äº**ï¼ç»åä¸å ¬å¼æ´æ¡ä¸¢æï¼å项åå ä¸è¡ãçä¸åºè°å±äºè°ã | ä½ç½® | æ¹å | æ¹å | |---|---|---| | å½å±ä¿¡æ¯ | æ | ç»ååå¹¶ï¼`rowspan`ï¼ï¼å项è¡è®©ä½ï¼`hidden`ï¼ | | ç»å ¬å¼ `formula_text` | 丢 | è¿ç»å¤´è¡çãæ£æµè¦æ±ãæ ¼ | | åæ° | 6 å `QualityTable` | æ°å¢ **7 å** `GroupedQualityTable`ï¼å¤ä¸åãå项ãï¼ | ### 21.2 å½å±å¤æ®ä¸åå¹¶è§åï¼å®æµå¾åºï¼ä¸æ¯æ¨æï¼ **å½å±åªç `parent_id`ï¼ä¸ç `item_type`ï¼ä¹ä¸çè¡åº** ââ è¡è¡¨éç¶é¡¹é£ä¸è¡ä¸å®çå项 **å¹¶ä¸ç¸é»**ï¼åä¸ä»½åéç¶é¡¹å¯è½æå¨ä¸ç»å项ä¸é´ï¼ã`item_type = 2` åªç¨æ¥å¤æ ãè¿ä¸ªææ å¤ä¸å¤æ ¼å½ç»å¤´ãã **åå¹¶å£å¾ç»ä¸æä¸æ¡è§å**ï¼**ç»å 第ä¸è¡è `span = è¯¥ç»æ»è¡æ°`ï¼åç»å ¶ä½è¡ä¸å¾ `span = 1` + `hidden = hidden`ã** è¿æ ·å¤æ ·åçç»å¤´ï¼ä¸ä¸ªææ å¤è¡ï¼ä¹èªå¨è½å¨è§åéï¼ä¸éè¦ç¬¬äºå¥åæ¯ã è½å°ææ®µæ¯**ç±æ°æ®ä¸åãä¸å¨æ¸²ææè®¡ç®**ï¼å并屿§åæ `rowspan="{{item.group.span}}" hidden="{{item.group.hidden}}"`ã乿以ç®ä¸åºæ¥ ââ ç»å®å¼æå¨æ°ç»ä¸ä¼æå ç´ pluckï¼`{{item.group.children.length}}` åä¸å°å¼ã åçï¼**æ¯ä¸é¡¹é½å¿ 须带 `group`**ï¼ç¬ç«é¡¹ç»ç©ºä¸²ä¸ `1`ï¼ï¼åæ®µç¼ºå¸æ¶ç»å®ç屿§ä¼è¢«è·³è¿ï¼ 该éèçæ ¼ååèä¼é²åºæ¥ã**没æåç»çæ¥åæ´ä½ä¸å¸¦ `group`**ï¼å并屿§ä¸ä¸ªé½ä¸åºç°ã ### 21.3 å使¯æµè§å¨éåºæ¥çï¼æ¬é¢å¯ä¸ççä¸ç¡®å®ç¹ï¼ ãéè䏿 ¼ä¼ä¸ä¼ææ´è¡å©ä½æ ¼å左移ä¸åãå¨ HTML ç»æå±é¢éªä¸åºæ¥ï¼åªè½æ° `td`ï¼ï¼ çæ£çè¡¨æ ¼å¸å±ç±æµè§å¨å®ãæä»¥ç¨**ç宿¸²æäº§ç© + çå® Chromium**ï¼å°±æ¯æå¡ç«¯ PDF ç¨çé£ä¸ªï¼ éæ ¼é `getBoundingClientRect().left`ï¼æ å°å表头åï¼ - 表头å左边çï¼`0 / 133 / 343 / 601 / 1154 / 1326 / 1458`ï¼ - **ç»å¤´è¡ â å 1â7**ï¼å ¨å ï¼ï¼ - **åé¡¹è¡ â å 1ã3ã4ã5ã6ã7** ââ åºå·ä»å¨å 1ï¼**å 2 被 `rowspan` å ä½èè·³è¿**ï¼ `å项` è½å 3ã`宿µå¼` è½å 5ã`å¤å®` è½å 7ã**没æå·¦ç§»ã没æä¸²ä½ã** - æ¯ä¸è¡çåå·åºå齿»¡è¶³ `ascending: true, unique: true`ã - æªå¾åè¯ï¼`mom-pro2-before/.qc-conformance/grouped-table-render.png` ï¼å¯è§ `æ°´å` è·¨ä½ 1â3 è¡ï¼`è¯æ ·è´¨é m` / `æ°´åç»æ w` å¹¶æå¨ãå项ãåï¼ã > æåå çå¿ç®æ¯éçï¼æä»¥ä¸ºå项è¡ç 6 ä¸ªæ ¼åä¼è½å°å 2â7ã宿µæ¯**å 1 + å 3â7**ã > ç»è®ºï¼ä¸éä½ï¼ä¸è´ï¼ä½è¿ç¨æå®æµæ´æ£ï¼ä¸æå¿ç®åè¿ææ¡£ã ### 21.4 ç»å¤´å¨æ¬å没æè¡æ¶ï¼**è¡¥ä¸è¡**ï¼ä¸æ¯ãæç¬ç«é¡¹å¤çã è¿æ¯æ¬è½®å¯ä¸ä¸å¤ãæ¹äºæ©æè®¾è®¡ãçå°æ¹ãæ©ææ¹æ¡åçæ¯ï¼åç»é¡¹å¨æ¬åçæ£éªæç»é没æèªå·±çè¡ â å¤ä¸ºãç»ä¸æç«ãæç¬ç«é¡¹å¤çãã**飿 ·ä¼åæ ·å¤ç°æ¬æ¬¡è¦ä¿®ç bug**ï¼å ¬å¼å丢äºï¼ã å®ç¨¿æ¹ä¸ºï¼**ä»ç¶ä¸ºå®è¡¥ä¸è¡**æ¥æ¾ç¤ºç»åä¸å ¬å¼ï¼å¹¶å¨ååºç `synthesizedGroupNames` é说æã 宿µç¡®è®¤è¿æ¡è·¯å¾å¯è¾¾ï¼`mes_qc_template_indicator` å³å®æ¬åæåªäºè¡ï¼ **æ£éªæ¨¡æ¿å®å ¨å¯è½åªå¾äºåé¡¹ãæ²¡å¾ç¶é¡¹**ã åæ¶æè¿æ¡ warning ä¸å¦ä¸æ¡**åå¼**ï¼æ©ææ¯åä¸ä¸ªåæ®µï¼æ··å¨ä¸èµ·è¯´ä¸æ¸ ï¼ï¼ | æ åµ | åæ®µ | æç¤ºè¯ä¹ | |---|---|---| | åç»é¡¹æ¬å**没æèªå·±çè¡** â å端补äºä¸è¡ | `synthesizedGroupNames` | ãæ¥åé为å®ä»¬åè¡¥äºä¸è¡æ¥æ¾ç¤ºç»åä¸å ¬å¼ã | | åç»é¡¹æ¬å**没æå±äºå®çå项** â æç¬ç«é¡¹ååºãç»åä¸åå¹¶ | `childlessGroupNames` | ãæ¥åéæç¬ç«æ£éªé¡¹ååºãç»åä¸åå¹¶ã | | ææ å·²è¢«å é¤çæç»æ¡æ° | `missingIndicatorCount` | ãæ N æ¡æç»çææ å·²è¢«å é¤ï¼å·²è·³è¿ã | **ç»å¤´çéç¹è¡æå®ä¹æ²¡æå®æµå¼**ï¼å¼å¨å项ä¸ï¼ï¼æä»¥å®**䏿¥**ãå°æªå½å ¥å®æµå¼ãââ ä¸ç¶æ¯ä¸ä»½å¸¦åç»çæ¥åé½ä¼è¯¯æ¥ã ### 21.5 å¤å®å£å¾ï¼ãæ å¤å®è§åãæ¯åæ³ç¶æ æç¨æ·ææ¿ï¼**没æä¸ä¸é乿²¡æå¤å®è§åç项æ¾ç¤ºãæ å¤å®è§åãï¼ä¸ä¸è®¡å ¥åæ ¼ç忝䏿¥åç»è®º** ï¼è¿ç¨åæ°å°±æ¯è¿ä¸ç±»çå ¸åï¼ã宿µè½å°ææï¼ - `report.total = 4`ã`passCount = 1`ã`passRate = 100.00%`ã`conclusion = åæ ¼` ââ ç»å¤´éç¹è¡ä¸è¿ç¨åæ°**没ææç»è®ºåä¸å»**ï¼ - å¤å®åç §æ ·æ¸²æåºãæ å¤å®è§åãï¼ä¸è¢«åç»ç»æåæï¼ï¼å¸¦è§æ ¼çåé¡¹ç §å¸¸å¤ãåæ ¼ãã `undecidableCount` çå£å¾å·®ä¸ç¹æ²¡åæ¸ ï¼å®æ°çæ¯**å¿«ç §é `result` 为空串ç项**ï¼ éé¢**æ··ç两ç§ç¶æ** ââ ãæ å¤å®è§åãï¼åæ³ï¼ä¸ãå¾ å¤å®ãï¼è§åæ§è¡å¤±è´¥ï¼æ¯çé®é¢ï¼ã æä»¥å端**ä¸è¦æå®å½æ 鿥è¦**ï¼è§ `docs/qc_report_generate_from_qc_frontend_integration.md` çå±ç¤ºè§åï¼èè²ä¿¡æ¯æ¡èéé»è²åè¦ï¼å 为å 乿¯ä¸ä»½æ¥åé½ä¼æè¿ç¨åæ°ï¼ã ### 21.6 æ¹å¨æ¸ å **å端 `yudao-module-mes`** - `MesQcIndicatorDO`ï¼è¡¥æ å° `parentId` / `itemType` / `formulaText` / `unitText` / `sortOrder` ï¼**åªå æ å°ï¼åæ¬æ¥å°±å¨åºé**ï¼æ¬è½®æ²¡å»ºè¡¨ãæ²¡æ¹ SQLï¼ã - æ°å¢ `MesQcIndicatorItemTypeEnum`ï¼1=å½å ¥é¡¹ï¼2=åç»é¡¹ï¼ã - æ°å¢ `api/qc/MesQcReportApiImpl` + API æ¥å£ï¼åå±åæ°å½ä¸å**æ `parent_id` è¿å两å±**ï¼ ç»å¤´çéç¹è¡ãå项è¡åå«äº§åºè¡ï¼è¡¥é½ã没æç» / è¡¥åºæ¥çè¡ / ææ å·²å ãä¸ç±» warningã **å端 `yudao-module-qcreport`** - `ReportContext`ï¼æ°å¢ `group`ï¼`childName` / `span` / `hidden`ï¼ï¼**æ¡ä»¶å¸¦åº**ï¼æ åç»å°±ä¸ç»ï¼ã - `ReportContextCodec`ï¼**æå** Mapâ对象转æ¢éè¡¥ `group`ï¼ä¸ç¨ Jackson åå°ï¼ æ¼äºè¿ä¸æ¥å»ç»å¿«ç §å°±ä¼ä¸¢åç»ï¼ã - `InspectionItem`ï¼æ°å¢ `requirement` ä¸åµå¥ `Group`ï¼**`copy()` ä¸å¹¶å¸¦åº** ï¼`ReportEvaluator` ä¼è° `copy()`ï¼ã - `MesQcReportContextMapper`ï¼åç»æ¶é项ä¸å `group`ãç»å¤´ä¸æ¥æ¼å½å®æµå¼ã䏿¡ warning ææ¡ã - `QcReportInstanceServiceImpl`ï¼`undecidableCount` å£å¾ä¸ `1_070_105_006` ææ¡ã **å端 `mom-pro2-before`** - æ°å¢ç¬¬ 13 个ç»ä»¶ `inspection/grouped-quality-table.ts`ï¼7 åï¼ã - `engine/context.ts`ï¼æ°å¢ `ReportContextItemGroup` ä¸ `item.group?`ã - `engine/render.ts` æªæ¹ï¼`rowspan` / `hidden` èµ°ç»å®å±æ§è¿æ¡è·¯ï¼æ¸²ææä¸åä¸è®¡ç®ï¼ã ### 21.7 éªè¯ï¼å·²å / æªååå¼å£°æï¼ **å·²åï¼ç¡®å®æ§ï¼** - **å端 `mvn -o -pl yudao-module-qcreport -am test`ï¼`Tests run: 141, Failures: 0, Errors: 0` å ¨ç»¿** ï¼Â§19/§20 è®°å½ä¸º 137ï¼æ¬è½®æ°å¢ 4 æ¡å ¨å¨ `MesQcReportContextMapperTest`ï¼è¯¥ç±»å ± 15 é¡¹ï¼ `mapsGroupStructure` / `groupAnchorDoesNotWarnAboutMissingActualValue` / `noGroupingLeavesGroupNull` / `warnsOnSkippedRows`ï¼ã â **å¿ é¡»å¸¦ `-am`**ï¼ä¸å¸¦ä¼æ¿å°æ¬å°ä»åºéæ§ç `mes-api` jarï¼æ¥ `NoSuchMethodError ⦠getGroupChildName()`ã - `mvn -o compile -pl yudao-module-mes -am -q` å¹²åã - **å端对æ `npx tsx .qc-conformance/assemble-ts.ts`**ï¼å ¨ç»¿ï¼ç»ä»¶æ°æè¨ç± 12 æ¹ä¸º **13**ã - **æ°å¢æè¨æ¢é `.qc-conformance/verify-grouped-table.ts`**ï¼ç»¿ï¼ï¼ä¸è ââ â æå¨**çå® `buildContent` 产ç©**ä¸ï¼è¡¨å¤´/è¡æ¨¡æ¿å 7 æ ¼ã`rowspan` **åªæå¨ä¸æ ¼ä¸** ï¼æéæ ¼ = ç»åè·¨éåï¼ãé夿 è®°è½å¨ `tbody/tr`ï¼â¡ ç»å¤´ + å项 + ç¬ç«é¡¹æ··æçéæ ¼æè¨ï¼ ⢠**é级**ï¼æ´ä»½æ°æ®é½æ²¡æ `group` æ¶éåææ®é表è䏿¯å¡æï¼æ `rowspan`/`hidden`ã è¡ä»å 满 7 åãå å®¹ç §å¸¸æ¸²æï¼ï¼ä¸ç¼ºå£ä»¥ `item.group.span` è¿ `errors` æ´é²ã**ä¸éé»**ã - **æµè§å¨å®æµ**è§ Â§21.3ï¼åä½ãæªå¾ï¼ã **æªå / ä¸ä¸»å¼ ** - **`FrontendConformanceTest` 没æå åç»ç¨ä¾**ï¼åå端éå对æéçæ¯ãæ¸²æäº§ç©ä¸åä¸å·®ãï¼ èåç»è¿ä¸é¢çå ³é®æ¯**æµè§å¨è¡¨æ ¼å¸å±**ï¼`rowspan` ä¸ `hidden` çè§£æç»æ**䏿¯åé¢éè½éä½ç**ã éåé¢éåªè½è¯æã两边é½è·³è¿/é½è¾åºåä¸ä¸ªå±æ§ãï¼è¯æä¸äºå没串ä½ãææçè¯æ®æ¯ä¸é¢ç æ¢é + ç宿µè§å¨éæ ¼æµéï¼æ æ¬è½®**ä¸å**è¿æ¡å¯¹æï¼**å¦å®å£°æä¸ºæªå**ã - **没æèµ°çå® HTTP èè°**ï¼çå® MES æ°æ® + éå¯å端 + UI éªæ¶ï¼ï¼æ¬è½®éªè¯å ¨é¨å¨ åå /对æ/æ¢éå±å®æã**è¿ä¸æ¡æ¯ã没åãï¼ä¸æ¯ãéè¿ãã** - **`GroupedQualityTable` ç AI éååç¡®çæªæµ**ï¼ä¸ä¸è½®ç宿µé½æ¯é对 `ReportHeader` / `ReportFooter` çéåï¼**没æè§å¯è¿æ¨¡åä¼ä¸ä¼æ£ç¡®æä¸è¿ç¬¬ 13 个ç»ä»¶**ã æ¨¡åéååªè½é çå®è°ç¨è§å¯ï¼ä¸ä¼ªé ç¡®å®æ§ã ### 21.8 æ¬è½®é¡ºå¸¦åç°ï¼**æªä¿®**ï¼äº¤ç¨æ·å¤æï¼ - **`docs/sql/config_export_all_20260918.sql` å·²ä¸è¿è¡åºæ¼ç§»**ï¼å®æµæ¯å¯¹ information_schema ä¸ å¯¼åºæä»¶ç `CREATE TABLE` åéåï¼ï¼ | 项 | æ°é | |---|---| | è¿è¡åºæã导åºé**没æç表** | 3ï¼`after_sale_problem` / `after_sale_visit` / `bpm_process_auto_approve_config`ï¼ | | è¿è¡åºæã导åºé**没æçå** | **86** | | 导åºéæãè¿è¡åº**没æç表** | 6ï¼`bpm_bak_20260904_*` å¤ä»½è¡¨å·²ä¸åå¨ï¼ | å ¶ä¸å±æ¬è½®ç¸å ³çåªæ `mes_qc_indicator` ç 5 åï¼`parent_id` / `item_type` / `formula_text` / `unit_text` / `sort_order`ï¼ï¼**å ¶ä½ 80 ä½åæ¥èªæ´æ©ç轮次** ï¼å®åé®é¢åºãBPM å 审æ¹å¼å ³ãIQC/IPQC/OQC/RQC çå®¡æ ¸ä¸æ§è¡æ ååæ®µçï¼ã æ `sql-config-export-init.md`ï¼æ°åºåå§åä¼**缺è¿äºå**ã**æ¬è½®æªå¨è¯¥æä»¶**ï¼ è¡¥å ¨å®çäºæå ¶ä»è½®æ¬¡çæ¼ç§»ä¸å¹¶å¿«ç §è¿æ¥ï¼å±äºè¦åç¬ææ¿çæä½ï¼ä¸å¤¹å¸¦å¨æ¬è½®éã - **ææ ä¸»æ°æ®ï¼`mes_qc_indicator`ï¼çå±çº§å段没æ CRUD çé¢**ï¼ `SaveReqVO` / `RespVO` éæ²¡æ `parent_id` / `item_type` / `formula_text` / `unit_text` / `sort_order` çæ å°ï¼**ç°å¨åªè½é SQL é åç»æ°æ®**ã æ¬è½®åªåäºãè¯»åºæ¥æ¸²æææ¥åãï¼**没æåãç¼è¾åç»ã**ã ### 21.9 æµè¯æ°æ®æ¸ ç æ¬è½®**æªä¸ä¼ æä»¶ãæªå»ºæ¨¡æ¿ãæªçææ¥åå®ä¾**ï¼ä¸äº§ç `blob` / éä»¶ / 模æ¿çæ¬ï¼ `docs/` ä¸çæ¹å¨ä¸ºææ¡£ï¼`mom-pro2-before/.qc-conformance/` ä¸ç产ç©ä¸ºæ¢éè¾åº ï¼`.html` / `.png`ï¼ä¸å ¥åºï¼ãç¨æ·èªå·±ä¸ä¼ ç `ç¾äºæ¨¡ç.docx`ï¼blob 67/70ãéä»¶ 73/74ï¼**å ¨ç¨æªå¨**ã ## äºåäºãè´¨æ£ææ å±çº§å¯ç»´æ¤ï¼åç»æ°æ®ä»ãåªè½ SQL é ãå°ãçé¢è½ç¼è¾ãï¼2026-09-19 ç»ï¼ > æ¿æ¥ §21.8 çç¬¬äºæ¡åç°ï¼ãææ ä¸»æ°æ®çå±çº§å段没æ CRUD çé¢ï¼ç°å¨åªè½é SQL é åç»æ°æ®ãã ### 22.1 è¦è¡¥çæ¯ãåãï¼ä¸æ¯ã读ã §21 åªåäºã**读**ãï¼æ¥åä¾§å·²è½æ `mes_qc_indicator.parent_id` æåç»é¡¹ä¸å®çå项è¿åæä¸¤å±è¡¨å¤´ã ä½ã**å**ãæ¯ç¼ºç ââ `MesQcIndicatorSaveReqVO` / `RespVO` å®å ¨æ²¡æå±çº§å段æ å°ï¼ `mes_qc_indicator` ç `parent_id` / `item_type` / `formula_text` / `sort_order` / `unit_text` äºå**å¨ä¸å¡çé¢ä¸æ å¤å¯å¡«**ã åæï¼ç°ç½é£ 3 个åç»ï¼`æ°´å` / `é ¸å«é` / `æ£ä¸éå«é`ï¼åªè½é SQL é ï¼ è¿ç»´æ³å ä¸ä¸ªåç»ãææä¸ªææ æè¿åç»ãè°æ´åç»é¡ºåºï¼å ¨é½å¾è¿æ°æ®åºã **æ¥åä¾§ä¸è¡æªæ¹**ï¼å·²æçåç»è¿åé»è¾æ `parent_id` åæ°ï¼ çé¢é åºæ¥çåç»ä¸ SQL é åºæ¥çåç»å¨æ¥åéå®å ¨åå½¢ã ### 22.2 ç¨æ·ææ¿çåæ¡å£å¾ | å²è·¯ | ææ¿ | |---|---| | åæ®µèå´ | **åªå å±çº§å项**ï¼æ¡ç®ç±»å / æå±åç» / å ¬å¼ / æåºå·ï¼ã`unit_text` **ä¸è¿çé¢** | | å 餿å项çåç» | **级èå é¤å项** | | å表åç° | **å¹³éºè¡¨æ ¼ + 两å**ï¼æ¡ç®ç±»å / æå±åç»ï¼ï¼ä¸æ¹ vxe æ è¡¨æ ¼ | | å±çº§æ·±åº¦ | **åªå 许两å±** | `unit_text` ä¸è¿ç便®ï¼å®æµï¼éæ¨æï¼ï¼å ¨æ¨¡å grep 该ååªåºç°å¨ `MesQcIndicatorDO.java` ä¸å¤å£°æï¼ **没æä»»ä½è¯»åæ¹**ãæ¥åçãåä½ãåèªæ£éªæ¹æ¡è¡ç `unit_measure_id` â `mes_md_unit_measure.name`ï¼ åè½ææ¯ææ èªå¸¦åä½ãç颿¾ä¸ä¸ªæ²¡äººè¯»çåæ®µåªä¼è¯¯å¯¼å½å ¥äººã ### 22.3 æ°æ®å®è¯ï¼å®¹å¨ `mysql8` / `ruoyi-vue-pro`ï¼`deleted=0`ï¼ 34 è¡æææ°æ®ï¼31 æ¡å½å ¥é¡¹ + 3 æ¡åç»é¡¹ï¼**å±çº§å䏿¡ NULL 齿²¡æ**ã | å | åç»é¡¹ï¼`item_type=2`ï¼3 è¡ï¼ | å½å ¥é¡¹ï¼`item_type=1`ï¼31 è¡ï¼ | |---|---|---| | `parent_id` | 3 æ¡å ¨ä¸º `0` | 9 æ¡ä¸ºåç» idï¼22 æ¡ä¸º `0` | | `result_type` | **å ¨ä¸º NULL** | å ¨æå¼ï¼1=FLOATï¼ | | `formula_text` | å ¨æå ¬å¼ï¼å¦ `w = (m1 - m0) / m à 100%`ï¼ | å ¨ä¸º NULL | | `unit_text` | å ¨ä¸º NULL | æå¼ï¼`g` / `%` / `mol/L` / `mL`ï¼ | | `sort_order` | 顶级 30 / 40 / 50 | 顶级 10..70ï¼å项 1..4 | 两æ¡ç±æ¤æ¨åºç硬å£å¾ï¼ 1. **åç»é¡¹ç `result_type` 为 NULL** â `SaveReqVO` ä¸ `resultType` ç `@NotNull` å¿ é¡»æ¿æï¼ æ¹æãä» å½å ¥é¡¹å¿ å¡«ãçæå¡ç«¯æ¡ä»¶æ ¡éªï¼è½åºæ¶åç»é¡¹å¼ºå¶æ¸ 空 `resultType` / `resultSpecification`ã 2. **`sort_order` æ¯ `NOT NULL DEFAULT 0`**ï¼ä½æ²¡æãå级æ¥é¿ã约æï¼é¡¶çº§æ¥é¿ 10ï¼å项æ¥é¿ 1ï¼ â æ°è®°å½ç空æ¶ç±æå¡ç«¯æä¸åé»è®¤å¼è¡¥é½ã å±çº§åä¸ç´¢å¼ `idx_qc_indicator_parent_id` **ç°ç½å·²åå¨**ï¼å¦ä¸å¼åè ç `feat(mes): æ°å¢è´¨æ£ææ åç»æ åå¹³è¡æµéåè½` å¸¦è¿æ¥çï¼â **æ¬è½®æ è¡¨ç»æåæ´ï¼ ä¸å¨ `docs/sql/config_export_all_*.sql`ã** ### 22.4 å æ¡å±çº§æ ¡éªè§åï¼æä¸å¡ä¼å çº§åªæç¬¬ä¸å¤ï¼ | é¡ºåº | æ¡ä»¶ | é误ç | |---|---|---| | 1 | åç»é¡¹å´å¸¦ `parentId != 0` | `1040601007` `QC_INDICATOR_GROUP_CANNOT_NEST` | | 2 | `parentId == id` | `1040601006` `QC_INDICATOR_PARENT_SELF` | | 3 | ç¶ææ ä¸åå¨ | `1040601004` `QC_INDICATOR_PARENT_NOT_EXISTS` | | 4 | ç¶ææ 䏿¯åç»é¡¹ | `1040601005` `QC_INDICATOR_PARENT_NOT_GROUP` | | 5 | å½å ¥é¡¹è `resultType` 为空 | `1040601009` `QC_INDICATOR_RESULT_TYPE_REQUIRED` | | 6 | æ¹æåç» / æ¹æå½å ¥é¡¹ï¼ä½è¯¥ææ å·²æå项 | `1040601008` `QC_INDICATOR_HAS_CHILDREN`ï¼ä¸¤ç§åºæ¯**ææ¡åèªç¬ç«**ï¼ | **ãåªå 许两å±ã没æåç¬å䏿¡æ ¡éª** ââ å®ç±è§å 1 + è§å 4 èªå¨æ¨åºï¼ç¶å¿ é¡»æ¯åç»é¡¹ï¼ èåç»é¡¹åªè½é¡¶çº§ï¼æä»¥ç¬¬ä¸å±ä¸å¯æé ã**ä¸å䏿¡å¯è¾¾æ§ä¸ºåçæ ¡éªã** æéä¸å¾å¸¦å ·ä½å¼ï¼åç»å / ç¶ææ å / åé¡¹æ¡æ°ï¼ä¸å¯æ§è¡å¨ä½ï¼ éµå® `error-message-precision.md`ï¼ä¸åæç¬¼ç»å¥ã ### 22.5 ä¿®æä¸ä¸ªçå®çåå ¥ç¼ºé·ï¼åæµæåºæ¥çï¼ä¸æ¯æµè¯åè±¡ï¼ `testUpdateIndicator_toGroup_clearsResultFields` æè¨å¤±è´¥ï¼`expected: <null> but was: <5>`ã æ ¹å ï¼MyBatis-Plus ç `updateById` é»è®¤ç¨ `FieldStrategy.NOT_NULL`ï¼**伿 `null` åæ®µä» UPDATE è¯å¥éåé¤**ãæä»¥ãå½å ¥é¡¹æ¹æåç»é¡¹ãæ¶ï¼`normalizeHierarchy` è½ç¶æ `resultType` ç½®äº `null`ï¼ **è¿ä¸ª null æ ¹æ¬æ²¡åè¿åº**ï¼æ§çç»æå¼ `5` æ®ç䏿¥ã è¿æ¯**ç产缺é·**ï¼ä¸æ¯æµè¯åæ³é®é¢ï¼æ¹æåç»é¡¹åç»æå¼ä¼éé»ççã ä¿®æ³æ¯ DO 级声æå¼ï¼ ```java @TableField(updateStrategy = FieldStrategy.ALWAYS) private Integer resultType; @TableField(updateStrategy = FieldStrategy.ALWAYS) private String resultSpecification; ``` éè¿æ¡è䏿¯æ¾å¼ `LambdaUpdateWrapper` ççç±ï¼ä»åºæç°æå ä¾ ï¼`DictDataDO.java:60`ã`CrmContractConfigDO.java`ã`CrmCustomerPoolConfigDO.java`ï¼ï¼ ä¸è¯¥ DO åªç± `MesQcIndicatorServiceImpl` åå ¥ï¼å 注解ççç¸åå¾å¯æ§ã **è¿æ®µä»·å¼å¨äºï¼åæµéä½äº `updateById` ç null åé¤è¡ä¸ºï¼èè¿å¨çº¯è¯»ä»£ç æ¶ä¸ä¼æ´é²ã** 顺带修çå¦ä¸å¤ï¼`validateResultSpecification` 忬æ¯ä¸¤åï¼è¢«ç»§æ¿ç代ç ç¨ä¸åè°ç¨ï¼ç¼è¯ä¸è¿ï¼ã æ¹ä¸ºææ¡ç®ç±»åæåè¿å ââ **åç»é¡¹çç»æå¼è½åºåä¼è¢«æ¸ ç©ºï¼æä»¥ä¸è¯¥è¢«ãç»æå¼å±æ§ä¸è½ä¸ºç©ºãæ¦ä½**ã ### 22.6 æ¹å¨æ¸ å **å端 `yudao-module-mes`ï¼æ¹ï¼** - `MesQcIndicatorDO`ï¼å 常é `PARENT_ID_ROOT = 0L`ï¼`resultType` / `resultSpecification` å `@TableField(updateStrategy = FieldStrategy.ALWAYS)`ï¼è§ §22.5ï¼ã - `MesQcIndicatorSaveReqVO`ï¼å `itemType` / `parentId` / `formulaText` / `sortOrder`ï¼ `resultType` 廿 `@NotNull`ã - `MesQcIndicatorRespVO`ï¼å `parentId` / `itemType` / `formulaText` / `sortOrder` / `parentName`ï¼ `@ExcelProperty` åªæ `æå±åç»` / `å ¬å¼` / `æåºå·` ä¸å¤ï¼`itemType` ä¸ææ³¨è§£ï¼ ç±»ä¸å·²æ `@ExcelIgnoreUnannotated` â ä¸è¿ Excelï¼ã - `MesQcIndicatorPageReqVO`ï¼å `itemType`ã - `MesQcIndicatorMapper`ï¼å `selectListByItemType` / `selectListByParentId` / `selectCountByParentId` / `selectMaxSortOrder`ï¼`selectPage` 追å `eqIfPresent(itemType)`ã - `MesQcIndicatorService` / `Impl`ï¼å `getIndicatorGroupList()`ï¼æ°å¢ `validateHierarchy` / `normalizeHierarchy`ï¼`deleteIndicator` æ¹çº§èå å项ï¼`validateResultSpecification` æ¹ä¸åã - `MesQcIndicatorController`ï¼`/page` ä¸ `/export-excel` ç¨ç§æ `fillParentName` åå¡« `parentName`ï¼æ°å¢ `GET /group-list`ã - `ErrorCodeConstants`ï¼`_004` ~ `_009` å æ¡æ°é误ç ã **åç«¯ï¼æµè¯ï¼** - æ°å¢ `MesQcIndicatorServiceImplTest`ï¼**14 ä¾**ï¼`BaseDbUnitTest` 飿 ¼ï¼H2 + çå® Mapperï¼ã - `src/test/resources/sql/create_tables.sql` è¡¥ `CREATE TABLE IF NOT EXISTS "mes_qc_indicator"` ï¼**纯追å 26 è¡ï¼0 å é¤**ï¼ï¼`clean.sql` è¡¥ `DELETE FROM "mes_qc_indicator";`ã **å端 `mom-pro2-before`ï¼æ¹ï¼** - `packages/constants/src/biz-mes-enum.ts`ï¼å `MesQcIndicatorItemType = { ENTRY: 1, GROUP: 2 }`ã **ä¸ååå ¸** ââ 1/2 两个å¼å¨å端æä¸¾ä¸æ¥åè¿åé»è¾é齿¯ç¡¬ç¼ç ï¼åæå¯æ¹å¼åå ¸ä¼ä¸ä»£ç è±èï¼ å页å表ç `MesQcResultValueType` å°±æ¯èµ°ç常éè·¯ã**æ¬è½®ä¸äº§çä»»ä½ DB / é ç½®åæ´ã** - `api/mes/qc/indicator/index.ts`ï¼`Indicator` æ¥å£å 5 ä¸ªåæ®µï¼å `getIndicatorGroupList()`ã - `views/mes/qc/indicator/data.ts`ï¼è¡¨åå æ¡ç®ç±»åï¼RadioGroupï¼/ æå±åç»ï¼`ApiTreeSelect`ï¼/ å ¬å¼ï¼Textareaï¼ä» åç»é¡¹ï¼/ æåºå·ï¼InputNumberï¼ï¼æç´¢è¡¨åå ãæ¡ç®ç±»åãï¼ å表åå ãæ¡ç®ç±»åãï¼slot Tagï¼ä¸ãæå±åç»ãï¼`parentName`ï¼é¡¶çº§æ¾ç¤º `-`ï¼ã - `views/mes/qc/indicator/index.vue`ï¼å `#itemType` / `#parentName` ä¸¤ä¸ªææ§½ã **æ²¡ææ°å¢èå䏿éç **ï¼`/group-list` å¤ç¨æ¢æç `mes:qc-indicator:query`ã ### 22.7 å端两å¤ãå¿ é¡»è¿ä¹åãçç»è 1. **忢æ¡ç®ç±»åæ¶ä¸»å¨æ¸ 空å½å ¥é¡¹ä¸å±å段**ï¼`itemType` ç `dependencies.trigger` éæ `parentId` ç½® `0`ã`resultType` / `resultSpecification` ç½® `undefined`ã **为ä»ä¹å¿ é¡»æ¸ **ï¼vben ç `dependencies.if` æ¯ãå é¤ DOMãè¯ä¹ï¼è¢«ç§»é¤çåæ®µï¼ å ¶å¼**ä»çå¨è¡¨åæ°æ®é**ï¼ä¼ä¸èµ·æäº¤ï¼æ®çç `parentId` ä¼è¢«å端å±çº§æ ¡éªé¡¶åæ¥ã 2. **`resultType` çå¿ å¡«ä»éæ `rules: 'required'` æ¹ä¸ºææ¡ç®ç±»åæ¡ä»¶å¤æ**ï¼ åç»é¡¹å¨è¡¨åä¸ä¸å±ç¤ºè¯¥å段ï¼`dependencies.if`ï¼ï¼ä¹ä¸å䏿 ¡éªï¼ æå¡ç«¯åæ ·ææ¡ç®ç±»åæ¡ä»¶æ ¡éªï¼è§ §22.4 è§å 5ï¼ã ### 22.8 éªè¯ï¼å·²å / æªååå¼å£°æï¼ **å·²åï¼ç¡®å®æ§ï¼** - `mvn compile -pl yudao-module-mes -am -q` â å¹²åéåºã - `mvn -o -pl yudao-module-mes test -Dtest=MesQcIndicatorServiceImplTest` â **`Tests run: 14, Failures: 0, Errors: 0`ï¼BUILD SUCCESS**ã ç¨ä¾éæ¡è¦ç §22.4 çè§åï¼å»ºåç»æåï¼ç»æå¼è¢«æ¸ 空ã`sortOrder` èªå¨è¡¥ 10ï¼/ 建å项æåï¼`sortOrder` = å级 max+1ï¼/ 顶级æ¥é¿ 10 / åç»å¸¦ `parentId` 被æ / ç¶ä¸æ¯åç»é¡¹è¢«æ / ç¶ä¸åå¨è¢«æ / ç¶æ¯èªå·±è¢«æ / å½å ¥é¡¹ç¼ºç»æå¼è¢«æ / ç»æå¼å±æ§ç¼ºå¤±è¢«æ / åç»é¡¹ä¸å¯åµå¥ / æ¹æåç»é¡¹æ¸ ç»æå¼ / æå项ä¸è½æ¹æåç» / æå项ä¸è½æ¹æå½å ¥é¡¹ / 级èå é¤ / ææ ä¸åå¨ã - å端 `pnpm typecheck`ï¼**æ¬è½®æ¹å¨çæä»¶ 0 æ¥é** ï¼`wls/*` çæ¢ææ¥éä¸å¨æ¬è½®èå´ï¼ã **å·²åï¼çå® HTTP + UI éªæ¶ï¼2026-09-19 ç¨æ·éå¯å端åï¼** éå¯åå æ ¸å®äºãè·çæ¯ä¸æ¯æ°ç±»ãï¼`/group-list` 彿¶è¿å 404ã`/page` çäºä¸ªæ°åæ®µå ¨ `null`ï¼ èæå¨åºï¼`@@hostname=2a8dea946ada`ï¼è¿äºå**䏿¡ NULL 齿²¡æ** â å¤ä¸ºãé¨åæ§ãé¨åæ°ãç æ··å classpathï¼è¯·ç¨æ·éå¯ãéå¯å `/group-list` è¿å 3 个åç»ï¼éªæ¶éå³è·éã **HTTP éªæ¶ï¼14 项ï¼å ¨è¿ï¼** | # | ç¨ä¾ | ç»æ | |---|---|---| | 1 | `/page?itemType=2` è¿æ»¤ | `total=3`ï¼åªè¿ååç»é¡¹ | | 2 | å项 `parentName` åå¡« | å项 22/23/24 ç `parentName` = `æ°´å`ï¼**ç¶ææ ä¸å¨å䏿¬¡çéç»æéä¹è½è§£æ**ï¼ | | 3 | 建åç»ï¼å¸¦ `resultType=1`ã`sortOrder` çç©ºï¼ | æåï¼è½åº `result_type=NULL`ï¼è¢«æ¸ 空ï¼ã`sort_order=80`ï¼å级 max 70 + 10ï¼ | | 4 | 建顶级å½å ¥é¡¹ï¼`sortOrder` çç©ºï¼ | æåï¼`sort_order=90`ï¼max 80 + 10ï¼ | | 5 | è´åï¼åç»é¡¹å¸¦ `parentId` | `1040601007`ï¼ææ¡å«`[ZZéæ³åµå¥åç»]`+`[æ°´å]`+æ¹æ³ | | 6 | è´åï¼ç¶æ¯å½å ¥é¡¹ï¼id=1 é ¸å¼ï¼ | `1040601005`ï¼ææ¡å«ç¶å + å ¶æ¡ç®ç±»å䏿å + æ¹æ³ | | 7 | è´åï¼ç¶æ¯èªå·± | `1040601006`ãç¶ææ ä¸è½æ¯èªå·±ã | | 8 | è´åï¼å½å ¥é¡¹ç¼ºç»æå¼ç±»å | `1040601009` | | 9 | è´åï¼ç¶ææ ä¸åå¨ï¼`parentId=999999`ï¼ | `1040601004`ï¼ææ¡å«ç¼å· 999999 + æ¹æ³ | | 10 | æå项ï¼`sortOrder` çç©ºï¼ | æåï¼`sort_order=1`ï¼**å级æ¥é¿ 1**ï¼ä¸é¡¶çº§ç 10 åºåå¼ï¼ | | 11 | è´åï¼æå项ä¸è½æ¹æå½å ¥é¡¹ | `1040601008`ï¼ææ¡å«åé¡¹æ¡æ° + ã请å å é¤æç§»èµ°è¿ 1 个å项ã | | 12 | æ¹æå°ç°ç½åç» 21ãæ°´åã | æåï¼å读 `parent_id=21` | | 13 | å åç»ï¼å¸¦ 2 个åé¡¹ï¼ | æåï¼**å读åç»ä¸å ¶ 2 个å项 `deleted` å ¨é¨ç¿»ä¸º 1**ï¼çº§èï¼ | | 14 | å¯¼åº Excel | HTTP 200 / 6 350 åèï¼è§£å `sheet1.xml` 读表头 = `ç¼å·ï½æ£æµé¡¹ç¼ç ï½æ£æµé¡¹åç§°ï½æ£æµé¡¹ç±»åï½æ£æµå·¥å ·ï½æå±åç»ï½å ¬å¼ï½æåºå·ï½ç»æå¼ç±»åï½ç»æå¼å±æ§ï½å¤æ³¨ï½å建æ¶é´` ââ **ä¸åæ°å¢é½å¨ã`æ¡ç®ç±»å` ç¡®å®ä¸å¨** | **â 䏿¡å£å¾éè¦è¯´æ¸ ï¼ç¨ä¾ 11 çææ¯ï¼**ï¼è§å 6 ç**第ä¸ä¸ªåæ¯**ï¼ãæå项æ¶ä¸è½æå°å«çåç»ä¸ãï¼ **ç»çé¢/æ¥å£ä¸å¯è¾¾** ââ è½å¸¦å项çåªæåç»é¡¹ï¼èåç»é¡¹æ³é `parentId` ä¼å 被è§å 1 ï¼`1040601007`ï¼æ¦æï¼å®æµç¨ä¾ãæå项ä¸è½æå°å«çåç»ä¸ãè¿åçæ£æ¯ `1040601007` è䏿¯ `1008`ï¼ **è¿æ¯è®¾è®¡ä½¿ç¶ï¼ä¸æ¯ç¼ºé·**ï¼è§å 1 ç»çæ¹æ³æ´å¯æ§è¡ï¼ãè¯¥åæ¯åªå¨**åºéåå¨ãæ¡ç®ç±»å=å½å ¥é¡¹å´æå项ã ç SQL é æ°**æ¶æå¯è½å½ä¸çï¼åæµ `testUpdateIndicator_hasChildrenCannotReparent` æ£æ¯**ç´æ¥ç¨ mapper é åºè¿ç§è¡**å»è¦çå®ãè§å 6 ç第äºä¸ªåæ¯ï¼æ¹æå½å ¥é¡¹ï¼ç»çé¢å®å ¨å¯è¾¾ï¼ç¨ä¾ 11ï¼ã **UI éªæ¶ï¼Playwrightï¼å ¨è¿ï¼** - **å表两å**ï¼è¡¨å¤´ä¸º `æ£æµé¡¹ç¼ç / æ£æµé¡¹åç§° / æ¡ç®ç±»å / æå±åç» / æ£æµé¡¹ç±»å / â¦`ï¼ åç»é¡¹æ¾ç¤º Tag `åç»é¡¹` + æå±åç» `-`ï¼å项æ¾ç¤º Tag `å½å ¥é¡¹` + æå±åç»ä¸º**ç¶åç»å**ã - **æ°å¢è¡¨åé»è®¤æ**ï¼æ¡ç®ç±»å RadioGroup é»è®¤éä¸ãå½å ¥é¡¹ãï¼`z.number().default()` çæï¼ï¼ å¯è§å段为 ç¼ç / åç§° / æ¡ç®ç±»å / æå±åç» / æ£æµé¡¹ç±»å / æ£æµå·¥å · / æåºå· / ç»æå¼ç±»å / 夿³¨ï¼**æ å ¬å¼**ã - **åå°ãåç»é¡¹ã**ï¼`æå±åç»` ä¸ `ç»æå¼ç±»å` **ä» DOM æ¶å¤±**ï¼`å ¬å¼` åºç°ä¸å¸¦å¿ å¡«æå·ã - **èµ°å®æ´æ¡é¾è·¯**ï¼UI 建åç»ï¼`ZZ-UI-GRP`ï¼â è½åº `item_type=2 / parent_id=0 / result_type=NULL / formula_text` é½å ¨ï¼ å建å½å ¥é¡¹å¹¶**ä»ä¸ææ éé䏿°åç»**ï¼æ èç¹ååº `æ°´å / é ¸å«é / æ£ä¸éå«é / ZZçé¢åç»` 4 é¡¹ï¼ æ°å¢çåç»**ç«å³å¯é**ï¼â è½åº `parent_id=40 / sort_order=1`ã - **UI å é¤åç»**ï¼äºæ¬¡ç¡®è®¤æ°æ³¡ææ¡ä¸ºãç¡®å®å é¤ ZZçé¢åç» åï¼ãï¼ç¡®è®¤å**å读åç»ä¸å ¶å项 `deleted` å ¨ä¸º 1**ã - **ç¼è¾åç»ï¼åªè¯»æ£æ¥ï¼æªä¿åï¼**ï¼æå¼ç°ç½åç» 21ãæ°´åãçä¿®æ¹å¼¹çª ââ `æå±åç»` **ä¸å¨ DOM é**ã `å ¬å¼` å¸¦å¿ å¡«æå·ï¼ä¸ `code / name / formulaText / sortOrder=30 / remark` å ¨é¨æ£ç¡®åå¡«ã **æªå / ä¸ä¸»å¼ ** - **`group-list` å¨åç»é¡¹æ°éå¾å¤§æ¶çè¡¨ç°æªè¯ä¼°**ï¼ç®åæ¯å ¨éè¿åãæ å页ã - **没æå¨çé¢ä¸éä¸è¯è¿å ¨é¨ 6 æ¡é误ç ç toast åç°**ï¼é误ç 䏿æ¡å¨ HTTP å±å·²éæ¡éªè¿ï¼ UI å±éªçæ¯è¡¨åå½¢æä¸é¾è·¯ï¼æªéæ¡å¶é é误ç Toastï¼ã ### 22.9 æ¬è½®é¡ºå¸¦è¸©å°çå·¥ç¨åï¼å¼å¾å¤ç¨ï¼ - **`mvn -o -pl yudao-module-mes test`ï¼ä¸å¸¦ `-am`ï¼ä¼æ¿å°æ¬å°ä»åºééæ§ç `mes-api` jar**ï¼ æ¥ `æ¾ä¸å°ç¬¦å· setGroupChildName/setGroupSpan/...` ä¹ç±»çåç¼è¯é误ã ä¿®æ³ï¼å `mvn -o -pl yudao-module-mes-api install -DskipTests -q` å·æ°ï¼æç´æ¥å¸¦ `-am`ã **§21.7 è®°è¿å䏿¡ï¼æ¬è½®åå¤ç°ä¸æ¬¡ã** - **å ¨æ¨¡åæµè¯å¥ä»¶æ 16 个类ã107 项失败ï¼ä¸æ¬è½®æ å ³ï¼ä¸å·²ç¨è¯æ®è¯ä¼ªãæ¯ææ¹åçã**ï¼ æ ¹å æ¯ä¸¤ç±»**åé**é®é¢ ââ â `NoSuchBeanDefinitionException: SrmSupplierApi`ï¼`MesQcIqcServiceImpl` æ³¨å ¥å®ï¼ ä½å ¶å·²æäº¤çæµè¯æ²¡æ `@MockitoBean`ï¼ï¼â¡ H2 æµè¯ DDL æ¼ç§» ï¼`Column "param_template_id"/"exclude_weekend"/"process_instance_id" not found`ï¼ã è¯æ®é¾ï¼`git status --short` 对è¿äºè·¯å¾**为空**ï¼æªè¢«æè§¦ç¢°ï¼ï¼ `git diff --stat create_tables.sql` = **26 insertions(+), 0 deletions**ï¼çº¯è¿½å ï¼ï¼ `git show HEAD:...create_tables.sql | grep -c <åå>` **ä¸åå¨ HEAD éå°±å·²æ¯ 0**ã **è¿ä¸æ¡å±äºå ¶ä»å¼åè çè½®æ¬¡ï¼æ¬è½®åªæ¥åãä¸ä»£ä¿®ã** - **夿ãè·çæ¯ä¸æ¯æ§ç±»ãçå¯å¤ç¨æ¢é**ï¼æ¿ä¸ä¸ª**åªææ°ä»£ç ææçæ¥å£**æä¸æ¬¡ ï¼æ¬è½®æ¯ `/group-list`ï¼ãè¿å 404 å³è¿ç¨éæ²¡ææ° Controllerï¼ è¿å 200 ä½åæ®µå ¨ `null` 忝ãé¨åæ§ãé¨åæ°ãçæ··å classpath ââ **æ¯çæä»¶ mtime æ´ç´æ¥**ã ### 22.10 æµè¯æ°æ®æ¸ ç éªæ¶æé´å ±é 6 è¡ï¼HTTPï¼35/36/37/38/39ï¼UIï¼40/41ï¼ï¼**å ¨é¨å·²ä»åºä¸ç¡¬å é¤**ï¼ å读ä¸å¼å·¥åé项ä¸è´ï¼ | 项 | æ¸ çå | å¼å·¥å | |---|---|---| | `code LIKE 'ZZ-%'` æ®ç | `0` | 0 | | `deleted=0` æ»è¡æ° | `34` | 34 | | åç»é¡¹ï¼`item_type=2`ï¼ | `3` | 3 | | æç¶çè¡ï¼`parent_id<>0`ï¼ | `9` | 9 | | ãæ°´åãçåé¡¹æ° | `4` | 4 | | `MAX(id)` | `34` | 34 | | åç» 21 æ¬èº«ï¼`SEED-BT-WATERï½æ°´åï½parent_id=0ï½item_type=2ï½sort_order=30ï½å ¬å¼`ï¼ | éåæ®µæªå | å | **ç°ç½é£ 34 è¡ï¼å« 21/26/31 ä¸ä¸ªåç»ä¸å项 22â25 / 27â30 / 32ï¼å ¨ç¨æªå¨**ï¼ åå æµè¯è·å¨ H2 å ååºï¼`jdbc:h2:mem:testdb`ï¼ï¼æ¯æ¬¡ç¨ä¾åç¨ `clean.sql` æ¸ è¡¨ï¼**ä¸ç¢°è¿è¡åº**ã æ¬è½®æ ä¸ä¼ ï¼ä¸æ¶å `blob` / éä»¶ã临æ¶èæ¬ä¸è§£å äº§ç©æ¾å¨ä»åºå¤ç `D:/qcl-tmp/`ï¼å·² `mv` æ¹å彿¡£ï¼`rm -rf` 被æéæç»ï¼ã ## äºåä¸ãAI å¯¼å ¥è®¤ä¸åºãåç»æ£éªé¡¹è¡¨ãï¼æ ¹å 卿½åå±ä¸¢åå¹¶ä¿¡æ¯ï¼2026-09-19 ç»ï¼ ### 23.1 ç°è±¡ ç¨æ·æ¿ç宿¥ææ£éªæ¥å `ç¾äºæ¨¡ç.docx` èµ° AI å¯¼å ¥ï¼è¯å«åºæ¥çæ£éªè¡¨æ¯**ä¸å±å¹³éº**ç `QualityTable`ï¼èåä»¶çæ£éªé¡¹æ¯**两å±ç»æ**ï¼ä¸ä¸ªæ£éªé¡¹ç®ä¸æè¥å¹²å项ï¼é¡¹ç®å纵ååå¹¶ï¼ã ç¨æ·å¨ç¡¬å·æ°ï¼Ctrl+F5ï¼å夿µä»æ¯åæ ·ç»æã ### 23.2 æå 误å¤è¿ä¸æ¬¡ï¼è®°å¨è¿éé¿å éç¯ ç¬¬ä¸æ¬¡ææ¥æ¶ææ¿ `hint = "è¿æ¯ IQC æ¥ææ£éªæ¥åï¼è¡¨æ ¼æåç»çæ£éªé¡¹ç®"` å¤è·ï¼ 模å 3/3 齿ä¸äº `GroupedQualityTable`ï¼äºæ¯æå¤å®æ ¹å æ¯ãåç«¯ç§¯æ¨æ¸ åè¿æãå°äºè¿ä¸ªç»ä»¶ãã **è¿ä¸ªç»è®ºæ¯éç**ï¼èä¸éçåå 就卿èªå·±ï¼é£å¥ hint çäºæçæ¡åå¨äºé¢å¹²éï¼ æ¨¡åä¸éè¦ä»è¡¨æ ¼éçåºåç»ç»æï¼åªè¦è¯» hint å°±å¤äºãç¨æ·ç¡¬å·æ°åç°è±¡ä¾æ§ï¼è¯ä¼ªäºè¿æ¡å¤æã **æè®ï¼å¯å¤ç¨ï¼**ï¼å¤ç°ã模åééç»ä»¶ãè¿ç±»é®é¢æ¶ï¼`hint` **å¿ é¡»ç空**ï¼æåæ ·ç §æç¨æ·è¾å ¥çï¼ï¼ å¦åæµçæ¯ãæç¤ºè¯éææ²¡æåçæ¡ãï¼ä¸æ¯ã模åè½ä¸è½èªå·±çåºæ¥ãã ### 23.3 çæ ¹å ï¼å®è¯ï¼éæ¨æï¼ æ¿ç©º hint å¤è·ï¼æ¸ å 13 项ã`GroupedQualityTable` å¨åºï¼ï¼æ¨¡å**æ¯æ¬¡é½æå¹³éºç `QualityTable`**ã äºæ¯è½¬åçæ¨¡åå°åºæ¶å°äºä»ä¹ââæçå® docx çæ½åç»ææåºæ¥ï¼é®é¢ä¸ç®äºç¶ï¼ `OfficeImportAdapter` ç¨ `row.getTableCells()` 䏿 ¼æ ¼åæåå Tab æ¼æ¥ï¼è POI 对åå¹¶åå æ ¼çè¡ä¸ºæ¯ï¼ | Word éçåæ³ | POI ç»ä½ ä»ä¹ | åæ | |---|---|---| | `w:gridSpan="2"`ï¼æ¨ªååå¹¶ï¼ | åªè¿å 1 个 `XWPFTableCell` | 被çä½çåå å**å空æ¶å¤±**ï¼è¯¥è¡åæ°ä¸è¡¨å¤´å¯¹ä¸ä¸ | | `w:vMerge`ï¼çºµååå¹¶ç»æ ¼ï¼ | æåæ¯**空串** | åç»ååªå¨ç¬¬ä¸è¡åºç°ï¼åå è¡çèµ·æ¥æ¯ã没æåç»çç¬ç«æ£éªé¡¹ã | å䏿¬¡è¯å«çæ½åç»æå¯¹ç §ï¼ç宿件ï¼éæé æ ·æ¬ï¼ï¼ ``` æ¹åï¼ç²åº¦-çä¸ç©æ¯ä¾ï¼%ï¼Particle Size-% RemainsON 20ç®ä¸ ANA.MTH-000034 0-5 åæ ¼Checkout 40ç®ä¸ ANA.MTH-000034 20-40 60ç®ä¸ ANA.MTH-000034 30-50 80ç®ä¸ ANA.MTH-000034 0-20 100ç®ä¸ ANA.MTH-000034 0-12 æ¹åï¼ç²åº¦-çä¸ç©æ¯ä¾ï¼%ï¼Particle Size-% RemainsON 20ç®ä¸ ANA.MTH-000034 0-5 åæ ¼Checkout âåä¸ 40ç®ä¸ ANA.MTH-000034 20-40 âåä¸ âåä¸ 60ç®ä¸ ANA.MTH-000034 30-50 âåä¸ âåä¸ 80ç®ä¸ ANA.MTH-000034 0-20 âåä¸ âåä¸ 100ç®ä¸ ANA.MTH-000034 0-12 âåä¸ ``` æ¹åçååè¡æ¯ã馿 ¼ç©ºçå¹³éºè¡ãââ**æ¨¡åæ²¡æä»»ä½ä¾æ®å¤æå®ä»¬å±äºä¸é¢é£ä¸è¡**ï¼ é¢ç®æ¬èº«å°±æ è§£ï¼æ¹åãä¸ä¸ä¸è¡åå¼ã被æè¯´åºæ¥ï¼ä¸¤å±ç»ææç¬¬ä¸æ¬¡è¿å ¥æ¨¡åçè§éã ### 23.4 æ¹å¨ï¼3 个ç产æä»¶ + 1 个æµè¯æä»¶ï¼ | æä»¶ | æ¹å¨ | |---|---| | `OfficeImportAdapter.appendBodyElements()` | è¡¨æ ¼åæ¯æ¢æ `appendTable()`ï¼æ `w:gridSpan` 补空å ä½ãæ `w:vMerge` æ ç»æ ¼ãæ¯è¡æ**æ´è¡¨ç½æ ¼å®½åº¦**ï¼åææè¡çæå¤§å¼ï¼ä¸æ¯è¡¨å¤´è¡ï¼è¡¥é½ | | `QcReportDocumentExtract` | æ°å¢å¸¸é `VERTICAL_MERGE_MARK = "âåä¸"`ãæ¾å¨è¿éæ¯å ä¸ºå®æ¯ãæ½ååºæ¥çææ¬é¿ä»ä¹æ ·ãè¿æ¡å¥çº¦çä¸é¨åï¼è `llm` å å·²ç»ä¾èµ `document` å ï¼`QcReportLlmCallPlanner` å°±å¼äº `QcReportDocumentExtract`ï¼ï¼ä¸¤è¾¹å ±ç¨ä¸ä»½å¸¸éï¼ä¸ä¼ååä¸ä¸ªåé¢éååèªæ¼ç§» | | `QcReportTemplatePromptBuilder.EXTRACTION_RULES` | 补第 8 æ¡ï¼è¯´æãâåä¸ã表示ãä¸ä¸ä¸è¡æ¯åä¸ä¸ªå¼ãï¼çºµååå¹¶ï¼ï¼ä¸æ¯å个åçæ®éå 容ï¼å¹¶æåºåä¸åè¿ç»åºç°æ¶é常就æ¯ä¸¤å±ç»æââ**使åªä¸ªç»ä»¶ä»ä»¥æ¸ å hint 为å**ï¼ä¸å¨è¿éæ¿ `hint` åå³å® | | `AiImportFixtures` + `QcReportImportAdapterTest` | æ°å¢ä¸¤ä¸ªå¤¹å ·ï¼çºµååå¹¶ / 横ååå¹¶ï¼ä¸ä¸¤æ¡ç¨ä¾ | 两å¤å»æçåèï¼ - **ç»æ ¼èªèº«ææåæ¶ä»¥æå为å**ï¼ä¸è¦çææ è®°ãç»æ ¼å¨ Word 鿬ä¸è¯¥æå 容ï¼çæå°±è¯´æå¶è¡¨ä¸è§èï¼ æ¤æ¶ä¸¢æåæ¯ä¸¢ç»ææ´å¯æã - **ç½æ ¼å®½åº¦åææè¡çæå¤§å¼**ï¼ä¸å表头è¡çåæ°ï¼è¡¨å¤´å¸¸å¸¸å¸¦æ¨ªååå¹¶ï¼æè¡¨å¤´å®½åº¦å伿ä¸é¢å è¡æªæã ### 23.5 éªè¯ï¼å·²å / æªååå¼å£°æï¼ **å·²å** - `mvn compile -pl yudao-module-qcreport -am -q` éè¿ã - `mvn -o -pl yudao-module-qcreport -am test` â **143 / 143 å ¨ç»¿**ï¼æ°å¢ 2 æ¡å¨å ï¼ã å ¶ä¸ `docxKeepsLayoutOrder` ç¨æ åå¹¶å¤¹å ·ï¼æ¹åè¾åºéåæªåï¼è¯æã没æåå¹¶çè¡¨æ ¼ä¸åå½±åãã - **ç¨æ¹åççå®ç±»è·ç宿件**ï¼`D:/qcl-tmp/MergeProbe.java`ï¼classpath æå° `target/classes`ï¼ è¾å ¥ç¨æ·é£ä»½ 142373 åèç `bs.docx`ï¼ï¼æ½åç»æå³ 23.3 çãæ¹åãå½¢æï¼ 5 è¡ `ç²åº¦-çä¸ç©æ¯ä¾` çåé¡¹å ¨é¨å¸¦ä¸ `âåä¸`ï¼è¡¨å¤´è¡ä¹ä» 5 æ ¼è¡¥æä¸æ°æ®è¡å¯¹é½ç 6 æ ¼ã **æªåï¼æªè³æ¬èè½ç¬ï¼** - **çå® HTTP å¤è·å°æªæ§è¡**ï¼è¿è¡ä¸çåç«¯ä»æ¯æ¹åçç±»ï¼å¯ç¨ 22.9 è®°å½çå¤å®æ¢é确认ï¼ï¼ éè¦ç¨æ·**æå·¥éå¯å端**ãéå¯åè¦ç¨ **空 hint** æä¸æ¬¡ `/qc-report/ai-import/draft`ï¼blob 以彿¬¡ä¸ä¼ 为åï¼ AI å¯¼å ¥å¼¹çªæ¯æ¬¡ä¸ä¼ é½ä¼æ¸ æä¸ä¸ä¸ª blobï¼id ä¼åï¼ï¼ç¡®è®¤æ¨¡åèªå·±æ¹æ `GroupedQualityTable`ã **å¨è·å®è¿ä¸æ¥ä¹åï¼åªè½å£°æãæ½åå±çè¾å ¥å·²ç»å ·å¤å¤å«ä¸¤å±ç»ææéçä¿¡æ¯ãï¼ ä¸è½å£°æãAI å·²ç»è½è®¤åºå¤æç»æã**ââåè çè´£ä»»å¨æ¨¡åï¼åªæçå®è°ç¨æè½åçã > åç»ï¼éå¯åå¤è·ç»ææ¯ **2/5**ï¼ä¸æ¯å ¨ä¸ï¼ï¼æ ¹å è½¬å°æç¤ºè¯æªè¾ä¸ï¼è§**第äºååè**ã ### 23.6 æ¬è½®æªå / å·²ç¥è¾¹çï¼å¦å®å£°æï¼ - **è¯ä¹å±çç¼ºå£æ²¡å¨**ï¼åä»¶éãæ£æµæ¹æ³ãè¿ä¸åã以åãæ£éªç»æ / ç»è®ºãè¿ç§**两级表头**ï¼ 13 个ç»ä»¶é没æä»»ä½ä¸ä¸ªè½è¡¨è¾¾ãè¿ä¸æ¯æ½åå±çé®é¢ââæåå·²ç»å®æ´æ½åºæ¥äºï¼è§ 23.3 çè¡å 容ï¼ï¼ æ¯ãå¯ç¨ç§¯æ¨ãç表达åè¾¹çãè¦è¡¥å°±å¾æ°å¢ä¸ä¸ªãå¯èªå®åçæ£éªè¡¨ãç»ä»¶ï¼ æé¡¹ç®è§åå±äº**å åº `docs/` è®¾è®¡ææ¡£å卿**çé£ç±»æ¹å¨ï¼æ¬è½®ä¸ç¢°ã - **æ¸ å大尿²¡è¿æ¥å¿**ï¼`QcReportAiImportServiceImpl` ç宿æ¥å¿æ `files/pages/calls/components/budgetExceeded/durationMs`ï¼ ä½æ²¡æ catalog 项æ°ã䏿¬¡åéå°ã模åéäºä¸åå¨çç»ä»¶ãè¿ç±»é®é¢æ¶ï¼è¿ä¸é¡¹è½ä¸ç¼æé¤ãæ¸ åä¼ éäºãã æ¹å¨å¾å°ï¼ä½ä¸å¨ç¨æ·æ¬è½®ãå¼å§ä¼åãææçèå´å ï¼çå¾ ç¨æ·ç¹å¤´ã ### 23.7 ä¸ä¸ªå¼å¾å¤ç¨çå·¥ç¨å Java ç `@argfile` æ¯**æå¹³å°é»è®¤å符é**读çãææå«ä¸æçè·¯å¾ï¼`D:/ç马/...`ï¼åè¿ argfile åï¼ `javac` æ¥ãæ¾ä¸å°ç¬¦å·ãï¼ä½éè¯¯ä¿¡æ¯æ¬èº«æ¯ä¹±ç ï¼çèµ·æ¥åå«çé®é¢ã **è§£å³åæ³**ï¼ææ°ç¼è¯ç `target/classes` æ·å°ä¸ä¸ªçº¯ ASCII è·¯å¾ï¼æ¬è½® `/d/qcl-tmp/qcr-classes-new`ï¼åæ¼è¿ classpathã ææ¥æ¶å¦æ `java @argfile` çæ¥ééåºç°ä¹±ç ï¼å æçå符éï¼å«å æç代ç ã ## äºååãåä¸ç»ï¼æç¤ºè¯æ¬èº«å¨ç»å¹³éºè¡¨æå¹¿åï¼2026-09-19 ç»ï¼ ### 24.1 ç°è±¡ï¼æ½åå±ä¿®å®ï¼ç¨æ·éå¯åãè¿æ¯è¿æ ·ã äºåä¸èè½å°åç¨æ·éå¯äºå端ï¼éä¼ åä¸ä»½ `ç¾äºæ¨¡ç.docx`ï¼è®¾è®¡å¨ç»å¸ä¸ä»æ¯**å¹³éº**åå ï¼æ£éªé¡¹ç®/æ åå¼/宿µå¼/å¤å®ï¼ãä¹å°±æ¯è¯´ï¼**è¾å ¥ä¾§çä¿¡æ¯å·²ç»å¤äºï¼æ¯æ¨¡å没é对ç»ä»¶ã** ### 24.2 空 hint å¤è·éåï¼ä¸æ¯ãå®å ¨æ²¡ç¨ãï¼æ¯ãä¸ç¨³å®ã æ¿ç¨æ·é£ä»½æä»¶ï¼blob 83ï¼éå¯åä¸ä¼ ï¼ç¡®è®¤å·²èµ°å°æ°ä»£ç ï¼ç¨**空 hint** æ 5 次 `/qc-report/ai-import/draft`ï¼ | æ¶ç¹ | å½ä¸ `GroupedQualityTable` | |---|---| | æ½åå±ä¿®å¤å | 0 / 5 | | æ½åå±ä¿®å¤åãæç¤ºè¯æªæ¹ | **2 / 5** | 2/5 è¯´ææ½åå±ä¿®å¤æ¯ççææï¼ä»ã䏿¬¡é½ä¸ä¸ãå°ãè½ä¸ãï¼ï¼ä½**ä¸ç¨³å®**ââç¨æ·çå°çé£ä¸æ¬¡å¤§æ¦çå°±æ¯æ²¡ä¸çé£ 3/5ã ### 24.3 çæ ¹å ï¼ç³»ç»æç¤ºè¯éæä¸å¤å¨ææ¨¡åå¾å¹³éºè¡¨ä¸æ¨ 审æç¤ºè¯å ¨æååç°ï¼çæ£å使¨¡åçæ¯æªè¾ï¼è䏿¯è½åï¼ 1. **æ½åè§å第 1 æ¡ç´æ¥ç¹åäº `QualityTable`**ï¼ã`QualityTable` ç `itemsPath` ä¸å¾å¡« `inspectionItems`ããè¿æ¡è§åçæ¬ææ¯ãæ£éªè¡¨çæ°æ®æºè·¯å¾ç»ä¸ãï¼ä½åé¢åæäºä¸ä¸ª**å ·ä½ç»ä»¶å**ï¼ äºæ¯æ´ä»½æç¤ºè¯éå¯ä¸è¢«è§åæ£é¢ç¹åçæ£éªè¡¨å°±æ¯å¹³éºè¡¨ââçäºå ¨ç¨ç»å®æå¹¿åã**å«çæå¤§ã** 2. **è¾åºæ ¼å¼ç¤ºä¾é第äºä¸ªç»ä»¶å°±æ¯ `{"type": "QualityTable"}`**ãç¤ºä¾æ¯æ¨¡åæå®¹æç §æçä¸è¥¿ã 3. 第 8 æ¡åªè¯´äºã`âåä¸` æ¯åå¹¶æ è®°ãï¼**没说è¿ä¸ªæ è®°æå³çä»ä¹ç»æ**ï¼æ¨¡å读å°çæ¯ãä¸ä¸ªè¦å¿½ç¥ç符å·ãï¼ è䏿¯ãè¿éåå¨ä¸¤å±ç»æãã ### 24.4 æ¹å¨ **å端æç¤ºè¯ï¼`QcReportTemplatePromptBuilder`ï¼ä¸å¤** | ä½ç½® | æ¹æ³ | |---|---| | `EXTRACTION_RULES` 第 1 æ¡ | `QualityTable ç itemsPath` â **ã塿¯å¸¦æ£éªé¡¹çè¡¨æ ¼ç»ä»¶ã**ï¼è§åä¸åç¹åä»»ä½ä¸ä¸ªå ·ä½ç»ä»¶ | | `EXTRACTION_RULES` å第 8 æ¡ææ 8 / 9 | 第 8 æ¡åªè®²æ è®°å«ä¹ï¼**æ°å¢ç¬¬ 9 æ¡**ç»åºæ¨è®ºï¼ãåä¸åéè¿ç»åºç° `âåä¸`ï¼è¿å¼ 表就æ¯ä¸¤å±ç»æâ¦â¦å¿ 须彿äºå®åä¸éåãï¼å¹¶ç¹æä¸¢æå项çäºæ¥å失ç | | `OUTPUT_FORMAT` ç»å°¾ | è¡¥ä¸å¥ãä¸é¢ç¤ºä¾éç type **åªæ¯æ ¼å¼ç¤ºæ**ï¼éåä¸å¾ä»¥ãå¯ç¨ç»ä»¶æ¸ åãéåç»ä»¶ hint 为åï¼ä¸è¦å 为示ä¾éåäºæä¸ª type å°±ç §æå®ã | **åç«¯ç§¯æ¨æ¸ åï¼`components/quality/inspection/quality-table.ts` ç `aiHint`ï¼ç¨æ·å·²æ¹åï¼** åæãâ¦â¦**æ¥åæ£ææä¸»è¦çç»ä»¶ã**ãæ¯ä¸å¥ä¼ä¸»å¨è¯¯å¯¼æ¨¡åçè¯ï¼å®æ¯ `QualityTable` èªå·±çèªè¿°ï¼ 模å读å°åªä¼æ´å¾åäºéå®ãæ¹ææ¡ä»¶å¼ï¼ ``` å¤è¡æ£éªæ°æ®çè¡¨æ ¼ï¼æ£éªé¡¹å¹³éºä¸å±ã没æç¶åå ³ç³»ï¼åºå·/æ£éªé¡¹ç®/æ åå¼/宿µå¼/åä½/å¤å®ï¼ã ä¸ä¸ªæ£éªé¡¹ç®ä¸æè¥å¹²å项ï¼åä»¶é表ç°ä¸ºè¯¥å纵ååå¹¶ãåç»è¡åçãâåä¸ãï¼æ¶ä¸è¦ç¨æï¼ æ¹ç¨åç»æ£éªé¡¹è¡¨ã æ¥åæ«å°¾çè½æ¬¾ç¾ç½²è¡ï¼æ£éªå/å®¡æ ¸äºº/æ¥æï¼ä¸è¦æ¾è¿æè¿éï¼é£å±äº ReportFooterã ``` æ¹æ³æ¯**æå¤å«æ¡ä»¶åè¿ hint æ¬èº«**ï¼`âåä¸` = æå项ï¼ï¼è䏿¯åªçä¸å¥ãæä¸éå夿æ åµãââ æ¨¡åæä¸æ²¡æå«ç线索è½å¤æã夿ãå¨åªã ### 24.5 éªè¯ï¼ç»è¿è¿è¡ä¸çå端ï¼ç´æ¥ç¨æ°æç¤ºè¯æç模å å端éå¯è¦ç¨æ·æä½ï¼ä¸ºäº**å¨éå¯ä¹åå°±è½åçãæ°æç¤ºè¯å°åºææ²¡æç¨ã**ï¼ ç¨åç¼è¯åºçç±»ææç¤ºè¯è½çï¼åç±èæ¬ç´æ¥æ DashScopeï¼`qwen-max`ï¼`temperature=0.7` ä¸åºä¸é ç½®ä¸è´ï¼ï¼ - `PromptDumpProbe.java`ï¼è¯» `bs-catalog.json`ï¼13 é¡¹æ¸ åï¼â è° `buildSystemPrompt` / `buildUserMessage`ï¼ç©º hintï¼â å UTF-8 æä»¶ã - `bs-direct-run.mjs`ï¼è¯»æç¤ºè¯ + `bs-extract-new.txt`ï¼**æ¹åæ½åå±å¯¹ç¨æ·é£ä»½ docx ççå®è¾åº**ï¼ï¼ POST `https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions`ï¼è· 5 è½®ç»è®¡å½ä¸ã | ç»å | å½ä¸ `GroupedQualityTable` | |---|---| | æ°æç¤ºè¯ + æ°æ¸ å hint | **5 / 5** | | æ°æç¤ºè¯ + **æ§**æ¸ å hintï¼æ¶èå¯¹ç §ï¼ | **5 / 5** | **ç»è®º**ï¼æ¬è½®èµ·å³å®ä½ç¨çæ¯**å端æç¤ºè¯é£ä¸å¤**ï¼å端 hint çä¿®æ¹å¨è¿ä»½è¾å ¥ä¸æ²¡ææ¹åç»æï¼ ä½å®ä¿®æçæ¯ä¸å¥**ç¡®å®é误çèªè¿°**ï¼ãæ¥åæ£ææä¸»è¦çç»ä»¶ãï¼ï¼ä¿çã ### 24.6 éªè¯ç¶æï¼å¡å¿ 忏 ï¼ - **å·²éªè¯**ï¼æ°æç¤ºè¯ + æ°æ¸ åï¼å¯¹ç¨æ·è¿ä»½ç宿件ãç宿¨¡åã空 hintï¼**5/5 稳å®å½ä¸åç»è¡¨**ã è¿æ¡é¾è·¯æ¯ãæç¤ºè¯ â æ¨¡ååå¤ãï¼ä¸é´æ²¡ç»è¿å端çè§£æ/å½ä¸åã - **æªéªè¯**ï¼æ´æ¡ `/qc-report/ai-import/draft` HTTP é¾è·¯ï¼éè¦ç¨æ·**åéå¯ä¸æ¬¡å端**ï¼ã è§£æå¨ä¸å½ä¸å卿¬è½®ä¸åæªæ¹ï¼ä¸æ½åå±ä¿®å¤åå·²è½äº§åºè¿ `GroupedQualityTable`ï¼å³ 2/5 é£ä¸¤æ¬¡ï¼ï¼ æä»¥æ´é¾è·¯çé£é©å¾ä½ââ使²¡è·è¿å°±ä¸åæãå·²éè¿ãã - **è¿ä¸è½®çæè®ï¼ä¸ 23.2 åæºï¼ç¬¬äºæ¬¡è¸©ï¼**ï¼å¤æã模åè½ä¸è½åå¯¹ãæ¶ï¼**è¾å ¥éæ¯ä¸å¤ å¯è½è¯±å¯¼å®çæªè¾é½è¦ä¸èµ·æ¥**ãä¸ä¸è½®æ¯èªå·±ç hint 诱导ï¼è¿ä¸è½®æ¯æç¤ºè¯éç¹åçç»ä»¶åä¸ç¤ºä¾ã ãè¾å ¥ä¿¡æ¯å¤äºãä¸çäºã模åä¼é对ãââä¸é´çæªè¾æ¬èº«å°±æ¯åéã ## äºåäºãéå修好ä¹åï¼å©ä¸çå·®è·å ¨å¨ã表达åãï¼2026-09-19 ç»ï¼ ### 25.1 å 确认éåç¡®å®ä¿®å¥½äº ç¨æ·éå¯åä»åãè¿æ¯è¿æ ·ããæ¿**表头**å½å¤æ®å³å¯åºå两个ç»ä»¶ï¼ | | 表头 | |---|---| | `QualityTable`ï¼å¹³éºï¼ | æ£éªé¡¹ç® / **æ åå¼** / 宿µå¼ / å¤å® | | `GroupedQualityTable`ï¼åç»ï¼ | æ£éªé¡¹ç® / **å项** / **æ£æµè¦æ±** / 宿µå¼ / å¤å® | ç¨æ·æªå¾éåºç°äºãå项ããæ£æµè¦æ±ãââ**å¹³éºè¡¨æ ¹æ¬æ²¡æè¿ä¸¤å** â ç»ä»¶å·²ç»æ¢è¿æ¥äºã ç¨ç©º hint å¤è· blob 84 èµ°å®æ´ HTTP é¾è·¯ä¹ç¡®è®¤è¿å `GroupedQualityTable`ã é¡ºå¸¦æ¾æ¸ ä¸ä¸ªççå bug çä¸è¥¿ï¼æªå¾éåå æ ¼ä¸ç `Table cell pec}}` **䏿¯ææ¡£å 容**ã `Table cell` æ¯ GrapesJS èªå·±çç»ä»¶ç±»ååï¼`node_modules/grapesjs/locale/en.js:43` ç `cell: 'Table cell'`ï¼ï¼ æ¯ç¼è¾å¨ç»å¨åå æ ¼å·¦ä¸è§çæ¬æµ®æ ç¾ï¼çä½äº `{{report.s` å 10 个å符ï¼å©ä¸ç `pec}}` æé²åºæ¥ã ### 25.2 å©ä¸çå·®è·å ¨é¨æ¯è¡¨è¾¾åï¼ä¸æ¯è¯å« é项æ¯å¯¹åä»¶å确认ï¼`GroupedQualityTable` ç 7 åã`SampleInfo` ç 6 ä¸ªåæ®µ**é½åæ»å¨ä»£ç 常éé**ï¼ åä»¶éçãæ£æµæ¹æ³ / ç»è®ºã两åä¸ãæ£éªç»æ | ç»è®ºã两级表头ã以ååä»¶ç 7 ä¸ªæ ·ååæ®µ ï¼äº§åæ°é / çäº§æ¥æ / åè±åç§ / æææ¥æçï¼**没æä»»ä½ç»ä»¶è£ å¾ä¸**ã ### 25.3 䏿¡å¿ é¡»å ä¿®ãå¦åæ°åè½å½¢åèè®¾çæ°æ®ä¾§ç¼ºå£ `MesQcReportItemRespDTO` **å·²ç»æ `checkMethod`ï¼æ£æµæ¹æ³ï¼**ï¼ä½ qcreport 弿ç `InspectionItem` æå®ä¸¢äºï¼`ReportContext.itemMap()` ä¸åçä½ç¨åéæ²¡æå®ãâ åå®ä¹éå°±ç®å `æ£æµæ¹æ³={{item.checkMethod}}`ï¼æ¸²ææä¹åªä¼çä¸ç空ç½å 䏿¡ãç»å®åä¸å°å¼ãã è¡¥è¿æ¡é¾è·¯è¦å¨ 6 å¤ï¼mapper / DO / itemMap / Codec / å端 context.ts / fixtureï¼ï¼è§è®¾è®¡ææ¡£ §4ã ### 25.4 æ¹æ¡ææ¡£å·²åºï¼**å¾ ç¨æ·ç¡®è®¤å卿** `docs/qc_report_configurable_columns_design.md`ãæ ¸å¿çµè«ï¼ - **䏿°å¢ç»ä»¶**ï¼ç» `GroupedQualityTable` å å¯é `columns` / `headerSpans`ãç» `SampleInfo` å å¯é `fields`ï¼ ç¼ºå¸æ¶åè½ç°æå¸¸é â å鿍¡æ¿é¶å½±åãæ æ°æ®è¿ç§»ã - åå®ä¹ç¼ç æ**åè¡å符串**ï¼å±æ§åè®®åªè®¤æ éï¼`QualityProps` ä¸ `attributes` 齿¯å符串ï¼ã - **ä¸éè¦æ¹æ¸²æå¨**ï¼å·²æ ¸æ¥ `HtmlRenderer` æ 任使ç»ä»¶ç±»å忝çé»è¾ã`CanvasSafety` æ¯é»ååèéç½åå â `colspan`/`rowspan` æ¬æ¥å°±æ¾è¡ï¼schema çæ¬å·ä¹ä¸ç¨å¨ã - æå¤§çé£é©å·²åè¿ææ¡£ï¼åå 第ä¸ä¸ªæ£éªè¡¨ä¼è®©éååå°ä¸ç¨³ï¼Â§äºåå çæºçï¼ï¼æä»¥**ä¸å **ã **å·²å / æªå**ï¼ææ¡£å·²è½çï¼`docs/` 被 gitignoreï¼éè¦ `git add -f` æè¿çæ¬åºï¼ï¼ **代ç ä¸è¡æªå¨**ï¼çç¨æ·å¯¹ææ¡£ §11 çåä¸ªå¾ è£å³ç¹è¡¨æã --- ## äºåå ã表达åè¡¥é½è½å°ï¼åå®ä¹ / 表头跨å / æ ·ååæ®µï¼2026-09-19 ç»ï¼ ç¨æ·å¯¹ §äºåäº çè®¾è®¡ææ¡£ç»åºãæç §ä½ çæ³æ³æ¥å°±è¡äºãï¼å³ææç夿è½å° §11 çåä¸ªå¾ è£å³ç¹ã ### 26.1 åä¸ªå¾ è£å³ç¹çè£å³ | å²è·¯ | è£å³ | 便® | |---|---|---| | 两级表头 `headerSpans` è¿è½®åè¿æ¯ä¸è½®å | **è¿è½®å** | åä»¶ãæ£éªç»æ \| ç»è®ºãç¡®å®æ¯ä¸¤å±ï¼å åååé½è¦å¨å䏿¹æä»¶ï¼ææä¸¤è½®åªæ¯å¤ä¸æ¬¡ç¼è¯ä¸èè° | | åä»¶çãç»è®ºãåæ å°å°åªä¸ªç»å® | 弿ç `resultText` | `resultText` å°±æ¯é项çå¤å®ææ¬ï¼`åæ ¼`/`ä¸åæ ¼`ï¼ï¼ä¸åä»¶ãç»è®ºãåçå 容ä¸è´ï¼`actualValue` æ¯å®æµå¼ãåä»¶é£ä¸åæ¯ç©ºç | | å ä¸ä¸ª `tool`ï¼å·¥å ·/æ²»å ·ï¼å段 | **ä¸å ** | æ åºæ¯ãæ è¯»åæ¹ï¼å äºå°±æ¯ç¬¬äºä¸ª `unit_text`ï¼è§ §22 çæ»åæè®ï¼ | | `SampleInfo` é»è®¤å段æ¯å¦æ©å° 7 项 | **ä¿æ 6 项** | æ©é»è®¤å¼ä¼æ¹å鿍¡æ¿ç渲æäº§ç©ï¼è¿èãåéé¶å½±åãï¼ï¼è `fields` è¦çå·²ç»è½è¡¨è¾¾ 7 项 | | å¹³éº `QualityTable` æ¯å¦ä¹å `columns` | **ä¸å ** | åå ä¸å¤ãåå¯ä»¥èªå®ãä¼è®©éåéæ°åå°ä¸ç¨³ï¼Â§äºåå çæºçï¼ï¼æ¶çåªæ¯å¹³éºè¡¨ä¹è½å¤å | ### 26.2 è½å°å 容 **å端ï¼`mom-pro2-before/src/components/quality/`ï¼** - `inspection/grouped-quality-table.ts`ï¼æ°å¢ `columns` / `headerSpans` ä¸¤ä¸ªææ¬å±æ§ï¼**æ æä¸ç» `defaultValue`ãä¸ç» `bindable`**ï¼ï¼`dataSchema` è¡¥ `item.checkMethod`ï¼`aiHint` è¡¥ã忝é»è®¤å¤æ¶å columnsã两å±è¡¨å¤´å headerSpansï¼ä¸è¦æ¢ç»ä»¶ãã - `header/sample-info.ts`ï¼æ°å¢ `fields` ææ¬å±æ§ã - `core/validator.ts`ï¼**æ¥ä¸ `definition.validate`**ï¼è¯¦è§ 26.3ï¼ã - `engine/context.ts`ï¼`ReportContextItem` è¡¥ `checkMethod`ï¼æ£æµæ¹æ³éè¡è¿ä¸ä¸æï¼ä¸ `requirement` å¹¶åä¸åå¹¶ï¼ã **å端** - `MesQcReportContextMapper` â `checkMethod` è´¯éå°æ¥åä¸ä¸æï¼`InspectionItem` ä¾§æ©å·²æè¯¥å段ï¼ã - `QcReportTemplatePromptBuilder`ï¼æ°å¢æ½åè§å 10ï¼`columns` + ç»å®è·¯å¾ç½ååï¼ã11ï¼`headerSpans`ï¼ã12ï¼`fields`ï¼ã**ãæ¥åä¸ä¸æçåæ®µãç½åå**ï¼è¯¦è§ 26.4ï¼ã ### 26.3 顺带修æä¸å¤æ»ä»£ç ï¼`definition.validate` 仿¥æ²¡è¢«è°ç¨ è®¾è®¡ææ¡£è¦æ±ãéæ³åå®ä¹è¦å¨ä¿åæ¶æ¥éãï¼ä½è½å°ä»£ç æ¶åç°ï¼**`definition.validate` 卿´åç«¯ä¸æ¬¡é½æ²¡è¢«è°ç¨è¿**ã åªæ `validateQualityProps` 被è°ç¨ï¼è£ é å¨ `assembler.ts:82`ï¼ï¼èå®ä¸ç¢° `definition.validate`ã ä¹å°±æ¯è¯´è¿ä¸ªé©ååäºä¹ä¸ä¼æ¦ä½ä»»ä½äººââä¸¤ä¸ªæ¢æç `validate` å®ç°ä¸ç´æå¨ç©ºæ¡£ä¸ã å·²æ `definition.validate?.(resolved)` æ¥è¿ `validateQualityProps` æ«å°¾ï¼æ°å¢ç䏿¡æ ¡éªä¸æ¢æç两æ¡åæ¶çæã > **仿ªè¦ç**ï¼è®¾è®¡å¨é**æå·¥ç¼è¾åç¹ä¿å**è¿æ¡è·¯ç®åä¸è· `validateQualityProps`ï¼åªæ AI å¯¼å ¥è£ é 飿¡è·¯è·ï¼ã > æå·¥å¡«éçåå®ä¹ä¸ä¼å¨ä¿åæ¶è¢«æ¦ä½ï¼åªä¼å¨åºä»¶æ¶ä»¥ãç»å®åä¸å°å¼ãæ´é²ã屿¢æç¼ºå£ï¼æ¬è½®æªæ¹ï¼å·²åè¿èè°ææ¡£ã ### 26.4 ç´è¿ç模åä¸è½®å®æµï¼ä¿®æä¸ä¸ªçå®ç¼ºé· åæ³ï¼ææ°æç¤ºè¯ä»æ°ç¼è¯ç class é dump åºæ¥ï¼**ä¸èµ°å端**ç´æ¥æ DashScope ï¼`qwen-max`ï¼ï¼åçå®çç¾äº docx æ½åç»æï¼`bs-extract-new.txt`ï¼ï¼**hint ç空**ï¼æ¯è½®è· 5 次çç¨³å®æ§ã | 轮次 | åç°çé®é¢ | ä¿®æ³ | ä¿®å | |---|---|---|---| | 第 1 è½® | 模åç»åç»è¡¨å `columns` æ¶**æãåé¡¹ãæ´å丢äº**ï¼3/3 è½®åªå 4 åï¼ãèåç»è¡¨ç**é»è®¤åæ¬æ¥å°±æå项** â è¦çåèæ¯ä¸è¦çæ´å·® | è§å 10 è¡¥ä¸æ¡ç¡¬è¦æ±ï¼åç»è¡¨å `columns` å¿ é¡»ä¿ç `å项={{item.group.childName}}`ï¼å¹¶è¯´æ¸ æ¼æåå项忴忶失 | 5/5 ä¿çï¼ä¸ä¸åä»¶ 6 å**éå对é½** | | 第 2 è½® | `headerSpans` æãç»è®ºãåçè·¨åæ°å¹¶è¿äºãæ£éªç»æãï¼åæ `æ£éªç»æ^6`ï¼ï¼è¡¨å¤´ä¼æç»è®ºåç»å°æ£éªç»æåºä¸ï¼å¦æä¸è½®å计 8 â åæ° 7 | è§å 11 è¡¥ä¸æ¡ï¼ç»è®º/å¤å®åå¨åä»¶éä¸å±äºä¸å±åç»æ 颿¶**è¦åç¬ææ®µ** | 5/5 è¾åº `æ£éªç»æ^5|ç»è®º^1` | | 第 3 è½® | `SampleInfo.fields` é**5/5 è½®é½ç¼é äºæ¥ååæ®µ**ï¼`{{report.productionDate}}` / `{{report.expiryDate}}` / `{{report.potatoVariety}}`ï¼ | æ ¹å æ¯è§å 7 åªè¯´ãä¸è®¸ç¼é ãå´**ä¸è¯´æåªäºé®**ãæ°å¢ãæ¥åä¸ä¸æçåæ®µãåï¼æ 23 个å¯ç»å®é®å ¨éååº | 5/5 å ¨é¨åæ³ï¼ä¸åå¨çæ ç®æ¹ä¸ºåè¿ `summary` 说æ | **ä¸ä¸ªç¼ºé·çå ±åç¹**ï¼é½ä¸æ¯æ¨¡åãä¸å¬è¯ãï¼èæ¯æç¤ºè¯**åªç»äºç¦ä»¤ã没ç»å¯é项**ã åçç½ååï¼è§å 10ï¼æ¬æ¥å°±ç»äºï¼æä»¥ `columns` çç»å®è·¯å¾ 5/5 åæ³ââåè¯äºãæå¼å¯ééåãææ¯ææåæ³ã ### 26.5 为ä»ä¹è¦ä» `ReportFields` åå°çæç½ååï¼è䏿¯æåå¸¸é æåçæ¸ åä¼å¨æäººç»æ¥åå åæ®µæ¶**éé»è¿æ**ï¼èæ¨¡åæ£æ¯é è¿ä»½æ¸ åæç¥é `{{report.xxx}}` è½åä»ä¹ï¼ æ¸ åéå°ä¸ä¸ªé®ï¼ç¨æ·å°±ä¼å¨æ¥åä¸çå°ä¸ç空ç½å 䏿¡åè¦ï¼ä¸**çä¸åºæ¯æç¤ºè¯è¿æé æç**ã æä»¥ç½ååç± `ReportFields` çåæ®µååå°çæï¼å模åï¼æ 跨模åè¦åï¼ï¼åæµ**éä¸ªåæ®µæ ¸å¯¹**è䏿¯æ½æ¥ä¸¤ä¸ªã ### 26.6 éªè¯ç»è®º | éªè¯é¡¹ | ç»æ | |---|---| | å鿍¡æ¿åèä¸è´ | **12 ç»å±æ§ç»åå ¨é¨éåèç¸å**ï¼sha256 ä¸è´ï¼ï¼å¹¶å»ç»ææ¢ééç两个æçº¹å¸¸é | | å端æ¢é `verify-columns.ts` | **83 æ¡æè¨å ¨ç»¿**ï¼åè¦ççè½å°ä½ç½®ã两级表头ç colspanãéªè¯ææ¡ãæ ·ååæ®µãXSS 转ä¹ãè§£æå¨è¾¹çãaiHint 䏿´»æ¸ åå¥çº¦ï¼ | | åç«¯åæµ | `yudao-module-qcreport` **147/147 å ¨ç»¿**ï¼æ°å¢ 4 æ¡ï¼2 æ¡æç¤ºè¯è§åæè¨ + 1 æ¡ç½ååå ¨éæ ¸å¯¹ + 1 æ¡ checkMethod æ å°ï¼ | | åç«¯ç±»åæ£æ¥ | `components/quality` ä¸ **0 个类åé误**ï¼æ¢æ 36 ä¸ªå¨ `src/views/wls|im|erp`ï¼ä¸æ¬è½®æ å ³ï¼ | | ç´è¿ç模å | éå **5/5**ãåä¸åä»¶ç 6 å**éåå¯¹é½ 5/5**ã`headerSpans` æ£ç¡® **5/5**ãæ ·ååæ®µé®åæ³ **5/5** | **å·²éªè¯ / æªéªè¯åå¼å£°æ**ï¼ - â å·²éªè¯ï¼ä¸è¿°äºé¡¹ï¼ä»¥åãæç¤ºè¯ + å端ç»ä»¶ãè¿æ¡é¾è·¯ç**ç´è¿**è¡ä¸ºã - ð¡ æªéªè¯ï¼**HTTP å ¨é¾è·¯ä¸ UI éªæ¶å°æªè·**ââæ¹çæ¯æç¤ºè¯ï¼å端ï¼ä¸å端ç»ä»¶ï¼å端é**éå¯**æçæã éå¯åéå¨çå® AI å¯¼å ¥å¼¹çªéç¡®è®¤ï¼æ£éªè¡¨åºç°ãæ£æµæ¹æ³ / æ åè¦æ± / ç»è®ºãåä¸ä¸¤çº§è¡¨å¤´ãæ ·åä¿¡æ¯åæåä»¶çåæ®µå±ç¤ºã `problems` é**没æ**ãç»å®åä¸å°å¼ãåè¦ã - â å·²ç¥è¾¹çï¼æå·¥ä¿åè·¯å¾ä¸è·æ ¡éªï¼26.3ï¼ï¼è¡¨æ ¼çãåä½ãåå¨åä»¶éæ²¡æå¯¹åºåæ¶ AI å¶å°ä¼å¤å ä¸å空åï¼æ 害ï¼ç¨æ·å¨è®¾è®¡å¨éå æå³å¯ï¼ã ### 26.7 æªå - å¹³éº `QualityTable` ç `columns`ï¼`unit_text` 䏿¥çé¢ï¼Excelãæ¡ç®ç±»åãåï¼åè§ 26.1 ä¸ Â§22ï¼ - è®¾è®¡å¨æå·¥ä¿åè·¯å¾çæ ¡éªé¸å£ï¼æ¢æç¼ºå£ï¼éåç¬å³çæ¯å¦è¦åæå ¨æ¨¡æ¿æ ¡éªï¼ - æªå¨ `HtmlRenderer` / `BASE_CSS` / `CanvasSafety`ï¼æ¬è½®æ ¸æ¥å确认æ éæ¹ï¼æ¸²æå¨æ æç»ä»¶ç±»å忝ãç½å忬就æ¾è¡ `colspan`/`rowspan`ï¼ - æªå¨ `docs/sql/config_export_all_*.sql`ï¼æ è¡¨ç»æåæ´ï¼ ## äºåä¸ãç»å¸æçä¸è¡¨å¤´ãå¤äºä¸ä¸ªç»è®ºãï¼ä¸¤å¤ç¬ç«ç¼ºé·ï¼2026-09-19 ç»ï¼ ç¨æ·å¨çå® UI éè·å® AI å¯¼å ¥ååé¦ï¼**ãå¤äºä¸ä¸ªç»è®ºï¼ä¼åä¸ä¸æçã**ã è¿ä¸æ¯ä¸æ¡é®é¢ï¼æ¯ä¸¤æ¡äºä¸ç¸å¹²ç缺é·å å¨åä¸å¼ 表ä¸ã ### 27.1 ç°è±¡ä¸åè¯ | # | ç°è±¡ | åè¯ | |---|---|---| | A | 表头å³ç«¯ãç»è®ºãäºå**ä¸ä¸åå°ä¸æ¬¡** | æ¨¡æ¿ 33 ç AI è稿é `columns` æ«ååçæ¯ `ç»è®º={{item.resultText}}`ï¼`headerSpans` æ«æ®µåçæ¯ `ç»è®º^1` ââ 两å±ååºä¸æ ¼ãç»è®ºã | | B | 表ä½**æ´ä½å·¦ç§»ä¸æ ¼**ï¼æ«ååªå©è¡¨å¤´ãè¢«æ¤æç«æç»æ¡ | å¤å» `GroupedQualityTable.buildContent` ç产ç©åç¬æ¸²æï¼ç»åæ ¼ `display:none`ã宽 0ï¼æ«å宽 64pxï¼å 5 æ ¼æ´ä½å·¦ç§»ãåçéä½ä¸ç¨æ·æªå¾éæ ¼ä¸è´ | B çæå ï¼ç»åæ ¼åä½ `hidden="{{item.group.hidden}}"`ã**ç»å¸ä¸è·ç»å®**ï¼æµè§å¨çå°ãæ `hidden` 屿§ãå°±æç« â è¯¥æ ¼ `display:none`ï¼è¿ä¸è¡åé¢çæ ¼æ´ä½å·¦ç§»ãè¿æ¯ä¸ªä¸ç´åå¨çèè¡ä¸ºï¼åªæ¯ä»å AI 仿²¡äº§åºè¿åç»è¡¨ï¼æä»¥æ²¡æ´é²ã A çæå æ¯è¯æ³ç¼ºå£ï¼åä»¶çãç»è®ºãå¨å件鿝**䏿 ¼çºµååå¹¶ãèªå·±å 满ä¸ä¸ä¸¤å±è¡¨å¤´**ï¼æåææ¬é表ç°ä¸ºä¸å±è¡¨å¤´é£ä¸æ ¼åçãâåä¸ãï¼ãä½ `headerSpans` çè¯æ³**ä¸å 许空æ é¢**ï¼`parseHeaderSpans` 对空æ é¢ç´æ¥æ¥éï¼ï¼è¡¨è¾¾ä¸åºã䏿 ¼ç空ãï¼äºæ¯æ¨¡ååªè½æååååä¸é ââ å°±éå¤äºã ### 27.2 ä¿®æ³ | # | æ¹å¨ | ä½ç½® | |---|---|---| | A | æ°å¢å¤æ®ï¼ææ®µè·¨ 1 åãä¸**该段æ é¢ä¸å®æ£ä¸æ¹é£ä¸åçåæ é¢ä¸åä¸å·®** â è®¤å®æ¯çºµååå¹¶æ ¼ï¼åº `rowspan="2"`ï¼ä¸å±è¡¨å¤´**è·³è¿è¯¥å** | `components/quality/inspection/grouped-quality-table.ts` | | B | åªå¨**ç»å¸ææ¡£å **æ³¨å ¥ä¸æ¡å¼å¯¼æ ·å¼ `td[hidden*="{{"] { display: table-cell; }`ï¼æãå¼è¿åå¨å ä½ç¬¦ãç `hidden` è¿åæå¯è§æ ¼ | `views/mes/qc/report/template/designer/use-designer.ts` | | â | æç¤ºè¯è§å 11 补两æ¡ï¼è¯¥æ®µæ é¢å¿ é¡»ä¸åæ é¢**ä¸åä¸å·®**ï¼**ä¸è¦èªä½ä¸»å¼ æè¯¥åæ¹åæè¿ä¹è¯**ï¼å¹¶åæåæï¼ | `QcReportTemplatePromptBuilder` è§å 11 | | â | åç»è¡¨ç `aiHint` ä¸ `headerSpans` ç屿§è¯´æåæ¥è¡¥ä¸è¿æ¡å½¢æ | åä¸ç»ä»¶ | **为ä»ä¹å¤æ®ç¨ãæ é¢ä¸åä¸å·®ãè䏿¯ãæå¨æåä¸åã**ï¼æå¨æååªæ¯å·§åï¼å¤æ®æ¬èº«å¾è¯´æåä»¶éé£ä¸¤æ ¼æ¯åä¸ä¸ªæ ¼ã宿µä¹è¯æè¿ä¸ªå¤æ®å¯è¢«æ¨¡åç¨³å®æ»¡è¶³ï¼è§ 27.4ï¼ã B çæ ·å¼**åªå å¨ç»å¸ç iframe é**ï¼ä¸åè¿ Schemaãä¸é模æ¿ä¿åï¼åºä»¶æ¶è¯¥å¼å·²è§£æç©ºä¸²æ `hidden`ï¼éæ©å¨ä¸åå½ä¸ï¼**æ¥å渲æä¸ä¸ªåé½ä¸åå½±å**ã æ ·å¼æ¶çªå° `td`ï¼è¯¥å±æ§ç®ååªç¨å¨åå æ ¼ä¸ï¼åªå¤©çç¨å°å«çæ ç¾ä¹è¦æé£ä¸ªæ ç¾ç `display` åå䏿¡ï¼ä¸è½ä¸æ¡éç¨è§åç³è¿å»ã ### 27.3 æç¤ºè¯ä¸ºä»ä¹è¦åå°ãä¸è¦æ¹åãè¿ä¹ç» è¡¥ä¸ã两边ä¸åä¸å·®ãåå¤è·ï¼ä»æ **1/5 è½®**åºé®é¢ï¼æ¨¡åæ `columns` éé£ä¸åæ¹åæãå¤å®ãï¼`headerSpans` å´è¿åçåä»¶ä¸çãç»è®ºãï¼ä¸¤è¾¹å¯¹ä¸ä¸ï¼åéåå»å¤å°ä¸ä¸ªè¡¨å¤´ã æ ¹å æ¯å䏿¡ä¿¡æ¯å¨æç¤ºè¯éåºç°ä¸¤æ¬¡ï¼å件表头ã`columns`ï¼æ¶æ¨¡åä¼åååçãè¡¥ä¸ãä¸è¦èªä½ä¸»å¼ æå®æ¹åæè¿ä¹è¯ãå¹¶åæåæåï¼å¤è· **5/5 è½®åæä¸ºä¸æ ¼**ã ### 27.4 éªè¯ç»è®º | éªè¯é¡¹ | ç»æ | |---|---| | å端æ¢é `verify-columns.ts` | å ¨ç»¿ï¼æ°å¢ 7 æ¡çºµååå¹¶æè¨ + 1 æ¡ hint æè¨ï¼ï¼åæå `rowspan="2"` å¨åºãä¸ååº `colspan="1"`ããç»è®ºãå ¨æ**åªå°ä¸æ¬¡**ãä¸ 2 æ ¼ + ä¸ 5 æ ¼ = 7ï¼**åä¾**ï¼ä¸æ ¼ä¸ååä¸ååæ¶ä¸åº `rowspan`ï¼å¤æ®æ¯ãä¸åä¸å·®ãè䏿¯ã卿åãï¼ | | åç«¯åæµ | `yudao-module-qcreport` **147/147 å ¨ç»¿**ï¼æ°å¢ 2 æ¡æç¤ºè¯æè¨ï¼ | | åç«¯ç±»åæ£æ¥ | æ¬è½®ç¸å ³æä»¶ **0 个类åé误**ï¼æ¢æ 36 个ä»å¨ `src/views/wls|im|erp`ï¼ä¸æ¬è½®æ å ³ï¼ | | 渲æå¤å»å¯¹ç § | ä¿®åï¼ç»è®ºå°ä¸¤æ¬¡ + æ´è¡å·¦ç§» + æ«å 64px ç«æï¼ä¿®åï¼`ç»è®º` 䏿 ¼è·¨ä¸¤è¡ã表ä½éæ ¼å¯¹é½ | | ç´è¿ç模å 5 è½® | éå **5/5** åç»è¡¨ï¼`columns` 6~7 åãè·¨åæ°å计=åæ° **5/5**ï¼æ«æ®µæ é¢ä¸æ«åååä¸è´ **5/5** â **纵ååæ 5/5**ï¼æ ·ååæ®µé®åæ³ **5/5** | **å·²éªè¯ / æªéªè¯åå¼å£°æ**ï¼ - â å·²éªè¯ï¼æ¢éãåç«¯åæµãç±»åæ£æ¥ã渲æå¤å»å¯¹ç §ãç´è¿ 5 è½®ã - ð¡ æªéªè¯ï¼**HTTP å ¨é¾è·¯ä¸ UI éªæ¶æªè·** ââ æç¤ºè¯æ¹å¨å¨å端类éï¼é**éå¯å端**æçæï¼ç»ä»¶ä¸ `aiHint` æ¹å¨å¨å端ï¼çæ´æ°å³å¯ã éå¯åéå¨çå® AI å¯¼å ¥å¼¹çªé确认ï¼è¡¨å¤´ãç»è®ºãåªåºç°ä¸æ¬¡ä¸ä¸ºä¸æ ¼è·¨ä¸¤è¡ã表ä½ä¸è¡¨å¤´éæ ¼å¯¹é½ãæ«åä¸åè¢«æ¤æç»æ¡ã å¦å¤**å鿍¡æ¿ 33 çè稿ä¸ä¼èªå¨å好**ââå®å·²ç»æ `ç»è®º^1` åè¿ç»å¸äºï¼ééæ°å¯¼å ¥ææå·¥æ¹ï¼ä½çºµåå并夿®æ¯æ¸²ææçæçï¼åªè¦ `columns` æ«å䏿«æ®µæ é¢ä¸è´ï¼**å·²ä¿åçæ¨¡æ¿éæ°åºä»¶ä¹ä¼èªå¨åæä¸æ ¼**ã ### 27.5 æªå - æ²¡å¨ `parseHeaderSpans` çãåè®¡å¿ é¡»çäºåæ°ãè¿æ¡ç¡¬è§åï¼æ°å¢ç纵ååå¹¶æ ¼æ 1 åè®¡å ¥å计ï¼è§åä¸åï¼ - æ²¡å¨æ¥åä¾§åæ°ã`HtmlRenderer`ã`BASE_CSS`ã`CanvasSafety` - 没åã䏿 ¼çæ£ç rowspan 表头ç¼è¾ãââ设计å¨å±æ§é¢æ¿ä»æ¯æååç¬¦ä¸²ï¼æ¬è½®åªä¿è¯å®æ¸²ææ£ç¡® ## äºåå «ãèªå®åçå宽ï¼é¿å ä½ç¬¦æè¡¨æ ¼æåºçº¸å¼ ï¼2026-09-19 ç»ï¼ ### 28.1 ç°è±¡ä¸åè¯ ç¨æ·åé¦ï¼æ¿æ¥ç¬¬äºåä¸èï¼éå¯å端åå¤çåä¸å¼ 模æ¿ï¼ï¼ãç»è®ºå¥½åè¶ åºå»äºï¼åä¸ªåæ³ä¼åä¸ä¸ãï¼éè®¾è®¡å¨æªå¾ã æªå¾éç表ç°ä¸ 27 èä¿®å®çå½¢æä¸åï¼è¡¨å¤´ãç»è®ºã**åªåºç°ä¸æ¬¡**ï¼27 èç纵ååå¹¶å·²çæï¼ï¼ä½é£ä¸åè¢«æ¤æäºåå 个åç´ ã两个åç«çæï¼æ´åè½å°çº¸å¼ å¤é¢ã ææ¨¡åå®é 产åºç屿§ï¼`D:/qcl-tmp/bs-direct-out.json` 第 5 è½®ï¼ä¸æªå¾éåä¸è´ï¼ç¼è¯æ HTMLï¼å¨ Chromium éæ A4 æ£æå®½ 672px éï¼ | åºæ¯ | æ´è¡¨å®½ | æº¢åº | æ«å宽 | |---|---|---|---| | èªå® 6 åï¼å¼ä¸ºå ä½ç¬¦ | **810px** | **138px** | **29px** | | åä¸ï¼å¼æ¢æç宿°æ®ï¼æ°´å/20ç®ä¸/çç®±å¹²ç¥æ³/â¤14.0/13.2/åæ ¼ï¼ | 672px | 0 | 86px | | åç»è¡¨**é»è®¤** 7 åï¼å¼ä¸ºå ä½ç¬¦ | 807px | 135px | 116px | æ ¹å 两å±ï¼ 1. **åå®½ç±æå°å å®¹å®½åº¦åæ¨**ãèªå®åä¸ä¸ªå®½åº¦å£°æé½æ²¡æï¼é»è®¤åæï¼åºå· 48pxãæ£æµè¦æ± 30%ã宿µå¼ 26%â¦â¦ï¼ï¼æ´è¡¨å宽åªè½ç±ååçæå°å 容宽度å³å®ï¼è弿¯ `{{item.standardValue}}` è¿ç±»ä¸å«ç©ºæ ¼çé¿ä¸²ï¼æå°å å®¹å®½åº¦å°±æ¯æ´ä¸²é¿åº¦ï¼å åä¸å å°±è¶ è¿ 672pxã 2. **åºä»¶æ¶å¼æ¬æ¥å°±ç**ï¼æä»¥è¿ä¸»è¦æ¯è®¾è®¡å¨éçè§æé®é¢ââä½æç ´çº¸å¼ ç临çç¹å¨çå®é¿å¼ï¼å¦ãâ¤0.5 mg/kgï¼ä»¥å¹²åºè®¡ï¼ãï¼ä¸ä¸æ ·ä¼è¸©å°ï¼ä¸æ¯çº¯è®¾è®¡å¨é®é¢ã ### 28.2 ä¿®æ³ï¼`overflow-wrap:anywhere`ï¼åªæå¨èªå®åä¸ `overflow-wrap:anywhere` ä¼å䏿å°å 容宽度计ç®ï¼`break-word` ä¸ä¼ï¼ï¼åå®½å æ¤ä¸å被é¿ä¸²ç»æ¶ã å ³é®åèæ¯**åªç»èªå®åå **ï¼ | å å¨åª | 6 åèªå®è¡¨ | é»è®¤ 7 ååç»è¡¨ | |---|---|---| | ä¸å | 810pxãæº¢åº 138px | 807pxãæº¢åº 135px | | å | 672pxã䏿º¢åº | 672pxã**ä½ãæ£éªé¡¹ç®ããå项ã被æ¤å° 32/33pxãè¡é« 59â283px** | é»è®¤åç宽度声æå·²ç»å®ä½çé¢ï¼åå 䏿¡æè¡åèæ´å·®ï¼å®æµï¼ãæä»¥è¿æ¡åªæå¨èªå®åä¸ï¼**没æ `columns` çå鿍¡æ¿äº§ç©éåä¸å**ï¼æ¢é sha256 å®çï¼ç¬¬äºèç `GROUPED_TABLE_BASELINE` æªåï¼ã é¡ºå¸¦æ ¸å®ï¼å ¨åº `qc_report_template_version` é `data-qc-columns` / `data-qc-headerSpans` / `data-qc-fields` å½ä¸æ°**å为 0**ï¼è¿ä¸ä¸ªå±æ§æ¯æ¬è½®æè¿çï¼ï¼æä»¥è¿æ¡æ¹å¨å¯¹æ¢æå·²å叿¥åé¶å½±åã ### 28.3 为ä»ä¹ä¸æ¯ãå¨ç»å¸ä¸åè¡¥ä¸æ¡å¼å¯¼æ ·å¼ã 第äºåä¸è飿¡ç»å¸å¼å¯¼æ ·å¼å¤ççæ¯**ç»å®ä¸æ±å¼**ï¼`hidden` 被å½çï¼ââåºä»¶ä¾§åä¸ä¸ªå±æ§ä¼è§£æç©ºä¸²ãè¡ä¸ºä¸è´ï¼å±äºçº¯è®¾è®¡å¨åè±¡ï¼æä»¥æ¾ç»å¸ä¸æ¯å¹²åçã 宽度è¿ä»¶äºä¸ä¸æ ·ï¼åºä»¶ä¾§åæ ·ä¼è¸©ï¼çå®é¿å¼ï¼ï¼ä¸¤ä¾§éè¦**åä¸å¥è§å**æè½æè§å³æå¾ãè¥åªå¨ç»å¸ä¸å æè¡ï¼ç»å¸ææè¡åç宽度åå¸ãåºä»¶ææå°å 容宽度åå¸ï¼åèå¶é åºæ°çã设计å¨ä¸åºä»¶ä¸ä¸æ ·ããæä»¥è¿æ¬¡åè¿ç»ä»¶äº§ç©éã ### 28.4 éªè¯ | 项 | ç»æ | |---|---| | `.qc-conformance/verify-columns.ts` æ°å¢ 3c 段 | å ¨ç»¿ï¼èªå®å 6 æ ¼å ¨å¸¦ `overflow-wrap:anywhere`ï¼é»è®¤åä¸å¸¦ | | `verify-columns.ts` 第 1 èåèæçº¹ | `GROUPED_TABLE_BASELINE` / `SAMPLE_INFO_BASELINE` **æªå** â é»è®¤å产ç©éåä¸å | | Chromium 宿µï¼672px æ£æå®½ï¼ | èªå® 6 åï¼ä¿®å 810px/æº¢åº 138px/æ«å 29px â ä¿®å 672px/䏿º¢åº/æ«å 97px | | ç宿°æ®å¯¹ç § | ä¿®åä¿®åå 672pxï¼å宽åå¸ä¸å | | 渲æå¤å»æªå¾ | `ç»è®º` 䏿 ¼è·¨ä¸¤è¡ã表ä½éæ ¼å¯¹é½ãè¡¨æ ¼è½å¨çº¸å¼ å ï¼`D:/qcl-tmp/gqt-final.png`ï¼ | | è£ é å¾è¿ | `assembleQualityCanvas` 产ç©å `overflow-wrap` / `anywhere` åå¨ï¼`skipped` / `problems` å空 â æ ·å¼ä¸ä¼è¢« GrapesJS ä¸¢å¼ | | åç«¯ç±»åæ£æ¥ | æ¬è½®ç¸å ³æä»¶ **0 个类åé误**ï¼æ¢æ 316 个ä¸åå è½®åé级ï¼åå¨ `src/views/wls\|im\|erp` çæ å ³ç®å½ï¼ | **å·²éªè¯ / æªéªè¯åå¼å£°æ**ï¼ - â å·²éªè¯ï¼æ¢éãåèæçº¹ãChromium 宿µéå®½ãæ¸²æå¤å»æªå¾ãè£ é å¾è¿ãç±»åæ£æ¥ãGrapesJS èªèº«ç¡®å®æ¶å½äº `overflow-wrap`ï¼dist å å½ä¸ï¼ã - ð¡ æªéªè¯ï¼**设计å¨éççå®è§ææªå¤è·**ãæ¬è½®æ¹å¨å¨å端ï¼ç»ä»¶äº§ç©ï¼ï¼çæ´æ°å³å¯çæï¼æ ééå¯å端ï¼ä½æªå¾éé£ä»½ç»å¸æ¯**æªä¿åç AI å¯¼å ¥è稿**ï¼å·æ°åçå°çæ¯åºéçæ§ v1.0ï¼å¹³éº `QualityTable`ï¼æ²¡æèªå®ä¹åï¼ï¼éè¦**éæ°è·ä¸æ¬¡ AI å¯¼å ¥**æè½å¤ç°æ°å½¢æã - 妿³¨ï¼æ¨¡æ¿ 33 åºéç v1.0 è稿ï¼`qc_report_template_version.id=21`ï¼ä»æ¯æ§çå¹³éºè¡¨ï¼ä¸æ¬è½®æ å ³ã ### 28.5 æªå - 没å¨é»è®¤åç宽度声æï¼é£ä¼è®©å鿍¡æ¿çæçååï¼ - 没ç»èªå®åå å®½åº¦è¯æ³ï¼å¦éãè¿ä¸åçªä¸ç¹ãå¾åæ© `columns` çè¯æ³ï¼æ¯ç¬ç«å³çï¼ - æ²¡å¨ `HtmlRenderer` / `BASE_CSS` / `CanvasSafety` / æ¥åä¾§åæ° - 没管äºåä¸èéççãæå·¥ç¼è¾åä¿åä¸è·æ ¡éªãæ¢æç¼ºå£ ## äºåä¹ã确认ãåç»å项ç项ç®åä¼ä¸ä¼åå¹¶ãï¼2026-09-19 ç»ï¼ ç¨æ·é®ï¼å±äºåä¸ä¸ªé¡¹ç®çå项ï¼ã项ç®ãåä¼ä¸ä¼çºµååå¹¶ãç»è®º**ä¼**ï¼ä½åªå¨**åºä»¶**ï¼HTML / PDFï¼éåå¹¶ï¼è®¾è®¡å¨ç»å¸ä¸çä¸è§ãæ¬è½®**没æ¹ä»»ä½ç产代ç **ï¼åªè¡¥äºå¯¹æè¦ç + åè¯ã ### 29.1 åå¹¶æä¹åççï¼ä¸æ®µåå½è°ï¼ | ç¯è | è°å | åä»ä¹ | |---|---|---| | æ åºåå¹¶å | æ¨¡æ¿ | `columns` éè¯¥åæ é¢å¸¦ `#`ï¼`buildContent` åªå¨è¿ä¸æ ¼å `rowspan="{{item.group.span}}"` ä¸ `hidden="{{item.group.hidden}}"` | | ç®åºåå¹¶å è¡ | å端 `MesQcReportApiImpl.java:331-351` | ç»ç第ä¸è¡ `span = entryRows.size()`ã`hidden = ""`ï¼ç»å å ¶ä½è¡ `span = 1`ã`hidden = "hidden"` | | 渲æè§è§åå¹¶ | 两侧渲æå¨ `render.ts:242` / `HtmlRenderer.java:222` | é½æãæ±å¼ç»æä¸ºç©ºä¸² â ä¸è¾åºè¯¥å±æ§ãå¤çï¼ç»å¤´è¡ `hidden` æ¶å¤±ã带 `rowspan` æ¾ç¤ºï¼å ¶ä½è¡ `hidden="hidden"` è®©æ´æ ¼ `display:none` | 䏿¡å£å¾ï¼åå¹¶è¡æ°æ¯**æ¬å该ç»å®é è¡æ°**èéä¸»æ°æ®å项æ°ï¼è¯¥ç»æ¬åæ å项è¡å `span=1` ä¸åå¹¶ï¼ç¬ç«é¡¹åçï¼ï¼`hidden` å¿ é¡»æ¯å符串ï¼é ã屿§å¨ä¸å¨ãèµ·ä½ç¨ï¼`hidden="false"` ç §æ ·éèï¼ã ### 29.2 为ä»ä¹è¿è½®æç¡®è®¤ï¼èªå®å忝ä¸ç´æ¯ç²åº 第 1~3 èçæ¢éæçæ¯**é»è®¤å**忝ï¼`tableSchema()` æå·¥åæï¼ï¼è **AI å¯¼å ¥ä¸å¾å¡« `columns`**ï¼èµ°çæ¯ `effectiveColumns` çå¦ä¸æ¡åæ¯ã两æ¡åæ¯åèªæ¼ `<tr>`ï¼å并屿§ææ²¡æè·çèµ°ï¼é»è®¤å绿ä¸ä»£è¡¨èªå®å绿ââ`verify-grouped-table.ts` éåå 对èªå®åæ¯**é¶è¦ç**ã å®é 读ç çï¼èªå®ååæ¯åæ ·ç¨ `column.merge ? GROUP_CELL_ATTRS : ''`ï¼`merge` æ¥èª `parseColumnSpec` ç `#` åç¼ï¼æä»¥æ¯éçãä½è¿æ¯ãè¯»ç æ¨å®ãï¼ä¸æ¯åè¯ï¼æ¬è½®è¡¥ä¸ã ### 29.3 æ¬è½®çåè¯ | 项 | åæ³ | ç»æ | |---|---|---| | èªå®åç»æ | ç¨æ¨¡åçå®äº§åºï¼æ¨¡æ¿ 33 è稿æåä¸è½®ï¼`#项ç®=â¦ï½â¦ï½ç»è®º=â¦`ã`headerSpans=æ£éªç»æ^5ï½ç»è®º^1`ï¼éåå `assembleQualityCanvas` | `[#项ç®]` æ ¼å¸¦ `rowspan="{{item.group.span}}"`+`hidden="{{item.group.hidden}}"`ï¼å ¨è¡¨åªæ¤ä¸æ ¼å¸¦ `rowspan`ï¼ä¸å± `æ£éªç»æ` `colspan=5`ã`ç»è®º` `rowspan=2`ãä¸å±è¡¨å¤´ 5 å | | è´åå¯¹ç § | åä¸ä»½ `columns` 廿 `#` | 两å¤ç»å®æ´ä¸ªæ¶å¤±ãåæ°ä¸ç»å®ä¸å â `#` 䏿¯è£ é¥°ï¼ææ²¡æå®ç»æç¡®å®ä¸å | | 渲æè¯ä¹ | `verify-grouped-table.ts` 第 2/3 èï¼æ¢æï¼æ¬è½®éè·ï¼ | `hidden=""` çæ ¼å¯è§ã`hidden="hidden"` çæ ¼ `display:none`ãåæ°ç¨³å® | | Chromium 宿µ | æå¼ç¬¬ 2 èæ¸²æäº§ç©ï¼è¯» `td.rowSpan` ä¸ `getBoundingClientRect()` | ç»åæ ¼ `rowSpan=3`ãåç´ é« 93px = å ¶åä¸è¡ï¼å 31pxï¼ä¹åï¼ç»å 两è¡çç»åæ ¼ `display:none` | | ç°ç½å®ä¾ | æ¥ `qc_report_instance`ï¼ä» id 44/45ï¼æ¨¡æ¿ 3ï¼ | 产ç©é**没æ** `rowspan`ï¼å¿«ç §é乿²¡æ `"group":` åæ®µ â è¿ä¸¤åæ¬èº«ä¸å¸¦åç»ç»æï¼å±æ£å¸¸ | ### 29.4 端å°ç«¯å®è·ï¼çæ¥å£ + çæ¸²æï¼ ä¸ä¸èåªè¯å°ã屿§è½æã渲æå¨è®¤å¾ããç¨æ·è¦æ±çä¸ç¼æåï¼äºæ¯èµ°äºä¸éçå®é¾è·¯ï¼ | æ¥ | å¨ä½ | ç»æ | |---|---|---| | 1 | æ¸æ¸ ç°ç¶ | ç°ç½**没æ**任使¨¡æ¿çç»å¸æ¾ `GroupedQualityTable`ï¼1/3/5/33 å ¨æ¯æå¹³ `QualityTable`ï¼ï¼å¿ é¡»æ°å»ºä¸ä¸ª | | 2 | åç° IQC å 1 **æ¬èº«å°±æä¸äºåç»ææ **ï¼21+22/23/24/25ã26+27~30ã31+32ï¼ï¼ç¶æ 4 已宿 | ä¸ç¨é è´¨æ£å | | 3 | ç¨ `assembleQualityCanvas` æ AI èç¨¿ï¼æ¨¡æ¿ 33 é£ä»½ï¼`#项ç®=â¦`ï¼ç¼è¯æç»å¸ | 踩å°ä¸¤ä¸ªåï¼è§ä¸ | | 4 | `POST /qc-report/template/create` å»ºæ¨¡æ¿ 38ï¼reportType=1ï¼â `template-version/create` åç»å¸ â `publish` | v1.0 建好并åå¸ | | 5 | `POST /qc-report/instance/generate-from-qc`ï¼qcType=1, qcId=1, templateId=38ï¼ | å®ä¾ 48ï¼17 项ï¼`warnings` 为空 | | 6 | 读å®ä¾ 48 ç `render_html` | åªæ 5 个**空 div**ã0 个 `<tr>` â æ¸²æå¨æ²¡åå°ç»ä»¶ç children | | 7 | æ¹ç¨ GrapesJS è§£æåéå v1.1 â éæ°åºä»¶ | å®ä¾ 49ï¼`render_html` 8356 å符ã22 个 `<tr>` | å®ä¾ 49 产ç©éçåå¹¶ï¼æ£å计æ°ï¼éç®æµï¼ï¼ ``` rowspan = ["2","1","1","5","1","1","1","1","6","1","1","1","1","1","2","1","1","1"] hidden = ["hidden" à 10] ``` `5` / `6` / `2` å嫿¯æ°´åï¼1 头 + 4 å项ï¼ãé ¸å«éï¼1 + 5ï¼ãæ£ä¸éå«éï¼1 + 1ï¼çç»åæ ¼ï¼è®©ä½æ ¼ 4+5+1 = 10 个ï¼ä¸ `hidden` æ°å¯¹ä¸ï¼æ¶å°¾é£ä¸ª `2` æ¯è¡¨å¤´ãç»è®ºã纵å跨两å±ã åä¸ä»½äº§ç©ä¸¢è¿ Chromium 宿µï¼è¯» `td.rowSpan` ä¸å®é åç´ é«ï¼ï¼ | ç» | rowSpan | åç´ é« | è¡é«å¯¹ç § | |---|---|---|---| | æ°´å | 5 | 155px | 5 à 31px | | é ¸å«é | 6 | 186px | 6 à 31px | | æ£ä¸éå«é | 2 | 62px | 2 à 31px | | 该 3 ç»å çå项è¡ï¼å ± 10 è¡ï¼ | 1 | 0 | `display:none`ï¼è®©ä½ | | ç¬ç«é¡¹ï¼å¤è§/è²åº¦/å¯åº¦/æå çï¼ | 1 | 31px | åå ä¸è¡ | 渲æå¤å»å¾ï¼`D:/qcl-tmp/merge-e2e.png`ï¼ã项ç®ãåä¸åå°åºã表头ãç»è®ºãåªå°ä¸éãè¡¨æ ¼è½å¨çº¸å¼ å 䏿º¢åºï¼ã **建模æ¿è¸©å°çåï¼ç¨åºå忍¡æ¿é½è¦æ³¨æï¼** 1. **è½åºçç»å¸å¿ é¡»æ¯è§£æåçèç¹æ ï¼ä¸è½æ¯ HTML 串ã** `assembleQualityCanvas` 产ç©éæ¯ä¸ªèç¹ç `components` æ¯**æªè§£æç HTML å符串**ââGrapesJS 卿µè§å¨é伿宿æèç¹æ ï¼ä½**å端渲æå¨ä¸è§£æ**ãç´æ¥æè£ é 产ç©å¡è¿ `schema_json`ï¼å端ä¼å¿½ç¥ childrenãæ¸²åº 5 个空 divï¼å®ä¾ 48 å°±æ¯è¿æ ·ï¼ã 2. **å GrapesJS è¦ä¼ èç¹å¯¹è±¡ï¼ä¸è½èªå·±æ¼ HTML 串å `setComponents`ã** èµ°åç¬¦ä¸²è§£ææ¶å±æ§åä¼è¢« HTML è§£æå¨**转å°å**ï¼`data-qc-itemsPath` â `data-qc-itemspath`ã`data-qc-headerSpans` â `data-qc-headerspans`ï¼ï¼å±æ§é¢æ¿ä¸æ¸²æé½ä¼ç»ä¸ä¸ã设计å¨èµ°çå°±æ¯ãç´æ¥ä¼ 对象ãè¿æ¡è·¯ï¼æä»¥åºéåçä¸ç´æ¯é©¼å³°ã 3. è§£æå `getProjectData()` çç»ææ¯ `{dataSources, assets, styles, pages, symbols}`ï¼ä¸åéç»å¸**éé®ä¸è´**ï¼å è `style` ä¼è¢«æåæ `styles` éç `#id` è§åã ### 29.5 å·²éªè¯ / æªéªè¯ - â å·²éªè¯ï¼æ¬è½®è¡¥é½ï¼ï¼èªå®åçå并屿§ç¡®å®è½æï¼è£ é 产ç©å®è¯ï¼ï¼æ¸²æå¨æ `span`/`hidden` 渲æçåå¹¶ï¼Chromium 宿µï¼ï¼è´åå¯¹ç §ï¼**çæ¥å£åºä»¶çæåé `rowspan`/`hidden` 䏿µè§å¨å®æµæçé½å¯¹**ï¼å®ä¾ 49ï¼ã - ð¡ æªéªè¯ï¼**PDF åºä»¶æªè·**ââæ¬è½®åªéª HTML 产ç©ãPDF æ¯åç«¯è° Chromium æå°åä¸ä»½ HTMLï¼å±æ¢æè½åï¼æªå¤è·ã - ä¸åä¸è½®ç¸åçæç¤ºï¼æªå¾éé£ä»½ç»å¸æ¯**æªä¿åç AI å¯¼å ¥è稿**ï¼å·æ°è®¾è®¡å¨çå°çæ¯åºéæ§çå¹³éº `QualityTable`ï¼`id=21`ï¼æ `columns`ï¼ã ### 29.6 æ¬è½®é åºçæµè¯æ°æ®ï¼**æªæ¸ ç**ï¼å¾ ç¨æ·å®å¤ºï¼ | ç±»å | ç¼å· | 说æ | |---|---|---| | æ¥åæ¨¡æ¿ | 38 `ZZ_VERIFY_MERGE`ãéªè¯-åç»çºµååå¹¶ã | reportType=1ã**å¯ç¨ä¸**ãcurrentVersion=v1.1 â **ä¼åºç°å¨ IQC åºæ¥åçæ¨¡æ¿ä¸æé** | | 模æ¿çæ¬ | 23 / 24 | åå·²åå¸ï¼24 æ¯è§£æåçæ£ç¡®ç»å¸ï¼23 æ¯åçé£ä»½ | | æ¥åå®ä¾ | 48 / 49 | 48 æ¯å产ç©ï¼ç©º divï¼ï¼49 æ¯åå¹¶æ£ç¡®çæå | **没碰**ï¼ç°ç½ 34 è¡è´¨æ£ææ ãIQC å 1 åå ¶ 16 è¡ãæ¨¡æ¿ 1/3/5/33 ä¸å ¶çæ¬ãå®ä¾ 44/45ã ### 29.7 çç - `mom-pro2-before/.qc-conformance/verify-grouped-table.ts`ï¼æ°å¢ç¬¬ 4 èï¼èªå®åçåå¹¶ç»æ + è´åå¯¹ç §ï¼ï¼æä»¶ä»åªè¦çé»è®¤ååæä¸¤æ¡åæ¯é½è¦çã - `docs/qc_report_ai_import_frontend_integration.md`ï¼`columns` çåæ³ä¹åè¡¥ä¸å°èãã#ãé£ä¸åå°åºæä¹åå¹¶çãï¼æä¸æ®µèè´£ã䏿¡å£å¾åæ¸ ï¼åå åªåäºãå `#` å°±åå¹¶ãï¼æ²¡è¯´åå¹¶å è¡ãä»ä¹æ¶åä¸åå¹¶ï¼ã - `docs/project-business/mermaid/05-quality-flow.mmd`ï¼`RCTXG` èç¹åå åãåªåå¹¶æ£éªé¡¹ç®ä¸åãââèªå®åä¹ååå¹¶åç± `#` å³å®ï¼æ¹ä¸ºæ `#` é£ä¸åæè¿°å¹¶è¡¥ä¸ä¸¤æ¡å£å¾ã ### 29.8 æªå - 没æ¹ä»»ä½ç产代ç ï¼åå¹¶é»è¾æ¬æ¥å°±éï¼ç¼ºçæ¯åè¯ï¼ - 没让设计å¨ç»å¸é¢è§åå¹¶ï¼ç»å¸åªæä¸è¡æªå±å¼ç模æ¿è¡ä¸ä¸æ±è§£ç»å®ï¼`rowspan="{{â¦}}"` æ¯éæ³å¼æ 1 å¤çï¼ââè¦åæ¯ç¬ç«å³ç - 没为端å°ç«¯å¤ç°å»é ä¸å¼ 带åç»ææ çè´¨æ£å ## ä¸åãæ ·åä¿¡æ¯æ«è¡è¡¥é½ç©ºä½ï¼2026-09-19 ç»ï¼ **ç°è±¡**ï¼ç¨æ·å¨è®¾è®¡å¨éçå°çï¼ï¼æ¨¡æ¿ 38 çãæ ·åä¿¡æ¯ã表æåä¸è¡æ¯ `æ£éªæ¥æ | {{report.inspectDate}} | 空 | 空`ââå³ä¾§ç§åºä¸¤ä¸ªåªæè¾¹æ¡çç©ºæ ¼åã **æ ¹å **ï¼`sample-info.ts` ç `buildRows` æ `pairsPerRow` æ¯ç» 2 æ ¼ååæ®µï¼æ«è¡å䏿»¡ä¸ç»æ¶ç §æ ·ææ ¼åéºæ»¡ï¼ä½ä¸çå就空çãåæ®µæ°ä¸æ¯ãæ¯è¡ç»æ°ãæ´æ°åæ¶å¿ ç¶åºç°ï¼è **AI å¯¼å ¥æåä»¶åæ®µå¡« `fields`ï¼å¥å¶ä¸ç±æä»¬å®**ï¼æ¬ååä»¶ 5 é¡¹ãæ¯è¡ 2 ç»ï¼æ£æ¯æå ¸åçä¸ç§ï¼ã **ä¿®æ³**ï¼æ«è¡åæ®µæ° `< pairsPerRow` æ¶ï¼è®©**æåä¸ä¸ªãå¼ãæ ¼**横ååæä½ä¸çåãè·¨åæ° = `(pairsPerRow - æ«è¡å段æ°) à 2 + 1`ã è¡¥å¨å¼æ ¼è䏿¯åæ®µåæ ¼ââåæ®µåæ ¼å¸¦ `width:90px`ï¼è¡¥ä¸å»è¿ä¸åç宽度ä¼è·çåï¼åè¡å·¦è¾¹ç¼å°±éå¼äºã **为ä»ä¹é»è®¤å段ä¸åå½±å**ï¼é»è®¤ 6 项 ÷ 2 = 3 个æ´è¡ï¼ä¸æ ¼é½ä¸è¡¥ã`verify-columns.ts` 第 1 èå»ç»çéåæçº¹ `SAMPLE_INFO_BASELINE` å æ¤åæ ·éè¿ââè¿ä¹æ¯ãæ¹è¿ä¸ªä¸ä¼å¨å°å·²å叿¨¡æ¿ãçæºå¨è¯æ®ã **æ¢é**ï¼`verify-columns.ts` 第 5 èè¡¥ 9 æ¡æè¨ââ5 项 ÷ 2 åº 3 è¡ 10 æ ¼ / åªå¤ 1 个 `colspan` / è·¨ 3 å / è¡¥çæ¯å¼æ ¼ï¼å¯¹çåæ®µåæ ¼æè¨ `width:90px` 䏿 `colspan`ï¼/ æ«è¡ä»æ¯ãæ£éªæ¥æ + å®çå¼ã/ é»è®¤ 6 项æ `colspan` / `pairsPerRow=1` æ°¸ä¸è¡¥ / 4 项 ÷ 3 è·¨ 5 åãè·éï¼ä¸ä¸ªå 弿¢éï¼åç»è¡¨ãéå¤è¡ãå¾ å¤å®ï¼ä¸ä¸¤ä¸ªæ¹å¨æä»¶ typecheck å ¨ç»¿ã **端å°ç«¯å¤çæå**ï¼ç¨æ´æ°åçç»ä»¶éè£ é æ¨¡æ¿ 38 çåä¸ä»½è稿 â GrapesJS è§£æ â å **v1.2**ï¼çæ¬ id 25ï¼â `generate-from-qc` åº**å®ä¾ 50**ï¼`QR20260919-0016`ï¼ã å®ä¾ 50 éæ ·åä¿¡æ¯æ«è¡å¼æ ¼ `colspan=3`ã宿µå®½ 809px = å ¶ä½ä¸åä¹åï¼90 + 250 + 469ï¼ï¼å³ä¾§åæ ç©ºæ ¼ï¼åç»è¡¨ç `rowspan`ï¼5/6/2ï¼ä¸ 10 ä¸ªè®©ä½æ ¼åæ ·ä¿çãæ¸²æå¤å»å¾ `D:/qcl-tmp/merge-v12.png`ã **çç**ï¼`sample-info.ts`ï¼`buildRows`ï¼ã`.qc-conformance/verify-columns.ts` 第 5 èã䏿¬¡æ§è£ é èæ¬è·å®å·²æ¬åºä»åºï¼`D:/qcl-tmp/build-merge-v12.ts`ï¼ã **æªå**ï¼æ²¡æ¹ `HtmlRenderer` / `BASE_CSS`ï¼æ²¡å¨æ¨¡æ¿ 33 ä¸å ¶å®å鿍¡æ¿çç»å¸ï¼æ°è§ååªå¨æ°è£ é æç¨æ·éåæ¶çæï¼ï¼PDF æªéè·ã ä¸å¡å¯è§åæªæ´æ°ââè¿æ¯ç»ä»¶æçç»èï¼ä¸è½å¨ãä¸å¡æ¨¡å / ä¸å¡å¯¹è±¡ / æµç¨æ¥éª¤ / ç¶ææµè½¬ / è·¨æ¨¡åæ°æ®æµ / AI è½å / è§è²æéãä»»ä½ä¸ç±»ã docs/ÖÇÄÜÖʼ챨¸æÆ½Ì¨-·½°¸Éè¼Æ.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,374 @@ # æºè½è´¨æ£æ¥åè®¾è®¡å¹³å° â æ¹æ¡è®¾è®¡ï¼Phase 0 é¡¹ç®æ«æäº§åºï¼ > ç¶æï¼**å¾ ç¨æ·ç¡®è®¤**ï¼ç¡®è®¤åæ¹å¯è¿å ¥ Phase 1 ç¼ç ï¼ > æ«ææ¥æï¼2026-09-17 > 便®ææ¡£ï¼`docs/æºè½è´¨æ£æ¥å设计平å°ââClaude Code Agent ä¸ä¸å¼åæç¤ºè¯.md` > ååï¼å çè§£ç°æç³»ç» â æå¤§åå¤ç¨ â æå°åä¾µå ¥ --- ## ä¸ãå½åé¡¹ç®ææ¯æ ### å端 | 项 | ç°å¼ | |---|---| | æ¡æ¶ | Spring Boot 4.1ï¼yudao ä½ç³»ï¼+ Spring Security | | ORM | MyBatis-Plusï¼`BaseMapperX` / `LambdaQueryWrapperX`ï¼ | | æ°æ®åº | MySQL 8ï¼`application-local.yaml` æå `ruoyi-vue-pro`ï¼ | | ç«¯å£ | 48080ï¼profile = `local`ï¼å¦æ dev/test/jhhg/wtxc/yzfx/zsjcï¼ | | å¤ç§æ· | **å ¨å±å ³é**ï¼`yudao.tenant.enable: false`ï¼â æ°è¡¨ä¸å `tenant_id` | | ææ¡£ | springdoc OpenAPI3 + Knife4j 4.5.0 | | Excel | FastExcel 1.3.0ï¼`ExcelUtils`ï¼ | | ç¼å | Redisï¼db 5ï¼ | ### å端ï¼`mom-pro2-before`ï¼**åªè¯»**ï¼ | 项 | ç°å¼ | |---|---| | å½¢æ | Vben Admin **5.7.0 ååºç¨ç**ï¼`@vben/web-antd-standalone`ï¼ï¼ä¸å¡ä»£ç 卿 ¹ `src/` | | æ¡æ¶ | Vue 3 + TypeScript + Vite + Pinia + Vue Router | | UI åº | **Ant Design Vue 4.2.6**ï¼å¯ä¸ï¼**æ Element Plus**ï¼ | | è¡¨æ ¼ | vxe-tableï¼`useVbenVxeGrid`ï¼+ `TableAction` | | è¯·æ± | `src/api/request.ts` ç `requestClient`ï¼åç¼ `/admin-api`ï¼ä»£çå° `192.168.0.10:48080` | | å«å | `#/* â ./src/*` | | å¼åç«¯å£ | 5666 | | å½ä»¤ | `pnpm dev` / `pnpm build` / `pnpm typecheck` | --- ## äºãå½ååç«¯æ¶æ - **è·¯ç±ä¸èåç±å端驱å¨**ï¼`src/router/access.ts` ä» `accessStore.accessMenus`ï¼å端 `system_menu`ï¼çæè·¯ç±ï¼ç¨ `import.meta.glob('../views/**/*.vue')` æå端 `component` å符串ï¼å¦ `mes/qc/defect/index`ï¼æ å°æç»ä»¶ã â **æ°å¢é¡µé¢åªéæ¾å° `src/views/**/index.vue` + æä¸æ¡ `system_menu` è®°å½ï¼æ éæå·¥æ³¨åè·¯ç±ã** - `src/router/routes/modules/*.ts` ä» ç¨äºæ¾éè页ï¼`hideInMenu: true`ï¼ï¼å¦è¯¦æ /ç¼è¾é¡µã - **å ¸å CRUD åèæ ·ä¾**ï¼æ°é¡µé¢ç §æç»æï¼ï¼ - `src/views/mes/qc/defect/index.vue`ï¼åè¡¨ï¼ - `src/views/mes/qc/defect/data.ts`ï¼`useGridColumns` / `useGridFormSchema` / `useFormSchema`ï¼ - `src/views/mes/qc/defect/modules/form.vue`ï¼å¼¹çªè¡¨åï¼ - `src/api/mes/qc/defect/index.ts`ï¼API åå±ï¼`export namespace XxxApi`ï¼ - ç®å½çº¦å®ï¼`src/views`ã`src/api`ï¼éå viewsï¼ã`src/store`ã`src/components`ã`src/types`ã`src/utils`ã - æéï¼æé®çº§ç¨ `v-access:code` æ `TableAction` ç `auth: [...]`ï¼å¤æç¨ `useAccess().hasAccessByCodes`ã - åå ¸ï¼`@vben/constants` ç `DICT_TYPE`ï¼`src/packages/constants/src/dict-enum.ts`ï¼ï¼é `getDictOptions` / `DictTag` / `cellRender: CellDict`ã --- ## ä¸ãå½ååç«¯æ¶æ - **Maven 夿¨¡å**ï¼å æ ¹ `cn.iocoder.yudao.module.{模å}`ãæ°å¢æ¨¡åéæ¹ 2 å¤ï¼æ ¹ `pom.xml` ç `<modules>`ã`yudao-server/pom.xml` ç `<dependencies>`ï¼`yudao-dependencies` åªç®¡ç¬¬ä¸æ¹çæ¬ï¼`lombok.config` å ¨å±çæï¼æ `spring.factories` éç»´æ¤ï¼ã - 忍¡åå åå±ï¼ä»¥ MES 为ä¾ï¼ï¼ | å± | è·¯å¾ | |---|---| | Controller | `controller/admin/{å}/{åå}/XxxController.java` | | VO | `controller/admin/{å}/{åå}/vo/Xxx{Save,Resp,Page}ReqVO.java` | | Service | `service/{å}/{åå}/XxxService.java` | | ServiceImpl | `service/{å}/{åå}/XxxServiceImpl.java` | | DO | `dal/dataobject/{å}/{åå}/XxxDO.java` | | Mapper | `dal/mysql/{å}/{åå}/XxxMapper.java` | | æä¸¾ | `enums/{å}/` | | é误ç | `enums/ErrorCodeConstants.java` | - éç¨è½åï¼ç´æ¥å¤ç¨ï¼ï¼`CommonResult<T>`ã`PageResult<T>`/`PageParam`ã`BaseDO`ï¼`createTime/updateTime/creator/updater/deleted` èªå¨å¡«å ï¼ã`ServiceException` + `ServiceExceptionUtil.exception(ErrorCodeConstants.XXX)`ã`BeanUtils`(MapStruct)ã`BaseMapperX`ã`LambdaQueryWrapperX`ã - **JSON ååå¨**ï¼`@TableName(value="...", autoResultMap=true)` + `@TableField(typeHandler = Jackson3TypeHandler.class)`ï¼è§ `BpmFormDO`ï¼ââæ¨¡æ¿ Schema 大忮µèµ°è¿ä¸ªæ¨¡å¼ã - æµè¯ï¼JUnit5 + `BaseDbUnitTest`ï¼H2ï¼/ `BaseMockitoUnitTest` + PODAMã --- ## åãå½åç®å½ç»æï¼å ³é®é¨åï¼ ``` mom-pro2-after/ # å端ä»åºæ ¹ âââ pom.xml # <modules> 注åå¤ âââ yudao-server/ # å¯å¨æ¨¡åï¼pom éèåä¸å¡æ¨¡åï¼ âââ yudao-dependencies/ # ç¬¬ä¸æ¹çæ¬ç®¡ç âââ yudao-framework/ # æ¡æ¶ starter âââ yudao-module-system|infra|bpm|member| â crm|erp|mes|mdm|hrm|srm|aftersales|ai|bi|im âââ yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/ â âââ controller/admin/qc/{iqc,ipqc,oqc,rqc,ncr,defect,indicator,template}/ â âââ service/qc/... â âââ dal/dataobject/qc/... â âââ enums/qc/ (MesQcTypeEnum / MesQcCheckResultEnum / MesQcResultValueTypeEnum ...) âââ docs/ # æ¹æ¡/èè°ææ¡£ âââ mom-pro2-before/ # å端ä»åºï¼åªè¯»ï¼ âââ src/{api,views,router,store,components,packages}/ ``` --- ## äºãå½å UI æ¡æ¶åçæ¬ **Ant Design Vue 4.2.6 + Vben Admin 5.7.0**ï¼æ Element Plusï¼ã â æ°æ¨¡å UI å¿ é¡»ç¨ **Vben + Ant Design Vue**ï¼**ç¦æ¢å¼å ¥ Element Plus**ã --- ## å ãå½åå¯ä»¥å¤ç¨çè½åï¼éç¹ï¼ | è½å | ç°ç¶ | å¤ç¨æ¹å¼ | |---|---|---| | **è´¨æ£ä¸å¡æ°æ®** | MES æå®æ´ qc åï¼`mes_qc_iqc/ipqc/oqc/rqc` + `*_line` + `mes_qc_template/_indicator/_item` + `mes_qc_indicator_result(_detail)` | æ°å¹³å°ç´æ¥æ³¨å ¥ `MesQcIqcService` çåæ°ï¼**ä¸éå»ºè´¨æ£æ¨¡å** | | è´¨æ£æä¸¾ | `MesQcTypeEnum`ã`MesQcCheckResultEnum`ã`MesQcResultValueTypeEnum` ç | ç´æ¥å¼ç¨ | | æä»¶ä¸ä¼ ï¼åç«¯ï¼ | infra `FileApi.createFile(bytes,name,dir,type)` â è¿å URLï¼system `StorageFileUtil.saveStorageAttachment(...)` ç»å®ä¸å¡è®°å½ | PDF çæåä¸è¡ä¿å | | æä»¶ä¸ä¼ ï¼åç«¯ï¼ | `src/api/system/storage/index.ts`ï¼`/system/storage-blob/upload` + `/system/storage-attachment/bind` | 模æ¿ç¼©ç¥å¾/éä»¶å¤ç¨ | | **äºç»´ç /æ¡ç ï¼å端ï¼** | `qrcode 1.5.4`ã`jsbarcode 3.12.3`ï¼å·²å°è£ `packages/effects/common-ui/src/components/barcode/barcode.vue`ï¼å« `getImageBase64()`ï¼ | **ç´æ¥å¤ç¨ï¼ä¸æ°å¢åº** | | æå° | `vue3-print-nb 0.1.4`ï¼èä¾ `views/bpm/processInstance/detail/modules/process-print.vue`ï¼ | å¤ç¨ | | ç¾å | `vue3-signature 0.4.4`ã`SignaturePad`ï¼HRM å·²ç¨ï¼ | å¤ç¨ | | ææ½ | `vuedraggable 4.1.0`ã`sortablejs 1.15.7` | å¤ç¨ | | å¯ææ¬ | `tinymce 7.9.3` / `@tiptap/*` | å¤ç¨ï¼RichText ç»ä»¶ï¼ | | å¾è¡¨ | `echarts 6.1.0` | å¤ç¨ï¼ç»è®¡ç»ä»¶ï¼ | | 设计å¨å ä¾ | `bpmn-js 18.16.1`ï¼`views/bpm/model/`ã`views/mes/process-design/`ï¼ä¸ºé¡¹ç®å å¯ä¸"ç»å¸+屿§é¢æ¿"å ä¾ï¼`@form-create/antd-designer 3.4.0` å·²è£ | åèå¸å±ç»ç» | | æéä½ç³» | `@PreAuthorize("@ss.hasPermission('x:y:z')")` + å端èå `system_menu` | 沿ç¨ï¼**䏿°å»ºæéç³»ç»** | | åå ¸ä½ç³» | `system_dict_type/data` + å端 `DICT_TYPE` | æ°å¢ `QC_REPORT_*` åå ¸é¡¹å³å¯ | --- ## ä¸ãå½åä¸è½å¤ç¨ãéè¦æ°å¢çè½å | ç¼ºå£ | 说æ | é£é© | |---|---|---| | **HTML/PDF 渲æå¼æ** | å ¨é¡¹ç®**é¶ PDF çæè½å**ãä» PDFBoxï¼CRM/ERPï¼åªè¯»è§£æï¼ãFastExcelï¼Excel 导åºï¼ã | é« â éæ°å¢ä¾èµ | | **Playwright / Chromium** | å®å ¨ä¸åå¨ | **é« â éä¸è½½ Chromiumï¼çº¦ 150MBï¼ï¼é确认ç½ç»/离线ç¯å¢** | | **GrapesJS** | å端**æªå®è£ **ï¼æ `grapesjs` åä»»ä½ preset/pluginï¼ | ä¸ â éæ°å¢ä¾èµï¼ææ¡£ §54 è¦æ±å 论è¯ï¼ | | æ¨¡æ¿ Schema è®¾è®¡å¨ | æ ï¼BPM 表åè®¾è®¡å¨æ¯å·¥ä½æµç¨éï¼éæ¥åæçï¼ | ä¸ | | æ¥åå®ä¾/çæ¬ä½ç³» | æ | ä½ | | è§å弿 / æ°æ®ç»å®å¼æ | æ ï¼`MesQcAiController` æ AI å¤å®å»ºè®®ï¼ééç¨è§åå¼æï¼ | ä¸ | | ç§¯æ¨æ¥è¡¨ `yudao-module-report` | æ ¹ pom **注éç¦ç¨**ï¼JimuReport 2.3.4 çæ¬ä»å¨ dependencies ä¸ï¼ | å½åå²çªé£é© | --- ## å «ãæ°æ®åºè®¾è®¡å»ºè®® **建议æ°å»ºç¬ç«æ¨¡å**ï¼ä¸è¡ä¸ä¸å¡è§£è¦ï¼ææ¡£ §35 è¦æ±ï¼ï¼è¡¨åéç¨ `qc_report_*`ï¼è´´å项ç®"模ååç¼"å½åä¹ æ¯ï¼ææ¡£åæä¸º `quality_report_*`ï¼è¯ä¹ä¸è´ï¼ï¼ | 表 | ç¨é | å ³é®å段 | |---|---|---| | `qc_report_template` | 模æ¿ä¸»è¡¨ | `template_code`(uk), `template_name`, `industry`, `report_type`, `page_size/orientation`, `status`, `current_version`, `description`, `thumbnail_url` | | `qc_report_template_version` | çæ¬å¿«ç §ï¼**åå¸åä¸å¯è¦ç**ï¼ | `template_id`, `version`, `schema`(JSON longtext), `html`, `css`, `status`, `publish_time` | | `qc_report_data_source` | æ°æ®æºå®ä¹ | `template_id`, `source_key`, `source_type`(SQL/API/å ç½®), `config`(JSON) | | `qc_report_rule` | å¤å®è§å | `template_id`, `target_path`, `expression`, `pass_value/fail_value` | | `qc_report_instance` | æ¥åå®ä¾ï¼**å¿ é¡»å `data_snapshot`**ï¼ | `report_no`(uk), `template_id`, `template_version`, `business_id`, `business_type`, `data_snapshot`(JSON), `render_html`, `pdf_file_url`, `status` | | `qc_report_render_record` | 渲æ/PDF èæ¶ä¸å¼å¸¸è®°å½ï¼ææ¡£ §41 æ¥å¿è¦æ±ï¼ | `report_id`, `render_start/end/duration`, `pdf_duration`, `browser_status`, `error_stack` | ç»ä¸ç»§æ¿ `BaseDO`ï¼ä¸å»º `tenant_id`ï¼Schema/Config ç±»åæ®µç¨ `Jackson3TypeHandler` + `autoResultMap=true`ã --- ## ä¹ãQuality Components æ¶æï¼åç«¯ï¼ æææ¡£ §8/§9 建ç«ç¬ç«ç»ä»¶åè®®å±ï¼**䏿ä¸å¡ç»ä»¶åæ»è¿ GrapesJS**ï¼ ``` src/components/quality/ âââ core/{types.ts, registry.ts, factory.ts, serializer.ts, validator.ts} âââ base/ layout/ inspection/ table/ result/ statistics/ signature/ header/ footer/ barcode/ qrcode/ âââ index.ts # ç»ä¸æ³¨åå ¥å£ï¼registerAllQualityComponents(editor)ï¼ ``` `QualityComponentDefinition` ç»ä¸åè®®ï¼`type / name / label / category / icon / defaults / traits / dataSchema / styleSchema / propertySchema / render / validate / serialize / deserialize`ã **è¡ä¸æ©å±**ï¼`registerIndustryPack('chemical')` åªæ³¨åæ°ç»ä»¶å ï¼**䏿¹æ ¸å¿ Designer**ã **ç»ä»¶**ï¼é¦æ¹ åºç¡ï¼Text/Title/RichText/Label/Image/Logo/Container/Row/Column/Section/Grid/Divider/Spacerï¼ï¼è´¨éä¸ä¸ï¼ReportHeader / SampleInfo / InspectionItem / QualityTable / Result / Statistics / Signature / Barcode / QRCode / PageHeader / PageFooterï¼ã **TypeScript**ï¼ç¦ç¨ `any`ï¼ç¬¬ä¸æ¹åºæ ç±»åæ¶æå¯å±é¨é离并注æåå ã --- ## åãGrapesJS éææ¹æ¡ - **å¿ é¡»æ°å¢ä¾èµ**ï¼`grapesjs`ï¼+ æé `grapesjs-preset-webpage`ï¼ã项ç®ç°æ `@form-create/antd-designer` æ¯**表å设计å¨**ï¼è¯ä¹ä¸ç¬¦ï¼`bpmn-js` æ¯æµç¨è®¾è®¡å¨ï¼åä¸è½æ¿ä»£æ¥åæçç»å¸ã - éæè¦ç¹ï¼`BlockManager` 注åç»ä»¶åï¼`Components.addType` 注åèªå®ä¹ç»ä»¶ï¼èªå®ä¹ `QualityPropertyPanel`ï¼**ä¸ä¾èµé»è®¤ StyleManager**ï¼ï¼`Commands` æ `ä¿å/é¢è§/åå¸/PDF`ï¼`StorageManager` ç¦ç¨ï¼æ¹ç±å端 API ååï¼ï¼`UndoManager` æä¾æ¤ééåï¼`DeviceManager` æçº¸å¼ 尺寸ï¼A4/A3/A5 + 横å/纵åï¼æ³¨å设å¤ã - Designer å¿ é¡»æåç»ä»¶ï¼ææ¡£ §56ï¼ç¦æ¢ 2000+ è¡åæä»¶ï¼ï¼`DesignerToolbar / Sidebar / Canvas / PropertyPanel / DataPanel / RulePanel / PageSettings`ã - ç¶æç¨ Pinia åçï¼`useDesignerStore`ï¼ï¼é¿å å·¨å Storeã --- ## åä¸ãTemplate Schema **ä¸ä¿å裸 HTML**ï¼ä¿åç»æå Schemaï¼ææ¡£ §21ï¼ï¼ ```json { "schemaVersion": "1.0.0", "page": { "size": "A4", "orientation": "portrait", "margin": { "top": 20, "right": 15, "bottom": 20, "left": 15 } }, "grapes": {}, "components": [], "dataSources": [], "bindings": [], "rules": [], "styles": [] } ``` çº¸å¼ æ¯æï¼`A3 / A4 / A5 / Letter / èªå®ä¹` à `portrait / landscape`ã --- ## åäºãData Binding æ¹æ¡ ç»ä¸ `{{path}}` åè®®ï¼ææ¡£ §18ï¼ï¼ ``` {{report.reportNo}} {{sample.sampleName}} {{sample.batchNo}} {{customer.name}} {{supplier.name}} {{inspectionItems[0].actualValue}} {{inspectionItems[0].result}} ``` æ¯æï¼å¯¹è±¡ / æ°ç» / åµå¥ / æ°ç»éåï¼å¨æè¡¨æ ¼ï¼/ 计ç®å段 / è¡¨è¾¾å¼ / æ¡ä»¶å段 / æ ¼å¼å / é»è®¤å¼ã **å¿ é¡»æä¾ `ContextBuilder`**ï¼æ MES è´¨æ£åæ®ï¼`MesQcIqcDO` + `*_line` + `mes_qc_indicator`ï¼æ å°ææ å Contextï¼ä¿è¯å¯å¤ç¨å° IQC/IPQC/OQC/RQCã --- ## åä¸ãRule Engine æ¹æ¡ ç¬ç« `QualityRuleEngine`ï¼**ç¦æ¢åæ»å¨ Vue ç»ä»¶é**ï¼ææ¡£ §17ï¼ï¼ - ç®åï¼`> >= < <= = != BETWEEN IN NOT_IN` + `AND / OR` - ç»æï¼`PASS / FAIL` - ç»è®¡å½æ°ï¼ææ¡£ §36ï¼ï¼`AVG/MAX/MIN/COUNT/STDDEV/CP/CPK/PASS_RATE/FAIL_RATE` - åå端**åèªå®ç°ä¸ä»½**ï¼å端ç¨äºè®¾è®¡æå®æ¶é¢è§å¤å®ï¼å端ç¨äºæ£å¼æ¸²æï¼ä»¥å端为åï¼æ¶é¤æ¼ç§»ï¼ã - **å®å ¨**ï¼è¡¨è¾¾å¼**ä¸å¾ç¨ `eval` / `ScriptEngine` ç´æ¥æ§è¡**ãæ¹æ¡ï¼èªå»ºè¯æ³/è¯æ³è§£æï¼éå½ä¸éï¼çæ AST æ±å¼ï¼ç½åå彿°è¡¨ã --- ## ååãHTML Render æ¹æ¡ æå¡åå±ï¼ææ¡£ §29ï¼Controller åªæ¶åï¼ï¼ ``` ReportRenderServiceï¼ç¼æï¼ âââ TemplateRenderService # å è½½çæ¬ Schema âââ DataBindingService # Context + ç»å®æ±å¼ âââ QualityRuleEngine # è§åè®¡ç® PASS/FAIL âââ HtmlRenderService # Schema â HTML + æ³¨å ¥ CSSï¼@pageãåé¡µï¼ âââ PlaywrightRenderService â PdfRenderService ``` - **HTML Sanitizationï¼ææ¡£ §33ï¼å¿ é¡»åï¼**ï¼å¥ç¦» `<script>`ã`<iframe>`ã`on*=` äºä»¶å±æ§ã`javascript:` URLï¼æ¸²æç«¯å¯¹å¤é¨èµæºåç½åå/å èåï¼æ¬å°åä½ãå¾çèµ°å ç½ URLï¼ã - 䏿åä½éå ç½®/æå®ï¼PDF 䏿å¯ç¨æ§éªæ¶é¡¹ï¼ã --- ## åäºãPlaywright / Chromium æ¹æ¡ - ä¾èµï¼`com.microsoft.playwright:playwright`ï¼çæ¬å ¥ `yudao-dependencies`ï¼ã - **`BrowserManager` åä¾**ï¼ææ¡£ §30ï¼ï¼æµè§å¨å¤ç¨ãPage çå½å¨æãå¹¶åä¿¡å·éï¼PDF å¹¶åä¸éï¼å»ºè®® 2~4ï¼ãè¶ æ¶ãå´©æºèªå¨æ¢å¤ãå ³éé©åã**ç¦æ¢æ¯æ¬¡è¯·æ± launchâclose**ã - â **æå¤§é£é©ç¹**ï¼é馿¬¡ä¸è½½ Chromiumï¼çº¦ 150MBï¼ã**è¥é¨ç½²ç¯å¢æ å¤ç½ï¼Playwright æ¹æ¡ä¸å¯è¡**ï¼å¿ é¡»æ¹ç¨çº¯ Java 渲æï¼openhtmltopdf + Flying Saucer æ iTextï¼ã**æ¤é¡¹éç¨æ·æç¡®ç¡®è®¤ã** --- ## åå ãPDF åé¡µæ¹æ¡ - `@page { size: A4; margin: ... }` + `page-break-before/after/inside`ã - QualityTableï¼`thead { display: table-header-group }` å®ç°**跨页éå¤è¡¨å¤´**ï¼`tr { page-break-inside: avoid }` é¿å è¡è¢«æå¼ã - 页ç/页è/页ç ï¼Playwright `pdf()` ç `headerTemplate/footerTemplate`ï¼`<span class="pageNumber">`ï¼ã - æ§è½ï¼1000+ è¡è¡¨æ ¼éå忏²æï¼é¿å 䏿¬¡æ§å·¨å DOMï¼ææ¡£ §39ï¼ã --- ## åä¸ãAPI 设计ï¼èæ¡ï¼å¾ 确认åç¼ï¼ | æ¹æ³ | è·¯å¾ | 说æ | |---|---|---| | POST | `/qc-report/template/create` | æ°å»ºæ¨¡æ¿ | | PUT | `/qc-report/template/update` | ä¿®æ¹æ¨¡æ¿ | | DELETE | `/qc-report/template/delete` | å é¤ | | GET | `/qc-report/template/page` | å页ï¼åç§°/ç¼ç /è¡ä¸/ç±»å/ç¶æ/å建人/æ¶é´ï¼ | | GET | `/qc-report/template/get` | 详æ | | POST | `/qc-report/template/copy` | å¤å¶ | | POST | `/qc-report/version/create` | åå»ºçæ¬ | | GET | `/qc-report/version/list` | çæ¬å表 | | POST | `/qc-report/version/publish` | åå¸ï¼ä¸å¯è¦çï¼ | | POST | `/qc-report/version/rollback` | åæ» | | POST | `/qc-report/render/preview` | é¢è§ â HTML | | POST | `/qc-report/render/html` | çæ HTML | | POST | `/qc-report/render/pdf` | çæ PDF | | GET | `/qc-report/instance/page` | æ¥åå®ä¾å表 | | GET | `/qc-report/instance/download` | ä¸è½½ PDF | > ææ¡£ç¤ºä¾ä¸º `/quality/report/*`ï¼æ¬é¡¹ç®çº¦å®ä¸º `/{模å}/{å}/*`ï¼æ 建议 `/qc-report/*`ã**å¾ ç¡®è®¤ã** æéç ï¼`qc-report:template:{query,create,update,delete,publish}`ã`qc-report:instance:{query,create,download}`ï¼æ²¿ç¨ç°æ `@ss.hasPermission` ä½ç³»ã --- ## åå «ãç®å½ç»æå»ºè®® å端ï¼**æ°å¢æ¨¡å** `yudao-module-qcreport` / å æ ¹ `cn.iocoder.yudao.module.qcreport`ï¼ï¼ ``` controller/admin/{template,version,instance,render}/... service/{template,version,instance,render}/... dal/dataobject|mysql/qcreport/... engine/{binding,rule,render}/ engine/render/playwright/{BrowserManager,PdfRenderService}.java config/ enums/ ``` å端ï¼**éææå**åå¨ï¼ï¼ ``` src/views/mes/qc/report/{template,designer,preview,instance}/ src/components/quality/{core,base,inspection,table,result,statistics,signature,...}/ src/api/mes/qc/report/ src/store/qualityReport.ts ``` > 模åå½åå¤éï¼`yudao-module-report`ï¼ä¸æ³¨éä¸çç§¯æ¨æ¥è¡¨æ¨¡å**éåï¼ä¸å»ºè®®**ï¼ã --- ## åä¹ãPhase 1~5 宿½è®¡å | Phase | å 容 | éªæ¶ | |---|---|---| | **0** | é¡¹ç®æ«æ + æ¹æ¡è®¾è®¡ï¼æ¬ææ¡£ï¼ | ç¨æ·ç¡®è®¤ â /â | | **1** | å端模åéª¨æ¶ + æ¨¡æ¿ CRUD + çæ¬è¡¨ + Schema ååï¼Designer åºç¡æ¡æ¶ + Component Registry + GrapesJS éæ + åºç¡ç»ä»¶ + ä¿å/å è½½ | 建模æ¿âæç»ä»¶âä¿åâéå¼ç¶æä¸è´ | | **2** | QualityTable / ReportHeader / SampleInfo / InspectionItem / Result + æ°æ®ç»å® + å¨æè¡¨æ ¼ + è§å弿 | `inspectionItems` èªå¨çæè¡¨æ ¼å¹¶ç®åº PASS/FAIL | | **3** | HtmlRender + Preview + Playwright + Chromium + PDF + A4/å页/页ç页è/页ç | A4 尺寸/è¾¹è·/䏿/å¤çº§è¡¨å¤´/éå¤è¡¨å¤´/跨页æ£ç¡® | | **4** | çæ¬ç®¡ç + æ¥åå®ä¾ + æé + æ¥å¿ + å¼å¸¸å¤ç | åå¸ä¸å¯è¦çã`data_snapshot` å»ç» | | **5** | èªå¨åæµè¯ + E2E + å¤æè¡¨æ ¼/å¤é¡µ/100+ è¡/å¾ç/äºç»´ç /ç¾å | ææ¡£ §43 å ¨é¨éªæ¶é¡¹éè¿ | --- ## äºåãé£é©æ¸ å | # | é£é© | ç级 | åºå¯¹ | |---|---|---|---| | 1 | **å端 Playwright/Chromium éèç½ä¸è½½ ~150MB** | ð´ é« | é确认é¨ç½²ç¯å¢å¤ç½å¯è¾¾ï¼å¦åæ¹ openhtmltopdf 纯 Java æ¹æ¡ | | 2 | **å端å±ç¦æ¹åº**ï¼`.claude/rules/frontend-project.md`ï¼æªç»æç¡®å è®¸ç¦æ¢ä¿®æ¹ `mom-pro2-before` ä»»ä½æä»¶ï¼ | ð´ é« | **å¿ é¡»è·å¾ç¨æ·æ¾å¼ææ**ï¼å¦åæ¬é¡¹ç®æ æ³äº¤ä» | | 3 | **GrapesJS éæ°å¢å端ä¾èµ**ï¼é¡¹ç®æ ï¼ | ð¡ ä¸ | éç¨æ·æ¹åï¼ä½ç§¯/æå»ºå½±åéè¯ä¼° | | 4 | æ°å¢ Maven 模åéæ¹æ ¹ `pom.xml` + `yudao-server/pom.xml`ï¼å ¬å ±æä»¶ï¼ | ð¡ ä¸ | æææ¡£ §53 说æå½±åé¢å¹¶éªè¯ `mvn compile -q` | | 5 | 表达å¼å¼æå®å ¨ï¼ç¦æ¢ evalï¼ | ð¡ ä¸ | èªå»º AST è§£æç½ååæ±å¼ | | 6 | æ¨¡æ¿ HTML å±ç¨æ·å¯ç¼è¾å 容ï¼XSSï¼ | ð¡ ä¸ | æå¡ç«¯ Sanitization + PDF æ¸²ææ²ç®± | | 7 | å岿¥åå ä¸å¡æ°æ®ååèå | ð¡ ä¸ | `data_snapshot` 强å¶è½åº | | 8 | åå²åº `ruoyi-vue-pro` 为è¿è¡åºï¼**æ°è¡¨ DDL ä¸å¾å¨æºåºä¹±è·** | ð¡ ä¸ | èµ° `docs/sql/` åå§åèæ¬ + ç®æ åºæ§è¡ | | 9 | 100+ 页 PDF å å/æ§è½ | ð¢ ä½ | BrowserManager å¹¶åéå¶ + æµå¼ååº | --- ## äºåä¸ãPhase 1 å¼å§åéè¦ç¡®è®¤çé®é¢ 1. **åç«¯ä¿®æ¹ææ**ï¼æ¯å¦å 许æä¿®æ¹ `mom-pro2-before`ï¼æ°å¢é¡µé¢/ç»ä»¶/API æä»¶ï¼ä¸éææ¢æä»£ç ï¼ï¼ - è¥ä¸å 许 â æ¬é¡¹ç®åªè½äº¤ä»**å端** + å端èè°æ¹æ¡ææ¡£ã 2. **Playwright å¯è¡æ§**ï¼å端è¿è¡/é¨ç½²ç¯å¢**è½å¦è®¿é®å¤ç½**ä¸è½½ Chromiumï¼è¥ä¸è½ï¼æ¯å¦æ¥åæ¹ç¨çº¯ Java 渲æï¼openhtmltopdfï¼ä½ä¸ºéçº§æ¹æ¡ï¼ 3. **GrapesJS ä¾èµ**ï¼æ¯å¦æ¹åæ°å¢ `grapesjs`ï¼åå¯é presetï¼ï¼ 4. **模åå½å**ï¼æ°å端模åç¨ `yudao-module-qcreport`ï¼è¡¨ `qc_report_*`ï¼æ¯å¦å¯ä»¥ï¼è¿æ¯æ²¿ç¨ææ¡£ç `quality_report_*` / `/quality/report/*` è·¯å¾ï¼ 5. **馿¹èå´**ï¼ä»å¤©æ¯å¦å åªå **Phase 1**ï¼æ¨¡æ¿ CRUD + Schema åå + Designer 骨æ¶ï¼ï¼è¿æ¯å¸æä¼å åå端 API + 渲æé¾è·¯ï¼ 6. **æ°æ®æºèå´**ï¼æ¥å主è¦é¢å **MES è´¨æ£åï¼IQC/IPQC/OQC/RQCï¼**ï¼æ¯å¦è¿æ ERP éè´å ¥åºæ£éª / CRM çå ¶ä»æ¥æºï¼ --- ## éå½ï¼æ¬æ¬¡æ«æçå ³é®äºå®åºå¤ - æ ¹æ¨¡åæ³¨åï¼`pom.xml:10-49` - MES è´¨æ£åï¼`yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/{iqc,ipqc,oqc,rqc,ncr,defect,indicator,template,indicatorresult,defectrecord}` - è´¨æ£æä¸¾ï¼`yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/` - JSON åèä¾ï¼`yudao-module-bpm/.../definition/BpmFormDO.java:53` - å端 CRUD èä¾ï¼`mom-pro2-before/src/views/mes/qc/defect/{index.vue,data.ts,modules/form.vue}`ã`src/api/mes/qc/defect/index.ts` - å端ä¸ä¼ APIï¼`mom-pro2-before/src/api/system/storage/index.ts` - å端æä»¶ APIï¼`yudao-module-infra/.../api/file/FileApi.java` docs/ÖÇÄÜÖʼ챨¸æÆ½Ì¨-Öʼ쵥¶Ô½Ó·½°¸.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,572 @@ # æºè½è´¨æ£æ¥åå¹³å° â è´¨æ£åå¯¹æ¥æ¹æ¡ï¼å·²ç¡®è®¤ï¼ > ç¶æï¼**å·²è·ç¨æ·ç¡®è®¤ï¼2026-09-19ï¼**ï¼Â§6 äºä¸ªå£å¾å ¨é¨æç»è®ºï¼æ §11 顺åºå®æ½ã > æ¥æï¼2026-09-19 > ç¸å ³ææ¡£ï¼`æºè½è´¨æ£æ¥åå¹³å°-æ¹æ¡è®¾è®¡.md`ï¼21 ç« æ»æ¹æ¡ï¼ã`qc_report_ai_import_frontend_integration.md`ã`qc_report_instance_frontend_integration.md`ã`æºè½è´¨æ£æ¥åå¹³å°-å¼åè¿åº¦.md` --- ## é¶ãç¨æ·ç¡®è®¤ç»è®ºï¼2026-09-19ï¼æ¬æ¬¡å®æ½ä¾æ®ï¼ | ç¼å· | é®é¢ | ç¨æ·ç»è®º | è½å°å½±å | |---|---|---|---| | §6.1 | æ¥åçåæ ¼ç»è®ºç±è°å® | **A** | `ReportFields` å¢ `qcResult` / `qcResultText`ï¼éä¼ MES åå¤å®ï¼å¼æé项å¤å®ç §å¸¸ç®ï¼æ¥å级ç»è®ºä»¥è´¨æ£å为å | | §6.3 | ä¸ä¸é为空 | **çææ¶å¼¹æç¤º** | ååºå¸¦å `undecidableCount`ï¼åç«¯å¿ é¡»æç¤ºãN 项å 缺å°è§æ ¼ä¸ä¸é被å¤ä¸ºå¾ å¤å®ã | | §6.2 | åªäºææ è¿æ¥å | **å ¨é¨è¿** | 䏿¥ `judge_flag`ãä¸ç»è¡è¡¨è¡¥åï¼è´¨æ£åè¡è¡¨æä»ä¹å°±åºä»ä¹ | | §6.4 | 夿 ·å / åææ 夿¬¡å½å ¥ | **ä¼** | åæ¹æ¡ **A**ï¼ä¸ä¸ª (æ ·å, ææ ) ç»å = ä¸è¡æ£éªé¡¹ï¼æ ·åå·åè¿ `remark`ï¼ä¸ä¸¢æ°æ® | | §7.3 | 模æ¿ç±»åä¸è´¨æ£åä¸ç¬¦ | **åªè½é对åºç±»å模æ¿** | å端 `105_005` æç»ï¼åç«¯éæ¨¡æ¿æ¶æ `reportType` è¿æ»¤ï¼è®©ç¨æ·éä¸å°é | | §6.5 | ä¸å¼ è´¨æ£åè½å¦åºå¤ä»½æ¥å | **å 许** | 忥è¿åå表ï¼UI å±ç¤ºãæ¥çæ¥å (N)ã | > â §6.3 çãå è¡¥æ°æ®ã䏿¥ï¼Â§11 æ¥ 1ï¼**ç¨æ·æªé**ï¼å³ä¸å¼ºå¶è¡¥æ°æ®ï¼ä½åºéç°åçæ£éªæ¨¡æ¿ä¸ä¸éå ¨ä¸ºç©ºï¼ > ç §ç°ç¶çæä¼æ´ä»½ãå¾ å¤å®ãã**è½å°æ¶é `undecidableCount` æç¤ºå¦å®åç¥ç¨æ·**ï¼ä¸ä»£å ¶è¡¥æ°æ®ã --- ## ä¸ãæ¬ææ¡£è¦è§£å³çé®é¢ ### 1.1 å·²å®å£å¾ï¼ç¨æ·å·²ææ¿ï¼ä¸åè®¨è®ºï¼ | å³ç | éæ© | |---|---| | æ¥å模æ¿ä¸è´¨æ£åçå ³èæ¹å¼ | **æ¯æ¬¡çææ¶äººå·¥éæ¨¡æ¿**ãä¸å¼å ¥ãé»è®¤æ¨¡æ¿ãæ¦å¿µ | | ç¬¬ä¸æ¥åä»ä¹ | **å åºè¿ä»½æ¹æ¡ææ¡£å¾ 确认**ï¼ç¡®è®¤ååå¨ä»£ç | ### 1.2 ç¨æ·åè¯ > æ¥å模æ¿è®¾è®¡è¿æ¯æ²¡æåè´¨æ£å ³èå§ï¼ç°å¨ä¸ç¥éæä¹å¯¼åºæ°æ® **è¿ä¸ªå¤ææ¯å¯¹çã** ç°ç¶ä¸æ¯ãå ³èåå¾ä¸å¥½ãï¼èæ¯**æ ¹æ¬æ²¡æå ³è**ã --- ## äºãç°ç¶æ ¸å¯¹ï¼ç»è®ºï¼æ²¡æ¥ä¸ï¼ä½æå°åºï¼ ### 2.1 å·²ç»æç | è½å | ä½ç½® | ç¶æ | |---|---|---| | æ¥åæ¨¡æ¿ CRUD + çæ¬ç®¡ç | `yudao-module-qcreport` | å¯ç¨ | | GrapesJS è®¾è®¡å¨ + 12 个质æ£ç»ä»¶ | å端 `views/mes/qc/report/template/designer` | å¯ç¨ | | AI å¯¼å ¥ï¼ä¸ä¼ æä»¶ â æ¨¡æ¿èç¨¿ï¼ | `service/aiimport/**` | å¯ç¨ï¼54/54 èè°å·²è¿ | | æå¡ç«¯æ¸²æå¼æ + å¤å®å¨ + è§å弿 | `engine/**` | å¯ç¨ï¼ä¸å端éå对æ | | åºä»¶ï¼æ¸²æ + è½åº + å»ç»å¿«ç § | `POST /qc-report/instance/generate` | **å¯ç¨ï¼ä½æ°æ®å¿ é¡»ç±è°ç¨æ¹ä¼ å ¥** | | å¯¼åº PDF + 彿¡£éä»¶ | `POST /qc-report/instance/pdf` | å¯ç¨ | | å®ä¾å表 / 详æ / å é¤ | å端 `views/mes/qc/report/instance` | å¯ç¨ï¼#45 è½å°ï¼ | ### 2.2 缺çä¸å | # | 缺ä»ä¹ | è¯æ® | |---|---|---| | 1 | **å端æ å°**ï¼è´¨æ£å â `ReportContext` | å ¨é¡¹ç®æä¸å° `MesQcReportApi`ï¼`yudao-module-mes/pom.xml` ä¸ä¾èµ qcreportï¼qcreport è¿ `-api` 忍¡å齿²¡æ | | 2 | **åç«¯å ¥å£**ï¼ä¸ä¸ªãç»æè¿å¼ è´¨æ£åçæ¥åãçæ¥å£ | åªæ `/qc-report/instance/generate`ï¼ä¸å®è¦æ±è°ç¨æ¹ææ´ä¸ª `ReportContext` æ¼å¥½ä¼ è¿æ¥ | | 3 | **åç«¯å ¥å£**ï¼è´¨æ£å页é¢ä¸çãçææ¥åãæé® | å端åªè°äºå®ä¾ç page / get / snapshot / pdf / deleteï¼**`instance/generate` ä¸ `instance/preview` åç«¯ä»æªè°ç¨** | `QcReportInstanceController` ç类注éèªå·±åçï¼ > è¿éæ¯ãæ¨¡æ¿ â æ¥åãç对å¤å ¥å£â¦â¦**æ°æ®ç±è°ç¨æ¹ä¼ å ¥ï¼æ¬æ¨¡åä¸è®¤è¯ä»»ä½ä¸å¡åæ®ã** æä»¥ä½ å¨çé¢ä¸æ¾ä¸å°åºå£æ¯æ£å¸¸çââ**ç°å¨è¦åºä¸ä»½æ¥åï¼åªè½æ¿ curl ææ¼ JSON**ã ### 2.3 å°åºå ¶å®çå¥½äº | å·²æç设计 | ä½ç½® | 说æ | |---|---|---| | 模æ¿å£°æèªå·±æå¡åªç±»è´¨æ£ | `qc_report_template.report_type` | ç¨çå°±æ¯ MES ç `MES_QC_TYPE` åå ¸ï¼IQC/IPQC/OQC/RQCï¼ï¼**ä¸è´¨æ£ååä¸å¥æä¸¾** | | å®ä¾å¯åæ¥ä¸å¡åæ® | `qc_report_instance.business_id` / `business_type` | åæ®µæ³¨éåçå°±æ¯ãä¸å¡åæ®ç¼å·ï¼å¦è´¨æ£å IDããä¸å¡åæ®ç±»åï¼å¦ mes_qc_iqcã | | åé¡µæ¯ææåæ®åæ¥ | `QcReportInstancePageReqVO.businessType` / `businessId` | æ¥è¯¢æ¡ä»¶**å·²ç»å好äº**ï¼`GET /qc-report/instance/page?businessType=mes_qc_iqc&businessId=1` ç´æ¥å¯ç¨ | **æ¢å¥è¯è¯´ï¼å ³èé®ã忥æ¡ä»¶å½åé½è®¾è®¡å¥½äºï¼åªæ¯æ²¡æä»»ä½ä»£ç å»ç¨å®ä»¬ã** ### 2.4 ä¸¤ä¸ªãæ¨¡æ¿ãä¸è¦æ·· | | æ£éªæ¨¡æ¿ | æ¥åæ¨¡æ¿ | |---|---|---| | 表 | `mes_qc_template` | `qc_report_template` | | å½å±æ¨¡å | MES | qcreport | | 管ä»ä¹ | **æ£ä»ä¹**ï¼æ£éªææ æ¸ åãå¤å®ä¸ä¸éãæ£æµæ¹æ³ | **é¿ä»ä¹æ ·**ï¼çé¢ãç»ä»¶ãç»å®ãå¤å®è§åãçº¸å¼ | | è°ç»´æ¤ | è´¨éå·¥ç¨å¸ | æ¥å设计人å | | åç«¯é¡µé¢ | MES è´¨æ£ â æ£éªæ¨¡æ¿ | MES è´¨æ£ â æ¥åæ¨¡æ¿ | æ¬æ¹æ¡**ä¸åå¹¶ãä¸è¿ç§»**è¿ä¸¤ä¸ªæ¦å¿µãä¸ä¸ªè´¨æ£åå¼ç¨ä¸ä¸ªæ£éªæ¨¡æ¿ï¼`mes_qc_iqc.template_id`ï¼ï¼çææ¥åæ¶äººå·¥åéä¸ä¸ªæ¥å模æ¿ââ两è ç¬ç«ã > **妿³¨**ï¼MES è´¨æ£å**å·²ç»æ** Excel 导åºï¼`GET /mes/qc-iqc/export-excel` çåç±»ï¼æé `mes:qc-*-:export`ï¼ï¼å¯¼åºçæ¯åºå®åç `.xls`ï¼åå· / ç©æ / æ¹å· / æ£æµæ¥æ / æ£éªææ / å¤å®ä¾æ® / æ£éªç»æ / æ£éªäºº / å®¡æ ¸äºº / 夿³¨ï¼è§ `MesQcQualityExportVO`ï¼ã飿¯ãæ°æ®è¡¨å¯¼åºãï¼ä¸æ¬æ¹æ¡çãæè®¾è®¡å¥½ç模æ¿åºæ¥åãæ¯ä¸¤æ¡ç¬ç«ç线ï¼äºä¸æ¿ä»£ã --- ## ä¸ãä¸å¼ çå®çè´¨æ£åé¿ä»ä¹æ · > 以ä¸å ¨é¨æ¥èªå½åæ°æ®åºå®æµï¼`ruoyi-vue-pro` åºï¼2026-09-19ï¼ï¼ä¸æ¯æ¨æµã > åºéç®ååªæ **1 å¼ IQC å**ï¼IPQC/OQC/RQC å为 0 æ¡ï¼ï¼æä»¥ä¸é¢è¿å¼ 忝å¯ä¸çæ ·æ¬ã ### 3.1 å头 `mes_qc_iqc`ï¼id=1ï¼ | åæ®µ | å¼ | |---|---| | code | `IQC_20260917001` | | name | `ç²¾èæ¥ææ£éª-0917` | | template_id | 6ï¼æ£éªæ¨¡æ¿ãæ£ä¸éæ£éªï¼GB/T 6027ï¼ãï¼ | | item_id | 2ï¼ç©æãç²¾èãï¼ | | vendor_id / source_doc_type / source_doc_code | **å ¨ä¸º NULL** | | vendor_batch | **NULL** | | received / check / qualified / unqualified_quantity | 100 / 100 / 100 / 0 | | **check_result** | **NULL** | | inspect_date | 2026-09-17 | | inspector_user_id | 1 | | **status** | **0ï¼è稿ï¼** | ### 3.2 ä¸å±æ°æ®ç»æ ``` mes_qc_iqc (1 æ¡) è´¨æ£å主ä½ï¼å¯¹æ¹ãæ°éãç»è®ºãæ¥æãæ£éªäºº âââ mes_qc_indicator_result (1 æ¡) æ ·åè®°å½ï¼qc_id + qc_type å ³è主ä½ï¼å¸¦æ ·åç¼å· code âââ mes_qc_indicator_result_detail (14 æ¡) 宿µå¼ï¼indicator_id + value âââ mes_qc_iqc_line (16 æ¡) å¤å®ä¾æ®ï¼indicator_id + æ åå¼ + ä¸ä¸éï¼æ³¨æï¼ä¸å«å®æµå¼ï¼ ``` ### 3.3 宿µå¼ä»åªæ¥ï¼å ³é®ï¼ **宿µå¼ä¸å¨ `mes_qc_iqc_line` é**ï¼å¨ `mes_qc_indicator_result_detail.value`ï¼å符串ï¼ã `mes_qc_iqc_line` åªæ¿è½½**å¤å®ä¾æ®**ï¼`standard_value`ã`max_threshold`ã`min_threshold`ã`check_method`ã`tool`ãç¼ºé·æ°éã æä»¥æé æ¥åéçä¸è¡æ£éªé¡¹ï¼éè¦**æè¡è¡¨ä¸ç»ææç»æ `indicator_id` æ¼èµ·æ¥**ã ### 3.4 宿µæç¤ºäºå个ã䏿¥ä¸ç¥éãçäºå® #### äºå® 1ï¼16 æ¡æ£éªè¡éï¼åªæ 13 æ¡æå®æµå¼ | ææ | ç»æå¼ç±»å | ææ å®æµå¼ | |---|---|---| | æ°´å / é ¸å«é / æ£ä¸éå«é | **NULL** | **æ ** | | å¤è§ãè²åº¦ãæ æ¶²æµåº¦ cãæ¶èæ»´å®æ¶²ä½ç§¯ Vãè¯æ ·è´¨é m Ã2ãç§°éç¶ m0ãè¯æ ·+ç§°éç¶ m1ãé ¸å«éç»æ w3ãæ°´åç»æ wãæ£ä¸éå«é /%ãå¯åº¦ãæå ç | 1(æµ®ç¹) / 3(ææ¬) | æ | é£ 3 个 `result_type=NULL`ãæ¢æ ä¸ä¸éåæ å®æµå¼çæ¯**åç»æ é¢é¡¹**ï¼ãæ°´åããé ¸å«éããæ£ä¸éå«éãæ¯ç»æé¡¹çå°æ é¢ï¼ã #### äºå® 2ï¼åä¸ä¸ªææ å¯ä»¥æå¤æ¡å®æµå¼ `é ¸å«éç»æ w3`ï¼indicator 30ï¼å¨åä¸ä»½æ ·å䏿 **ä¸¤æ¡æç»ï¼0.09 ä¸ 0.095**ã ç®æ¯èªæ´½ï¼16 è¡ â 3 个åç»é¡¹ = 13 个æå®æµå¼çææ ï¼13 + 1 æ¡éå¤ = 14 æ¡æç»ï¼æ£å¥½å¯¹ä¸ `mes_qc_indicator_result_detail` ç 14 æ¡ã â æ å°ä¸è½å设ãä¸ä¸ªææ ä¸ä¸ªå¼ãã #### äºå® 3ï¼ä¸ä¸é卿ºå¤´å°±æ¯ç©ºçï¼ä¸åªæ¯æ²¡å¤å¶ | ä½ç½® | standard_value | ä¸ä¸é | |---|---|---| | `mes_qc_template_indicator`ï¼æ¨¡æ¿ï¼16 è¡ï¼ | **å ¨ NULL** | **å ¨ NULL** | | `mes_qc_iqc_line`ï¼åæ®ï¼16 è¡ï¼ | **å ¨ NULL** | **å ¨ NULL** | æä»¥ä¸æ¯ãçæåæ®æ¶ä¸¢äºä¸ä¸éãï¼èæ¯**åæ ¹æ²¡å½**ã **åæï¼æéè¦ï¼**ï¼æ¥åå¤å®å¨çå项å¤å®ä¼å 级æ¯ãé项è§å â è§æ ¼ä¸ä¸é â æ°æ®èªå¸¦ç»æãï¼`ReportEvaluator.evaluateByLimit`ï¼ãä¸ä¸é为空 â å两æ¡é½è½ç©º â **æ¯ä¸é¡¹é½åæãå¾ å¤å®ã**ï¼è¿è `passCount != total` â **æ¥åç»è®ºä¹æ¯ãå¾ å¤å®ããåæ ¼ç空ç½**ã **å³ï¼ç°å¨æé¾è·¯æ¥éï¼åºæ¥çæ¥å伿¯ä¸æ´é¡µãå¾ å¤å®ããç¨æ·ä¼ä»¥ä¸ºåè½åäºã** #### äºå® 4ï¼ææ¬åææ å 夿° | `mes_qc_indicator.result_type` | æ°é | è½å¦è¢«åºé´å¤å® | |---|---|---| | 3 = ææ¬ | **19** | ä¸è½ | | 1 = æµ®ç¹ | 12 | è½ï¼ä½éä¸ä¸éï¼ | | NULLï¼åç»é¡¹ï¼ | 3 | ä¸éç¨ | å¤å½¢å¦ãå¤è§ = æ¾æ¸ éææ¶²ä½ï¼æ æºæ¢°æè´¨ãæ¯ææ¬å¼ï¼æ²¡æä¸ä¸éï¼å¤å®å¨ç»ä¸åº PASS/FAILã #### äºå® 5ï¼`judge_flag`ï¼æ¯å¦åä¸åæ ¼å¤å®ï¼å¨åæ®ä¾§ä¸¢äº | 表 | æ¯å¦æ `judge_flag` | |---|---| | `mes_qc_template_indicator` | **æ**ï¼`tinyint`ï¼æ³¨éãæ¯å¦åä¸åæ ¼å¤å®ï¼1=æ¯ï¼0=å¦ãï¼ | | `mes_qc_iqc_line` | **没æè¿ä¸å** | æ¨¡æ¿ 6 é 7 项 `judge_flag=1`ã9 项 `judge_flag=0`ãä½çæåæ®åè¿ä¸ªä¿¡æ¯**ä¸å¯æ¢å¤**ââåæ®è¡åªå© `indicator_id / æ åå¼ / ä¸ä¸é / ç¼ºé·æ°é / 夿³¨`ã â æ¥åè¥æ³ãåªç»è®¡åä¸å¤å®ç项ãï¼**ä¸è½åªè¯»åæ®ï¼å¿ 须忥æ£éªæ¨¡æ¿**ï¼æè ç»åæ®è¡å ä¸åï¼æ¹è¡¨ï¼ã #### äºå® 6ï¼è¡åºä¿¡æ¯ä¹å¨åæ®ä¾§ä¸¢äº æ¨¡æ¿æ `sort_order`ï¼10/20/40/50/60/70 è¿æ ·çåç»æåºï¼ï¼åæ®è¡**没æ**ãæä»¥æ¥å鿣éªé¡¹ç顺åºåªè½æ `line.id`ï¼ä¸æ£éªæ¨¡æ¿ä¸çæå顺åºå¯è½ä¸ä¸è´ã --- ## åãç®æ æµç¨ ``` è´¨æ£åå表 / 详æ â ââ ç¹ãçææ¥åã â â â ââ 1. 鿥忍¡æ¿ï¼äººå·¥éï¼å·²å®å£å¾ï¼ â â ââ å表æ qc_report_template.report_type = 该质æ£ç±»å è¿æ»¤ â â â ââ 2. åç«¯ï¼æè´¨æ£åä¸»ä½ + è¡æç» + æ ·å宿µå¼ â æ¼æ ReportContext â â â ââ 3. å端ï¼è°æ¢æ instanceService.generateï¼æ¸²æ + è½åº + å»ç»å¿«ç §ï¼ â â ââ businessId = è´¨æ£å IDï¼businessType = mes_qc_iqc / ipqc / oqc / rqc â â â ââ 4. è¿åæ¥åå®ä¾ ID 䏿¥åç¼å· â ââ æ¥åå·²çæ â å表/详æ å¯ç´æ¥è·³è½¬æ¥åå®ä¾ï¼æ businessType + businessId åæ¥ï¼æ¥è¯¢æ¡ä»¶å·²å°±ç»ªï¼ â ââ æ¥åå®ä¾é¡µ â ãå¯¼åº PDFãâ 彿¡£éä»¶åºï¼æ¢æè½åï¼ä¸å¨ï¼ ``` **å¯ä¸è½åºç¹æ¯æ¢æç模æ¿çæ¬ + å®ä¾**ï¼æ¬æ¹æ¡ä¸æ°å¢ä»»ä½ä¿åè·¯å¾ã --- ## äºãåæ®µæ å°ï¼æ ¸å¿ï¼ ### 5.1 æ¥åçº§åæ®µï¼`ReportContext.report` | `ReportContext` åæ®µ | åç±»åæ®æ¥æº | 夿³¨ | |---|---|---| | `reportNo` | **ä¸åè´¨æ£å code**ï¼ç± `ReportNoGenerator` æ `QR+æ¥æ+æµæ°´` çæ | ç¨æ·å¯å¨çææ¶è¦ç | | `reportName` | `name`ï¼æ£éªååç§°ï¼ | åç±»é½æ | | `sampleNo` | `mes_qc_indicator_result.code`ï¼æ ·åç¼å·ï¼å®æµ `IR_20260917001`ï¼ | 夿 ·åè§ Â§6.4 | | `productCode` | `mes_md_item.code`ï¼ç» `item_id`ï¼ | 宿µå¼ `2` | | `productName` | `mes_md_item.name` | 宿µå¼ `ç²¾è` | | `spec` | `mes_md_item.specification` | **宿µä¸º NULL**ï¼æ¥åä¸ä¼æ¯ç©º | | `batchNo` | IQC â `vendor_batch`ï¼OQC / RQC â `batch_code`ï¼**IPQC â æ æ¤å段** | 宿µ IQC ç `vendor_batch` 为 NULL | | `workOrderNo` | IPQC â `work_order_id` æ¥å·¥åç¼å·ï¼å ¶ä½ä¸ç±»æ | 建议åéç¨ `source_doc_code` | | `inspectType` | ç±è´¨æ£ç±»åæ å°ï¼IQCâæ¥ææ£éª / IPQCâè¿ç¨æ£éª / OQCâåºè´§æ£éª / RQCâéè´§æ£éª | å `MesQcTypeEnum` ç name | | `inspector` | `inspector_user_id` â `system_users.nickname` | åç±»é½æ | | `inspectDate` | `inspect_date` | åç±»é½æ | | `department` | **åç±»åæ®é½æ²¡æé¨é¨å段** | è§ Â§6.1 å³ç | | `customerName` | OQC â `client_id` æ¥ CRM 客æ·åï¼å ¶ä½ä¸ç±»æ | | | `supplierName` | IQC â `vendor_id` æ¥ SRM ä¾åºååï¼å ¶ä½ä¸ç±»æ | 宿µä¸º NULL | | `result` / `resultText` / `conclusion` / `passRate` / `total` / `passCount` / `failCount` | **ç± `ReportEvaluator` ç®åºå¹¶è¦ç**ï¼è°ç¨æ¹ä¸å¿ å¡« | è§ Â§6.1 | ### 5.2 æ£éªé¡¹ï¼`ReportContext.inspectionItems[]` | `InspectionItem` åæ®µ | æ¥æº | 夿³¨ | |---|---|---| | `index` | çææ¶æé¡ºåº 1..N | | | `itemCode` | `mes_qc_indicator.code` | | | `itemName` | `mes_qc_indicator.name` | | | `standardValue` | `mes_qc_xxx_line.standard_value` | BigDecimal â å»å°¾é¶å符串ï¼**宿µå ¨ NULL**ï¼ | | `actualValue` | `mes_qc_indicator_result_detail.value`ï¼Stringï¼ | ç» `result_id` â `result.qc_id` å ³è | | `unit` | `mes_qc_xxx_line.unit_measure_id` â `mes_md_unit_measure.name` | **注æ**ï¼æ¨¡æ¿ä¾§ä¹æ `unit_measure_id`ï¼å®æµä¸¤è 齿¯ NULL | | `upperLimit` | `mes_qc_xxx_line.max_threshold` | Doubleï¼**宿µå ¨ NULL**ï¼ | | `lowerLimit` | `mes_qc_xxx_line.min_threshold` | Doubleï¼**宿µå ¨ NULL**ï¼ | | `result` / `resultText` | ç± `ReportEvaluator` åå | ä¸å MES ç `check_result` | | `remark` | `line.remark` æ `detail.remark`ï¼**两å¤é½æ**ï¼ | éå®å£å¾ï¼è§ §6.5 | > â **ååä¸ä¸è´çå**ï¼åæ®è¡è¡¨ç¨ `max_threshold` / `min_threshold`ï¼æ£éªæ¨¡æ¿è¡¨ç¨ `threshold_max` / `threshold_min`ãåæ å°æ¶å«å¼æ··ã ### 5.3 åç±»åæ®çå·®å¼ | | IQC æ¥ææ£éª | IPQC è¿ç¨æ£éª | OQC åºè´§æ£éª | RQC éè´§æ£éª | |---|---|---|---|---| | 主ä½è¡¨ | `mes_qc_iqc` | `mes_qc_ipqc` | `mes_qc_oqc` | `mes_qc_rqc` | | è¡è¡¨ | `mes_qc_iqc_line` | `mes_qc_ipqc_line` | `mes_qc_oqc_line` | `mes_qc_rqc_line` | | **è¡è¡¨åç»æ** | **å表å®å ¨åæ**ï¼13 åä¸è´ï¼ä» ç¶å¤é®ååä¸åï¼`iqc_id` / `ipqc_id` / `oqc_id` / `rqc_id`ï¼ | | | | | å¯¹æ¹ | `vendor_id`ï¼SRM ä¾åºåï¼ | æ | `client_id`ï¼CRM 客æ·ï¼ | æ | | æ¹æ¬¡ | `vendor_batch` | **æ ** | `batch_code` | `batch_code` | | çäº§å ³è | æ | `work_order_id` / `task_id` / `workstation_id` / `process_id` | æ | æ | | æ¥æ | `receive_date` + `inspect_date` | `inspect_date` | `out_date` + `inspect_date` | `inspect_date` | | æ°é | æ¥æ¶ / æ£æµ / åæ ¼ / ä¸åæ ¼ | æ£æµ / åæ ¼ / ä¸åæ ¼ + å·¥åº / æåº / å ¶ä»åº | åºè´§ / æ£æµ / åæ ¼ / ä¸åæ ¼ + æä½æ£æµæ° / æå¤§ä¸åæ ¼æ° | æ£æµ / åæ ¼ / ä¸åæ ¼ | | èªæåç±» | æ | `type`ï¼`MES_IPQC_TYPE`ï¼ | æ | `type`ï¼`mes_rqc_type`ï¼ | | ç»è®º | `check_result`ï¼åæ ¼ / ç¹é / ä¸åæ ¼éè´§ / ä¸åæ ¼æ¥åºï¼ | å | å | å | **æ¶è·**ï¼è¡è¡¨å表åæï¼æå³ç**æ å°é»è¾å¯ä»¥åªåä¸ä»½**ï¼ç¨æ³åæç»ä¸æ¥å£æ¿æ¥ï¼ä¸å¿ ååå¥ã --- ## å ãå¿ é¡»å ææ¿çå£å¾ï¼æéè¦æ§æåºï¼ ### 6.1 ãæéè¦ãæ¥åçåæ ¼ç»è®ºç±è°å®ï¼ **å²çªç¹**ï¼ | | MES è´¨æ£å | æ¥å弿 | |---|---|---| | ç»è®ºåå¼ | `check_result`ï¼åæ ¼(1) / **ç¹é(2)** / ä¸åæ ¼éè´§(3) / ä¸åæ ¼æ¥åº(4) | `QualityResult`ï¼**åªæ PASS / FAIL**ï¼å¤å ãå¾ å¤å®ãå ä½ | | è°å¡« | **人填ç** ââ 人å¯è½å¤ãç¹éãï¼å¯å ¥åºä½éåæ ¼ï¼ | **ç®åºæ¥ç** ââ ä¸ä¸éæè§å | | ä¼ä¸ä¼è¢«è¦ç | â | `ReportEvaluator` æ æ¡ä»¶æ§è¡ `ReportFields.applyResult(verdict)`ï¼**è¦ç**è°ç¨æ¹ä¼ å ¥ç result / resultText / conclusion | **å¿ ç¶åºç°ççç¾**ï¼è´¨æ£åå¤ãç¹éãï¼æ¥å弿åªè®¤ PASS/FAILï¼å¤åç®åºãä¸åæ ¼ããæ¥åä¸è´¨æ£åç»è®ºææ¶ï¼å®¢æ·æ¿å°æ¥åä¼è´¨çã **ä¸ä¸ªéæ©**ï¼ | æ¹æ¡ | åæ³ | 代价 | |---|---|---| | **Aï¼æ¨èï¼** | æ¥åç»è®ºä»¥**弿**为åï¼MES åå¤å®å¦åå°ä¸ä¸ª**æ°å¢çæ¥åçº§åæ®µ**ï¼å¦ `report.qcResult` / `report.qcResultText`ï¼ï¼æ¨¡æ¿éå¯ç¨ `{{report.qcResult}}` æ¾ç¤ºãè´¨æ£ååå¤ï¼ç¹éã | **卿°æ®å¥çº¦**ï¼è¦æ¹ `ReportFields` + `ReportContext.reportMap()` + å端 `engine/context.ts`ï¼ä¸ `FrontendConformanceTest` ä¼éå对æï¼é忥å»ç»äº§ç© | | B | åªåç°å¼æå¤å®ï¼**å®å ¨ä¸ä½ç°** MES ç `check_result` | é¶æ¹å¨ï¼ä½æ¥åå¯è½ä¸è´¨æ£åç»è®ºä¸ä¸è´ï¼ç¹éè¢«å¤æä¸åæ ¼ï¼ï¼ä¸ç¨æ·æ ä»å¯è§ | | C | **以质æ£å为å**ï¼è®©å¼æå«å¤ | å¼ææ²¡æãè·³è¿å¤å®ãå¼å ³ãè¦ä¹ä¸ä¼ ä¸ä¸éï¼ååé¡¹å ¨åå¾ å¤å®ï¼ãè¦ä¹æ¯ä¸ªæ¥å模æ¿é½é report 级è§åââ**ä¾èµäººå·¥é 模æ¿ï¼å®¹ææ¼**ï¼ä¸è½åªé 代ç ä¿è¯ | > æçå»ºè®®æ¯ **A**ãçç±æ¯ãç¹éãè¿ç±»ä¸é´ææ¯è´¨éä¸å¡ççå®ç¶æï¼æ¥åä¸ç´æ¥æ¹æä¼é ææ¥åä¸åæ®å¯¹ä¸ä¸ï¼è B çä¸ä¸è´æ¯**éé»**çï¼ç¨æ·ä¸ä¼åç°ï¼é®é¢ä¼æµå°å®¢æ·é£è¾¹ãA ç代价æ¯å¯æ§çââåªæ¯å ä¸¤ä¸ªåæ®µï¼ä¸æ¯æ¹å¤å®é»è¾ã > > ä½å¦æå¸æ**è¿ä¸è½®é¶å¥çº¦æ¹å¨ãå è·éé¾è·¯**ï¼å¯ä»¥å æ **B** ä¸çº¿å¹¶æç¡®åç¥ãæ¥åç»è®ºä»¥æ¥å模æ¿çå¤å®è§å为åï¼ä¸è´¨æ£åç人工å¤å®å¯è½ä¸åãï¼æ A çä½ä¸ä¸è½®ã ### 6.2 ãå³å®æ¥åé¿ä»ä¹æ ·ãåªäºææ 该åºç°å¨æ¥åä¸ï¼ 宿µé£å¼ å 16 è¡éï¼**9 è¡æ¯ `judge_flag=0`**ï¼å ¶ä¸å å«ï¼ - 3 个åç»æ é¢é¡¹ï¼æ°´å / é ¸å«é / æ£ä¸éå«éï¼æ 宿µå¼ï¼ - ä¸é´è®¡ç®éï¼æ æ¶²æµåº¦ cãæ¶èæ»´å®æ¶²ä½ç§¯ Vãè¯æ ·è´¨é m Ã2ãç§°éç¶ m0ãè¯æ ·+ç§°éç¶ m1 **ãæ¶èæ»´å®æ¶²ä½ç§¯ãåãç§°éç¶è´¨éãæ¯åéªå®¤å é¨ç计ç®è¿ç¨ï¼ä¸è¯¥åºç°å¨ç»å®¢æ·ççæåæ£éªæ¥åä¸ã** ä¸ä¸ªéæ©ï¼ | æ¹æ¡ | åæ³ | |---|---| | **Aï¼æ¨èï¼** | **åªå `judge_flag=1` ç项**ï¼è¯¥æ¨¡æ¿ä¸ 7 项ï¼ä½ä¸ºæ¥åçãæ£éªé¡¹ç®è¡¨ãï¼`judge_flag=0` ç项ä¸è¿æ¥å | | B | å ¨é¨è¿æ¥åï¼åç»é¡¹å½å°æ é¢ãä¸é´é彿ç»è¡ | | C | å ¨é¨è¿æ¥åï¼ä½æ `judge_flag` åæãå¤å®é¡¹ãåãåè项ã两个åºå | **代价ï¼A çé®é¢ï¼**ï¼`judge_flag` **å¨åæ®è¡ä¸ä¸åå¨**ï¼äºå® 5ï¼ï¼å¿ é¡»**忥 `mes_qc_template_indicator`**ï¼ç¨ `mes_qc_iqc.template_id`ï¼ãè¿æä¸¤ä¸ªéæ£ï¼ 1. æ£éªæ¨¡æ¿å¯è½è¢«æ¹è¿ï¼æ¹äº `judge_flag`ï¼ï¼æ¥åä¼è·çåââèæ¥åæ¬åºåºäº**è´¨æ£å彿¶**çå£å¾ã好å¨è´¨æ£åæå»ç»å¿«ç §æºå¶ï¼`dataSnapshot`ï¼ï¼è½åºåä¸åå½±åï¼ä½**çæçé£ä¸å»**åçæ¯æ¨¡æ¿å½åå¼ã 2. è¥è¯¥è´¨æ£åçæ£éªæ¨¡æ¿è¢«å é¤ï¼åæ¥æ¿ä¸å° â éè¦å åºï¼éåå°æ¹æ¡ B ææ¥éï¼ã æ¿ä»£åæ³ï¼**ç»åå¼ è¡è¡¨åå ä¸å `judge_flag`**ï¼çæåæ®æ¶ä»æ¨¡æ¿å¸¦è¿æ¥ãè¿æ ·ãåæ®èªå å«ãï¼ä¸åä¾èµåæ¥ãéè¦æ¹ 4 å¼ è¡¨ + çæåæ®ç代ç ï¼å±äº MES ä¾§æ¹å¨ï¼ã > **éè¦ä½ å®**ï¼(A) 忥æ£éªæ¨¡æ¿ / (Aâ²) ç»è¡è¡¨å åè®©åæ®èªå å« / (B) å ¨é¨è¿æ¥åã ### 6.3 ãåç½®æ°æ®é®é¢ãä¸ä¸é为空 â æ¥åä¼å ¨ãå¾ å¤å®ã **è¿ä¸ä»£ç æ å ³ï¼æ¯æ°æ®é®é¢ï¼ä½ä¼ç´æ¥å³å®ç¨æ·çå°çææã** 宿µï¼æ£éªæ¨¡æ¿ä¸è´¨æ£åè¡ç `standard_value` / ä¸ä¸é**å ¨é¨ä¸º NULL**ãæ¥åå¤å®å¨çä¼å 级æ¯ãé项è§å â è§æ ¼ä¸ä¸é â æ°æ®èªå¸¦ç»æãï¼å两æ¡é½è½ç©º â **æ¯ä¸é¡¹é½æ¯ãå¾ å¤å®ã**ï¼æ¥åç»è®ºä¸ºãå¾ å¤å®ããåæ ¼ç空ç½ã ä¸ä¸ªéæ©ï¼ | æ¹æ¡ | åæ³ | |---|---| | **Aï¼æ¨èï¼** | **å è¡¥æ°æ®åä¸åè½**ï¼æè¿å¥æ£éªæ¨¡æ¿çä¸ä¸éå½è¿å»ï¼è³å°ç» 7 个 `judge_flag=1` ç项å½ï¼ãåè½ç §åï¼ä½**éªæ¶æ¶å¿ é¡»ç¨å½äºä¸ä¸éçæ¨¡æ¿**ï¼å¦åçå°çå ¨æ¯ãå¾ å¤å®ãï¼ä¼è¯¯å¤ä¸ºåè½å | | B | åè½å ä¸ï¼çé¢æç¡®æç¤ºãæ N 项å 缺å°è§æ ¼ä¸ä¸éèæ æ³å¤å®ãï¼å¼å¯¼ç¨æ·å»è¡¥ | | C | é æ¥å模æ¿çå¤å®è§åå åºï¼è§åå¯ä»¥ä»ææ ç¼ç / ææ¬å¼å¤æï¼ | > **建议 A + B åæ¶å**ï¼ä»£ç ç §åï¼ä½å½ãæ ä»»ä½ä¸é¡¹å¯å¤å®ãæ¶ï¼çæç»æéå䏿¡æç¡®æç¤ºââãæ¬æ¬¡æ¥åæ N 项å 缺å°è§æ ¼ä¸ä¸é被å¤ä¸ºãå¾ å¤å®ãï¼è¯·å°æ£éªæ¨¡æ¿ä¸è¡¥å ä¸ä¸éãï¼å¹¶éä¼ å°å端ãè¿æ ·æ¢ä¸åè£ æåï¼ä¹ä¸éé»ã ### 6.4 ä¸ä¸ªè´¨æ£åå¤ä»½æ ·å / å䏿æ 夿¡å®æµå¼ **宿µå°ä¸¤ç§å¤æåº¦**ï¼ 1. å䏿æ 夿¡å®æµå¼ï¼`é ¸å«éç»æ w3` å¨åä¸ä»½æ ·å䏿 0.09 ä¸ 0.095 两æ¡ã 2. 夿 ·åï¼`mes_qc_indicator_result` ç»æä¸æ¯æä¸ä¸ªè´¨æ£åæ N æ¡æ ·åè®°å½ï¼`qc_id` + `qc_type` å ³èï¼ï¼ä½**宿µåºéåªæ 1 æ¡**ï¼æ æ³è¯å®å®é ä¸å¡ä¼ä¸ä¼åºç°ã `InspectionItem` æ¯**æå¹³ä¸å±**ï¼æ æ³è¡¨è¾¾ãæ ·åãè¿ä¸å±ã | æ¹æ¡ | åæ³ | |---|---| | **Aï¼æ¨èï¼** | ä¸ä¸ª (æ ·å, ææ ) ç»å = ä¸è¡æ£éªé¡¹ï¼`sampleNo` æ¼è¿ `itemCode` æ `remark` 以åºåã夿 ·åæ¶æ¥ååé¿ä½ä¿¡æ¯ä¸ä¸¢ | | B | åªå**第ä¸ä»½æ ·å**ï¼å ¶ä½å¿½ç¥å¹¶å¨æ¥å warnings éæç¤º | | C | å䏿æ 夿¡å®æµå¼å**æå䏿¡**ï¼è§ä¸ºå¤æµè¦çï¼ | > **éè¦ä½ 确认ä¸å¡äºå®**ï¼**IQC / IPQC / OQC / RQC å®é ä¼ä¸ä¼å½å¤ä»½æ ·åï¼å䏿æ ä¼ä¸ä¼å½å¤æ¬¡ï¼** è¿å³å®äºé A è¿æ¯ B/Cãç®ååºéåªæ 1 ä»½æ ·åï¼æç»ä¸åºç»è®ºã ### 6.5 å ¶ä½éè¦å®çå°å£å¾ | é®é¢ | 说æ | 建议 | |---|---|---| | `remark` ååªä¸ª | è¡è¡¨ä¸ç»ææç»**齿** `remark` | æ¼æ¥ï¼`line.remark` ä½å¤å®ä¾æ®å¤æ³¨ï¼`detail.remark` ä½å®æµå¤æ³¨ | | ææ¬åææ æä¹å | 19/34 æ¯ææ¬åï¼æ ä¸ä¸é â å¿ ç¶ãå¾ å¤å®ã | æ¥åä¸å¦å¸¸å±ç¤ºææ¬å¼ï¼ç»è®ºæ 注è䏿¯æ¥éï¼éè¦å¤å®å°±é 模æ¿è§å | | æ¥åéçæ£éªé¡¹é¡ºåº | åæ®è¡æ `sort_order`ï¼äºå® 6ï¼ | æ `line.id`ï¼è¥è¦ææ¨¡æ¿é¡ºåºé忥 `mes_qc_template_indicator.sort_order` | | è´¨æ£åä»ä¹æ¶åå è®¸åºæ¥å | åºéå¯ä¸é£å¼ å `status=0`ï¼è稿ï¼ä¸ `check_result=NULL` | **建议ï¼`status != 已宿` æ `check_result` ä¸ºç©ºæ¶æç»**ï¼æ¥ç²¾ç¡®é误ç ï¼è§ §7.3ï¼ï¼ä¸çæåæåæ¥å | | `department` ä»åªæ¥ | åç±»åæ®é½æ²¡æé¨é¨å段 | ç±æ£éªäººæå±é¨é¨å¸¦åºï¼æçç©ºç±æ¨¡æ¿ä¸åæ» | | ä¸å¼ è´¨æ£åè½å¦åºå¤ä»½æ¥å | ç»æä¸å 许ï¼å®ä¾è¡¨æ å¯ä¸çº¦æï¼ | **建议å 许**ï¼åä¸å¼ åç¨ä¸å模æ¿åºä¸åç¨éçæ¥åï¼å é¨ç / 客æ·çï¼ã忥å表èªç¶æ¯æ | --- ## ä¸ãæ¥å£å¥çº¦ ### 7.1 æ°å¢æ¥å£ | æ¹æ³ | è·¯å¾ | 说æ | æéç | |---|---|---|---| | POST | `/qc-report/instance/generate-from-qc` | ç±è´¨æ£åçææ¥å | `qc-report:instance:generate`ï¼æ°å¢ï¼ | | GET | `/qc-report/instance/page-by-business` | æè´¨æ£å忥已çæçæ¥å | `qc-report:instance:query`ï¼å¤ç¨ï¼ | > åæ¥å ¶å®å¯ä»¥ç´æ¥å¤ç¨æ¢æç `GET /qc-report/instance/page?businessType=...&businessId=...`ï¼æ¥è¯¢æ¡ä»¶å·²åå¨ï¼ï¼ > ä¸è¡¨ç `page-by-business` åªæ¯ä¸ºäºè®©å端å°ä¼ ä¸¤ä¸ªåæ°ï¼**å¯éï¼ä¸å¿ è¦**ã ### 7.2 è¯·æ± / ååº **请æ±** `POST /qc-report/instance/generate-from-qc` | åæ° | ç±»å | å¿ å¡« | 说æ | |---|---|---|---| | `qcType` | String | æ¯ | `IQC` / `IPQC` / `OQC` / `RQC`ï¼å³å®è¯»åªå¼ 表 | | `qcId` | Long | æ¯ | è´¨æ£å ID | | `templateId` | Long | æ¯ | **æ¥åæ¨¡æ¿ IDï¼äººå·¥é**ï¼å·²å®å£å¾ï¼ | | `version` | String | å¦ | æ¥å模æ¿çæ¬ï¼ä¸ä¼ ç¨å½åå·²åå¸çæ¬ | | `reportNo` | String | å¦ | æ¥åç¼å·ï¼ä¸ä¼ èªå¨çæ | | `sampleStrategy` | String | å¦ | 夿 ·åå£å¾ï¼è§ §6.4ãé»è®¤ `FIRST` | **ååº** | åæ®µ | ç±»å | 说æ | |---|---|---| | `id` | Long | æ¥åå®ä¾ ID | | `reportNo` | String | æ¥åç¼å· | | `templateVersion` | String | å®é 使ç¨ç模æ¿çæ¬ | | `itemCount` | Integer | æ¬æ¬¡åå ¥çæ£éªé¡¹æ¡æ° | | `undecidableCount` | Integer | **å 缺å°ä¸ä¸é / è§åèãå¾ å¤å®ãç项æ°**ï¼Â§6.3-B çè½å°ï¼ | | `warnings` | String[] | 软æç¤ºï¼è·³è¿çåç»é¡¹ãè¢«æªæçå¤ä½å®æµå¼ãæ ·åçåè | | `errors` | String[] | æ¸²æææ°æ®ç¼ºå£ï¼æ¢ææºå¶ï¼ | ### 7.3 é误ç ï¼æï¼`1_070_105_xxx` æ®µï¼ > â **æ¬èæ¯æ¹æ¡é¶æ®µæç¨¿ï¼å®é å®ç°å·²å离ââ以 `docs/qc_report_generate_from_qc_frontend_integration.md` ä¸ä»£ç 为å**ï¼ > `105_004`ï¼æ¨¡æ¿ä¸åå¨/å·²åç¨ï¼ä¸ `105_007`ï¼æ¨¡æ¿æ å·²åå¸çæ¬ï¼**æªå®ç°**ï¼æ¹ä¸ºå¤ç¨æ¢æé误ç > `1_070_100_000`/`1_070_100_002`/`1_070_101_007`/`1_070_101_008`ãçç±ï¼é£å æ¡ææ¡æ¬æ¥å°±ç²¾ç¡®ï¼ > åé ä¸éåªä¼å¤ä¸¤æ¡ä¼æ¼ç§»ç坿¬ãå®é è½å°çæ¯ä¸é¢æ â çå æ¡ã | ç | åºæ¯ | ææ¡è¦ç¹ï¼éµå® `error-message-precision.md`ï¼ | è½å° | |---|---|---|---| | `105_000` | è´¨æ£ç±»åä¸åæ³ | æææ¶å°çå¼ä¸å 许çåå¼ | â å·²å®ç° | | `105_001` | è´¨æ£åä¸åå¨ | ææç±»å + ID | â å·²å®ç° | | `105_002` | è´¨æ£åæªå®æï¼ä¸è½åºæ¥å | ç»åºå½åç¶æå + 建议å¨ä½ï¼å æåæ®èµ°å®ï¼ | â å·²å®ç° | | `105_003` | è´¨æ£åæªå¤å®ï¼`check_result` ä¸ºç©ºï¼ | ç»åºå½åå¼ + 建议å¨ä½ | â å·²å®ç° | | ~~`105_004`~~ | ~~æ¥å模æ¿ä¸åå¨ / å·²åç¨~~ | ~~æææ¨¡æ¿ ID ä¸ç¶æ~~ | â **æªå®ç°**ï¼å¤ç¨ `1_070_100_000` / `1_070_100_002` | | `105_005` | æ¥å模æ¿çæ¥åç±»åä¸è´¨æ£åä¸ç¬¦ | **ç»åºåæ¹å¼**ï¼æ¨¡æ¿æ¯ IPQCï¼è´¨æ£åæ¯ IQCï¼ | â å·²å®ç°ï¼æ¨¡æ¿æªé ç±»åæ¶å¤ç¨åç ãæ¢ææ¡ï¼ | | `105_006` | è´¨æ£å没æå¯ç¨çæ£éªé¡¹ | 说æåå ï¼æ è¡æç» / å ¨é¨ä¸ºåç»é¡¹ï¼ | â å·²å®ç° | | ~~`105_007`~~ | ~~æ¥åæ¨¡æ¿æ å·²åå¸çæ¬~~ | ~~建议å å»è®¾è®¡å¨åå¸~~ | â **æªå®ç°**ï¼å¤ç¨ `1_070_101_007` / `1_070_101_008` | > **`105_005` 说æ**ï¼æ¨¡æ¿ç `report_type` ä¸è´¨æ£åç±»åä¸ç¬¦æ¶**æç»**ï¼è¿æ¯**å 许ä½è¦å**ï¼ > å·²æ**æç»**è½å°ï¼é²æ¢ IQC ååºæåºè´§æ¥åè¿ç§ä½çº§é误ï¼ï¼å¹¶å¨åç«¯éæ¨¡æ¿æ¶**åªååç±»åçæ¨¡æ¿**ï¼ > è®©ç¨æ·æ ¹æ¬éä¸å°éçã ### 7.4 è½åºçº¦å® | å®ä¾å段 | å¼ | |---|---| | `businessId` | `String.valueOf(qcId)` | | `businessType` | `mes_qc_iqc` / `mes_qc_ipqc` / `mes_qc_oqc` / `mes_qc_rqc` | | `dataSnapshot` | æ¢ææºå¶èªå¨å»ç»ï¼**ä¸å¨** | > â **`businessType` åç¬¦ä¸²å¿ é¡»ä¸ `QcReportInstanceGenerateReqVO` éå·²æç示ä¾å¼å®å ¨ä¸è´**ï¼`mes_qc_iqc` çï¼ï¼ > å¦å忥伿¥ä¸å°ãè¿å个å符串æ¯äºå®ä¸çå¥çº¦ã --- ## å «ãå端èè°æ¹æ¡ ### 8.1 æ¶åé¡µé¢ | é¡µé¢ | æ¹å¨ | |---|---| | MES è´¨æ£ â æ¥ææ£éªï¼IQCï¼å表 / 详æ | å ãçææ¥åãæé® + 鿍¡æ¿å¼¹çª + ãæ¥çæ¥åãå ¥å£ | | MES è´¨æ£ â è¿ç¨æ£éªï¼IPQCï¼ | åä¸ | | MES è´¨æ£ â åºè´§æ£éªï¼OQCï¼ | åä¸ | | MES è´¨æ£ â éè´§æ£éªï¼RQCï¼ | åä¸ | | MES è´¨æ£ â æ¥åå®ä¾ï¼æ¢æï¼ | æ éæ¹å¨ï¼ä½ä¼å¼å§åºç°ç宿°æ® | ### 8.2 æé®ä¸æé | æé® | æéç | ä½ç½® | |---|---|---| | çææ¥å | `qc-report:instance:generate` | è´¨æ£åå表æä½åã详æ 页 | | æ¥çæ¥å | `qc-report:instance:query`ï¼å¤ç¨ï¼ | åä¸ä½ç½®ï¼ä» å¨å·²ææ¥åæ¶æ¾ç¤º | ### 8.3 鿍¡æ¿å¼¹çª | 项 | 说æ | |---|---| | 模æ¿åè¡¨æ°æ®æº | `GET /qc-report/template/page`ï¼å端**å¿ é¡»å¸¦ `reportType` = å½åè´¨æ£ç±»å** çè¿æ»¤æ¡ä»¶ | | å±ç¤ºå段 | 模æ¿åç§°ãç¼ç ãå½åçæ¬ãç¶æ | | è¿æ»¤ | åªå**å¯ç¨**ä¸**æå·²åå¸çæ¬**çæ¨¡æ¿ | | é»è®¤ä¸éä¸ | å·²å®å£å¾æ¯ã人工éãï¼ä¸è¦é¢éï¼é¿å è¯¯ç¹ | | æ å¯ç¨æ¨¡æ¿æ¶ | æç¡®æç¤ºã该æ¥åç±»åä¸è¿æ²¡æå¯ç¨æ¨¡æ¿ï¼è¯·å å°æ¥å模æ¿ä¸è®¾è®¡å¹¶åå¸ãï¼å¹¶ç»åºè·³è½¬å ¥å£ | ### 8.4 çæåçè¡¨ç° | åºæ¯ | è¡¨ç° | |---|---| | çææå | æç¤ºæ¥åç¼å·ï¼æãçææ¥åãæé®æ¢æãæ¥çæ¥åãï¼è¥ `warnings` é空ï¼éæ¡æç¤º | | `undecidableCount > 0` | **å¿ é¡»æç¤º**ï¼ãæ¬æ¬¡æ¥åæ N 项å 缺å°è§æ ¼ä¸ä¸é被å¤ä¸ºãå¾ å¤å®ãï¼è¯·å°æ£éªæ¨¡æ¿ä¸è¡¥å åéæ°çæã | | çæå¤±è´¥ | æ §7.3 çé误ç åæ ·å±ç¤ºåç«¯ææ¡ï¼å端ä¸è¦åå ä¸å±ï¼æ¢æå£å¾ï¼ | | è´¨æ£åæªå®æ | æé®ç½®ç°å¹¶ç»åºåå ï¼ä¸è¦è®©ç¨æ·ç¹äºææ¥é | ### 8.5 è°ç¨æ¶åºï¼å ³é®ï¼ä¿¡æ¯å¸¦å ¥ï¼ ``` æå¼è´¨æ£åå表 â GET /mes/qc-iqc/page ï¼æ¢æï¼ â 坹已宿çåï¼GET /qc-report/instance/page?businessType=mes_qc_iqc&businessId={id} ââ ææ°æ® â æ¾ç¤ºãæ¥çæ¥åãï¼æ æ°æ® â æ¾ç¤ºãçææ¥åã ç¹ãçææ¥åã â GET /qc-report/template/page?reportType=IQC&status=0 ï¼åªåå¯ç¨æ¨¡æ¿ï¼ â ç¨æ·éæ¨¡æ¿ â POST /qc-report/instance/generate-from-qc { qcType, qcId, templateId } â æ¿å° reportId / reportNo / undecidableCount / warnings ç¹ãæ¥çæ¥åã â è·³è½¬æ¢ææ¥åå®ä¾è¯¦æ 页ï¼ç¨ reportIdï¼ ``` **ä¸å¸¦å ¥çä¸è¥¿**ï¼æ¥å模æ¿**ä¸è®°å¿**è´¨æ£åçä»»ä½ä¿¡æ¯ï¼`templateId` æ¯æ¬¡ç±ç¨æ·éï¼å·²å®å£å¾ï¼ï¼è´¨æ£åä¹**ä¸å**æ¥å IDï¼åæ¥é `businessType + businessId` æ¥è¯¢ã ### 8.6 注æäºé¡¹ - **åæ¥æ¯æ¥è¯¢è䏿¯å段**ï¼ä¸è¦å¨è´¨æ£å表ä¸å ãæ¥å IDãåï¼è¿å `file-upload.md` å类精ç¥ââä¸å¡è¡¨ä¸åæ´¾çå¼ç¨ï¼ï¼ç¨å®ä¾è¡¨ç `businessType/businessId` 忥ã - **ä¸ä¸ªè´¨æ£åå¯è½æå¤ä»½æ¥å**ï¼åæ¥è¿åå表ï¼UI è¦è½å±ç¤ºå¤ä»½ï¼å¦ãæ¥çæ¥å (2)ãï¼ã - **æ¥å模æ¿çç±»åè¿æ»¤å¿ é¡»ç±åç«¯ä¼ **ï¼å端ä¹ä¼æ ¡éªï¼Â§7.3 `105_005`ï¼ï¼ä½åç«¯è¿æ»¤è½è®©ç¨æ·æ ¹æ¬éä¸å°éçã - **å»ºè®®åæ¶æ ¡éªè´¨æ£åç¶æ**ï¼`status != 已宿` æ¶æé®ç½®ç°ï¼å«è®©ç¨æ·ç½ç¹ã - **`mes:qc-*-:query` ä¸ `qc-report:instance:generate` æ¯ä¸¤ä¸ªç¬ç«æé**ï¼ä¸ä¸ªæ§å¶è½ä¸è½çè´¨æ£åï¼ä¸ä¸ªæ§å¶è½ä¸è½åºæ¥åãç»è½¦é´çç»é¿åªå¼åè å³å¯ã --- ## ä¹ãæç¡®ä¸åï¼é²èå´èå»¶ï¼ 1. **ä¸åå¹¶ãæ£éªæ¨¡æ¿ãä¸ãæ¥å模æ¿ã**ââ两è èè´£ä¸åï¼æ£ä»ä¹ vs é¿ä»ä¹æ ·ï¼ã 2. **ä¸åãé»è®¤æ¨¡æ¿ã**ââå·²å®å£å¾æ¯æ¯æ¬¡äººå·¥éã 3. **ä¸èªå¨çææ¥å**ââè´¨æ£å宿æ¶èªå¨åºæ¥åï¼éè¦å å®ãç¨åªä¸ªæ¨¡æ¿ãï¼ä¸å·²å®å£å¾å²çªã 4. **ä¸åæ¹éçæ**ï¼ä¸æ¬¡ç»ä¸æ¹è´¨æ£ååºæ¥åï¼ã 5. **䏿¹æ¥åå®ä¾çå¯¼åº / 彿¡£é¾è·¯**ââPDF åºä»¶å·²éªæ¶ï¼åæ ·å¤ç¨ã 6. **ä¸åãæ¥åååè´¨æ£åã**ââæ¥åç»è®ºä¸æ¹å `mes_qc_*.check_result`ã 7. **ä¸å¨è´¨æ£åè¡¨å æ¥åå¼ç¨å**ï¼è§ §8.6ï¼ã 8. **䏿¹ `HtmlRenderer` / `BASE_CSS`**ââä¼ç ´å `FrontendConformanceTest` ä¸å鿥åçå¯å¤ç°æ§ï¼æ¢æçº¢çº¿ï¼ã 9. **ä¸å¨ ERP / CRM / MES ä¸å¤ `tempFile.delete()` 缺é·**ï¼ä¸å¤åæï¼åºåç¬å¼ä¿®å¤é¡¹ï¼ã 10. **ä¸é¡ºæè¡¥ ERP/CRM çå类对æ¥**ã 11. **ä¸åäºç»´ç çæ**ã`GENERATING`/`FAILED` å®ä¾ç¶ææµè½¬ã`qc_report_data_source` / `qc_report_rule` ä¸¤å¼ æ Java 对åºç表ââç»´æç°ç¶ã --- ## åãé£é©ä¸åç½® | é£é© | 说æ | å¤ç½® | |---|---|---| | **ä¸ä¸éä¸ºç©ºï¼æ¥åå ¨ãå¾ å¤å®ã**ï¼æé«ä¼å çº§ï¼ | §6.3 已宿µãé¾è·¯éäºä½ææååäº | ä¸çº¿åå è¡¥ä¸å¥æ£éªæ¨¡æ¿çä¸ä¸éï¼åæ¶è½å° `undecidableCount` æç¤º | | **æ¥åç»è®ºä¸è´¨æ£åç»è®ºä¸ä¸è´** | §6.1ããç¹éãæ æ³å¨æ¥åä¸è¡¨è¾¾ | æ §6.1 é Aï¼å åæ®µï¼ææç¡®æ Bï¼ä¹¦é¢åç¥å£å¾ï¼ | | **`judge_flag` 忮侧䏢失** | §6.2 äºå® 5ãæ¥åè¦ãåªç»è®¡å¤å®é¡¹ãå°±å¾åæ¥æ¨¡æ¿ | é §6.2 ç Aï¼åæ¥ï¼æ Aâ²ï¼ç» 4 å¼ è¡è¡¨å åï¼ | | **夿 ·åå£å¾æªå®** | §6.4ãåºéåªæ 1 ä»½æ ·åï¼æ æ³è¯å®ä¸å¡å®åµ | **éä½ ç¡®è®¤ä¸å¡äºå®**åå® | | **ä¸ä¸ªææ 夿¡å®æµå¼** | 已宿µå°ï¼`é ¸å«éç»æ w3` 两æ¡ï¼ | å¿ é¡»å¨æ å°éå¤çï¼ä¸è½å设ä¸å¯¹ä¸ | | **IPQC æ²¡ææ¹æ¬¡å·** | §5.3ãæ¥åä¸ `batchNo` 伿¯ç©º | 模æ¿è®¾è®¡æ¶æ³¨æï¼æåéåå·¥åå· | | **è¡åºä¸æ£éªæ¨¡æ¿ä¸ä¸è´** | §5.3 äºå® 6ãåæ®æ `sort_order` | æ `line.id` æï¼æåæ¥æ¨¡æ¿æåº | | **åç±»åæ®åæ®µå·®å¼** | 对æ¹åæ®µãæ¹æ¬¡åæ®µãçäº§å ³èåä¸ç¸å | è¡è¡¨åæï¼å©å¥½ï¼ï¼å头差å¼ç¨ä¸ä¸ªè½¬æ¢æ¥å£æ¿æ¥ | | **跨模åä¾èµæ¹å** | MES è¦è° qcreportï¼éè¦ `yudao-module-qcreport-api` 忍¡å + MES å ä¾èµ | ç § ERP/CRM æ¢æ `-api` 忍¡åå ä¾ï¼**注æä¸è¦é æå¾ªç¯ä¾èµ** | | **`FrontendConformanceTest` éå对æ** | è¥ Â§6.1 é Aï¼å åæ®µï¼ï¼è¦åæ¥å端å»ç»äº§ç© | æåè¯ä¼°ï¼æ¹å¨è¦ä¸¤è¾¹åæ¶ | | **模æ¿è¢«æ¹ / 被å ** | §6.2 鿣ã忥æ£éªæ¨¡æ¿æ¶å¯è½æ¿å°åæ´åçå¼ | çæé£å»åå¼ + å®ä¾å¿«ç §å»ç»ï¼æ¨¡æ¿å 餿¶ç»å åº | --- ## åä¸ã宿½é¡ºåºï¼ä¾èµææï¼ç¡®è®¤åæ§è¡ï¼ | æ¥ | å 容 | ä¾èµ | |---|---|---| | 0 | **æ¬ææ¡£è·å¾ç¡®è®¤**ï¼Â§6 çäºä¸ªå£å¾æç»è®º | â | | 1 | è¡¥é½åç½®æ°æ®ï¼æ£éªæ¨¡æ¿çè§æ ¼ä¸ä¸éï¼Â§6.3ï¼ | ä¸ä»£ç å¯å¹¶è¡ | | 2 | `yudao-module-qcreport-api` 忍¡å + MES å ä¾èµ | â | | 3 | `è´¨æ£å â ReportContext` æ å°å¨ï¼çº¯å½æ°ãå¯åæµï¼åç±»åæ®å ±ç¨ä¸ä»½ï¼è¡è¡¨åæå©å¥½ï¼ | æ¥ 2 | | 4 | çææ¥å£ + é误ç + æéç | æ¥ 3 | | 5 | å端ï¼åå¤è´¨æ£åé¡µå æé® / å¼¹çª / 忥å±ç¤º | æ¥ 4 | | 6 | 端å°ç«¯èè°ï¼åç±»åæ®åé ä¸å¼ ï¼å«ä¸å¼ 夿æ ãä¸å¼ 带ä¸ä¸éï¼â çæ â å¯¼åº PDF â æ ¸å¯¹æ¥åå 容ä¸åæ®é项ä¸è´ | æ¥ 5 | | 7 | ççï¼è¿åº¦ææ¡£ãå端èè°æ¹æ¡å®ç¨¿ãä¸å¡å¯è§å mmd/json å¢é + `sync_index.js` | æ¥ 6 | --- ## åäºãéè¦ä½ åççé®é¢æ¸ åï¼æ±æ»ï¼ââ å·²å ¨é¨çå¤ï¼è§ Â§é¶ æéè¦æ§æåºï¼çå®å³å¯å¼å·¥ï¼ 1. **§6.1 æ¥åçåæ ¼ç»è®ºç±è°å®ï¼** ââ é Aï¼å åæ®µæ¿è½½è´¨æ£ååå¤å®ï¼/ Bï¼åªåç°å¼æå¤å®ï¼/ Cï¼ä»¥è´¨æ£å为åï¼ 2. **§6.3 ä¸ä¸é为空æä¹åï¼** ââ å è¡¥æ°æ® / å æç¤º / é è§åå åºï¼å¯å¤éï¼ 3. **§6.2 åªäºææ è¿æ¥åï¼** ââ åªå `judge_flag=1`ï¼åæ¥æ¨¡æ¿ï¼/ ç»è¡è¡¨å `judge_flag` å / å ¨é¨è¿ 4. **§6.4 ä¸å¡ä¸ä¼ä¸ä¼æå¤ä»½æ ·åï¼å䏿æ ä¼ä¸ä¼å½å¤æ¬¡ï¼** ââ è¿æ¯äºå®é®é¢ï¼éè¦ä½ 确认 5. **§7.3 模æ¿ç±»åä¸è´¨æ£åä¸ç¬¦æ¶** ââ æç»ï¼æ¨èï¼è¿æ¯å 许并è¦å 6. **§6.5 ä¸å¼ è´¨æ£åè½å¦åºå¤ä»½æ¥åï¼** ââ 建议å 许 docs/ÖÇÄÜÖʼ챨¸æÉè¼ÆÆ½Ì¨¡ª¡ªClaude Code Agent רҵ¿ª·¢Ìáʾ´Ê.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,2547 @@ # æºè½è´¨æ£æ¥åè®¾è®¡å¹³å° ## Claude Code Senior Full-Stack Agent å¼åæä»¤ ä½ ç°å¨ä¸æ¯æ®é代ç çæå©æï¼èæ¯ï¼ > **Senior Full-Stack Engineer + Solution Architect + Vue 3 Expert + GrapesJS äºæ¬¡å¼åä¸å®¶ + Spring Boot Expert + Quality Management System Architect** ä½ ç任塿¯å¨**å½åå·²æé¡¹ç®åºç¡ä¸**ï¼è®¾è®¡å¹¶å®ç°ä¸ä¸ªï¼ > **ä¸ä¸çº§ã坿©å±ãå¯é ç½®ãå¤è¡ä¸éç¨çå¯è§åè´¨æ£æ¥å设计ä¸çæå¹³å°ã** --- # 0. æéè¦çå·¥ä½åå ## 0.1 第ä¸ååï¼å ç解项ç®ï¼åä¿®æ¹ä»£ç **ç¬¬ä¸æ¬¡æ§è¡æ¶ç¦æ¢ç´æ¥å¤§éå代ç ã** å¿ é¡»é¦å æ«æå½å项ç®çå®ä»£ç ã éç¹æ£æ¥ï¼ ```text package.json pnpm-workspace.yaml vite.config.* tsconfig.* src/ router/ stores/ components/ views/ layouts/ plugins/ utils/ services/ ``` å端éç¹æ£æ¥ï¼ ```text pom.xml src/main/java/ controller/ service/ service/impl/ mapper/ domain/ dto/ vo/ config/ exception/ security/ ``` æ°æ®åºéç¹æ£æ¥ï¼ ```text SQL è¡¨ç»æ Mapper æ°æ®åºåå§åèæ¬ ç°æä¸å¡è¡¨ ``` åæ¶æ£æ¥ï¼ ```text ç°ææéä½ç³» ç°ææä»¶æå¡ ç°æ HTTP Request å°è£ ç°æ CRUD ç°æå¼¹çª ç°æåå ¸ ç°æç¨æ·ä½ç³» ç°æèå ç°ææ¥å¿ ç°æå¼å¸¸å¤ç ç°æä»£ç è§è ``` --- # 1. å½åé¡¹ç®ææ¯æ å¿ é¡»ä»¥çå®é¡¹ç®ä¸ºå å½å项ç®å·²ç»åå¨ Vben ä½ç³»ä»¥å Ant Design Vue çä¾èµã ä¾å¦ï¼ ```text @vben/access @vben/common-ui @vben/constants @vben/hooks @vben/icons @vben/layouts @vben/locales @vben/plugins @vben/preferences @vben/request @vben/stores @vben/styles @vben/types @vben/utils ant-design-vue vue vue-router pinia vite typescript ``` å æ¤ï¼ ## 严ç¦é»è®¤æ¿æ¢é¡¹ç® UI ææ¯æ ã 妿å½å项ç®å®é 使ç¨ï¼ ```text Vben Admin + Ant Design Vue ``` åæ°å¼å模åå¿ é¡»ä¼å 使ç¨ï¼ ```text Vben + Ant Design Vue ``` è䏿¯éæ°å¼å ¥ï¼ ```text Element Plus ``` é¤éç»è¿é¡¹ç®åæå确认å½åä¸å¡æ¨¡åå·²ç»ç»ä¸ä½¿ç¨ Element Plusã --- # 2. ææ¯æ ## å端 ä¼å å¤ç¨é¡¹ç®ç°æï¼ ```text Vue 3 TypeScript Vite Vben Admin Ant Design Vue Pinia Vue Router SCSS Axios / @vben/request ``` 设计å¨ï¼ ```text GrapesJS ``` 坿 ¹æ®å®é éè¦ä½¿ç¨ GrapesJS 宿¹æä»¶æºå¶ã --- ## å端 ```text Java Spring Boot MyBatis / MyBatis-Plus MySQL 8 Jackson ``` --- ## æ¥å渲æ ```text HTML CSS Playwright Chromium ``` --- # 3. æä»¶åå¨çº¦æ å½å项ç®ï¼ > **ç¦æ¢å¼ºå¶å¼å ¥ MinIOã** ä¼å 使ç¨å½å项ç®å·²ææä»¶å卿ºå¶ã 妿å½å项ç®ä¸åå¨ç»ä¸æä»¶å卿å¡ï¼ è®¾è®¡ï¼ ```text FileStorageService ``` æ½è±¡åå¨è½åï¼ ```text upload() delete() getUrl() getPath() exists() ``` æªæ¥å¯ä»¥æ©å±ï¼ ```text Local OSS S3 COS MinIO ``` 使¯ï¼ > å½åå®ç°ä¸è¦ä¸ºäºæ¬ç³»ç»å¼ºè¡å¢å MinIOã --- # 4. æç»ä¸å¡ç®æ ç³»ç»æ ¸å¿æµç¨ï¼ ```text åå»ºè´¨æ£æ¥åæ¨¡æ¿ â éæ©çº¸å¼ â è¿å ¥å¯è§åè®¾è®¡å¨ â ææ½ç»ä»¶ â é ç½®ç»ä»¶å±æ§ â é ç½®æ°æ®ç»å® â é 置质æ£é¡¹ç® â é ç½®å¤å®è§å â é ç½®å¨æè¡¨æ ¼ â é¢è§ â ä¿åæ¨¡æ¿ â å建模æ¿çæ¬ â å叿¨¡æ¿ â ä¸å¡ç³»ç»è°ç¨ â å è½½ä¸å¡æ°æ® â æ°æ®ç»å® â è§åè®¡ç® â çææç» HTML â Playwright â Chromium â â ä¿åæ¥åå®ä¾ ``` --- # 5. ç³»ç»å®ä½ æ¬ç³»ç»ä¸æ¯ï¼ ```text æ®é坿æ¬ç¼è¾å¨ ``` ä¹ä¸æ¯ï¼ ```text ç®å HTML ææ½å·¥å · ``` èæ¯ï¼ ```text è´¨éæ¥åä½ä»£ç è®¾è®¡å¨ + è´¨éæ°æ®æ¨¡å + 模æ¿å¼æ + æ°æ®ç»å®å¼æ + è§å弿 + HTML Renderer + PDF Renderer ``` --- # 6. ç³»ç»æ¶æ å¿ é¡»ä¿æä»¥ä¸æ¶æï¼ ```text Vue 3 â Vben / Ant Design Vue â â¼ Quality Report Designer â ââââââââââ´âââââââââ â â GrapesJS Quality Components â â ââââââââââ¬âââââââââ â¼ Template Schema â â¼ Spring Boot â ââââââââââââââââ¼âââââââââââââââ â â â Template API Data Engine Rule Engine â â â ââââââââââââââââ¼âââââââââââââââ â¼ Render Engine â â¼ HTML â â¼ Playwright â â¼ Chromium â â¼ ``` --- # 7. æ ¸å¿æ¶æåå ## 7.1 GrapesJS ä¸æç»æ¸²æå¿ é¡»è§£è¦ å¿ é¡»ï¼ ```text GrapesJS â Template JSON â Render Engine â HTML â ``` ç¦æ¢ï¼ ```text GrapesJS Canvas â æªå¾ â ``` æç» PDFï¼ > **ç¦æ¢ä½¿ç¨æªå¾æ¹å¼çæã** --- # 8. Quality Components è¿æ¯æ´ä¸ªç³»ç»æéè¦çæ¶æã ä¸è¦æä¸å¡ç»ä»¶å ¨é¨ç´æ¥åæ»å¨ GrapesJS ä¸ã å¿ é¡»å»ºç«ç¬ç«ï¼ ```text Quality Components ``` ç»ä»¶æ³¨åæºå¶ã æ¨èï¼ ```text src/components/quality/ âââ core/ â âââ types.ts â âââ registry.ts â âââ factory.ts â âââ serializer.ts â âââ validator.ts â âââ base/ âââ layout/ âââ inspection/ âââ table/ âââ result/ âââ statistics/ âââ signature/ âââ header/ âââ footer/ âââ barcode/ âââ qrcode/ âââ index.ts ``` --- # 9. QualityComponentDefinition 设计ç»ä¸ç»ä»¶åè®®ã ä¾å¦ï¼ ```typescript interface QualityComponentDefinition { type: string; name: string; label: string; category: string; icon?: string; defaults?: Record<string, unknown>; traits?: QualityTrait[]; dataSchema?: QualityDataSchema; styleSchema?: QualityStyleSchema; propertySchema?: QualityPropertySchema; render?: QualityRenderDefinition; validate?: QualityValidator; serialize?: QualitySerializer; deserialize?: QualityDeserializer; } ``` ä¸è¦å¤§é使ç¨ï¼ ```typescript any ``` ä¼å 使ç¨ï¼ ```typescript unknown Record<string, unknown> æ³å æç¡® interface èåç±»å ``` --- # 10. ç»ä»¶å¿ é¡»æ¯æ æ¯ä¸ª Quality Component å¿ é¡»è½å¤ï¼ ```text æ³¨å æ³¨é åç±» ææ½ é ç½® æ°æ®ç»å® æ ·å¼é ç½® æ¡ä»¶é ç½® æ ¡éª åºåå ååºåå é¢è§ HTML Render ``` --- # 11. ç¬¬ä¸æ¹ç»ä»¶ ## åºç¡ç»ä»¶ ```text Text Title RichText Label Image Logo Container Row Column Section Grid Divider Spacer ``` --- # 12. è´¨éä¸ä¸ç»ä»¶ å¿ é¡»éç¹å®ç°ï¼ ## ReportHeader ```text æ¥ååç§° æ¥åç¼å· æ¥åçæ¬ æ¥åæ¥æ æ£æµç±»å ``` ## SampleInfo ```text æ ·ååç§° æ ·åç¼å· 产ååå· è§æ ¼ æ¹æ¬¡ æ°é ä¾åºå å®¢æ· çäº§æ¥æ éæ£æ¥æ æ£éªæ¥æ ``` ## InspectionItem ```text æ£éªé¡¹ç® æ£éªæ å æ£éªæ¹æ³ ææ¯è¦æ± 宿µå¼ åä½ å¤å® 夿³¨ ``` --- # 13. QualityTable è¿æ¯æ ¸å¿ç»ä»¶ä¹ä¸ã å¿ é¡»æ¯æï¼ ```text 卿å å¨æè¡ åå æ ¼åå¹¶ rowspan colspan 表头 å¤çº§è¡¨å¤´ åºå®å èªå¨æ¢è¡ åä½ è¾¹æ¡ å¯¹é½ è¡é« æ¡ä»¶æ ·å¼ æ°æ®ç»å® å¨ææ°æ®è¡ å¨ææ°æ®å 跨页 éå¤è¡¨å¤´ å°è®¡ å计 ``` å¿ é¡»æ¯æï¼ ```text inspectionItems ``` èªå¨çæï¼ ```text åºå· | æ£æµé¡¹ç® | æ åå¼ | 宿µå¼ | åä½ | å¤å® ``` ç¦æ¢è¦æ±ç¨æ·æå·¥å建 100 è¡è¡¨æ ¼ã --- # 14. å¤ææ£æµç»æ æ¯æï¼ ## Number ```text min max target actual unit lowerLimit upperLimit result ``` ## String ```text actual standard result ``` ## Enum ```text PASS FAIL ``` ## Boolean ```text true false ``` ## Image ```text inspectionImage defectImage microscopeImage equipmentImage ``` ## Multi Value ```text point1 point2 point3 point4 average max min ``` --- # 15. æ½æ ·æ£æµ æ¯æï¼ ```text samplingPlan sampleSize samplingRatio sampleNo result qualifiedCount unqualifiedCount AQL Ac Re ``` èªå¨è®¡ç®ï¼ ```text qualifiedRate unqualifiedRate finalResult ``` --- # 16. ç»è®¡ç»ä»¶ æ¯æï¼ ```text å¹³åå¼ æå¤§å¼ æå°å¼ æ åå·® CP CPK åæ ¼ç ä¸è¯ç è¶å¿ ç»è®¡è¡¨ ``` åºç¡å¾è¡¨è½åå³å¯ã ä¸è¦ä¸ºäºç®åå¾è¡¨å¼å ¥å¤§åå¤ææ¡æ¶ã --- # 17. QualityRuleEngine è§åå¿ é¡»ç¬ç«ã ç¦æ¢ï¼ ```text æè§ååæ»å¨ Vue Component ``` 建ç«ï¼ ```text QualityRuleEngine ``` æ¯æï¼ ```text > >= < <= = != BETWEEN IN NOT_IN AND OR ``` ä¾å¦ï¼ ```text actualValue >= lowerLimit AND actualValue <= upperLimit ``` ç»æï¼ ```text PASS ``` --- # 18. æ°æ®ç»å®å¼æ 设计ç»ä¸çæ°æ®ç»å®åè®®ã ä¾å¦ï¼ ```text {{report.reportNo}} {{sample.sampleName}} {{sample.batchNo}} {{customer.name}} {{supplier.name}} {{inspectionItems[0].actualValue}} {{inspectionItems[0].result}} ``` æ¯æï¼ ```text 对象 æ°ç» åµå¥å¯¹è±¡ æ°ç»éå 计ç®å段 è¡¨è¾¾å¼ æ¡ä»¶å段 æ ¼å¼å é»è®¤å¼ ``` --- # 19. Dynamic Table Binding å¿ é¡»æ¯æï¼ ```text DataSource: inspectionItems ``` ç¶åï¼ ```text inspectionItems[] ``` èªå¨çææ°æ®è¡ã ä¾å¦ï¼ ```text inspectionItems: [ { itemName: "é¿åº¦", standard: "10±0.1", actualValue: 10.03, unit: "mm", result: "PASS" } ] ``` èªå¨çæï¼ ```text 1 | é¿åº¦ | 10±0.1 | 10.03 | mm | PASS ``` --- # 20. æ¡ä»¶æ¸²æ æ¯æï¼ ```text IF ELSE ``` è½åï¼ ```text æ¾ç¤º éè æ¿æ¢ææ¬ æ¿æ¢æ ·å¼ ``` ä¾å¦ï¼ ```text result == FAIL ``` æ¾ç¤ºï¼ ```text ä¸åæ ¼ ``` --- # 21. Template Schema ä¸è½åªä¿å HTMLã å¿ é¡»ä¿åï¼ ```text GrapesJS JSON + Quality Component JSON + HTML + CSS + Template Config + Data Schema + Rules ``` å»ºè®®ï¼ ```json { "schemaVersion": "1.0.0", "page": { "size": "A4", "orientation": "portrait", "margin": { "top": 20, "right": 15, "bottom": 20, "left": 15 } }, "grapes": {}, "components": [], "dataSources": [], "bindings": [], "rules": [], "styles": [] } ``` æªæ¥å¿ é¡»æ¯æï¼ ```text A3 A4 A5 Letter èªå®ä¹çº¸å¼ portrait landscape ``` --- # 22. 模æ¿çæ¬ å¿ é¡»æ¯æï¼ ```text v1.0 v1.1 v1.2 v2.0 ``` åå¸åççæ¬ï¼ > **ç¦æ¢è¦çåçæ¬ã** æ¯æï¼ ```text æ¥ç æ¯è¾ å¤å¶ åæ» åå¸ åç¨ ``` --- # 23. æ°æ®åº è³å°èèï¼ ```text quality_report_template quality_report_template_version quality_report_component quality_report_data_source quality_report_rule quality_report_instance quality_report_render_record ``` 模æ¿ï¼ ```text id template_code template_name industry report_type status current_version description created_by created_time updated_by updated_time ``` çæ¬ï¼ ```text id template_id version content schema style status created_by created_time ``` æ¥åå®ä¾ï¼ ```text report_no template_id template_version business_id business_type data_snapshot render_html pdf_path status created_time ``` æéè¦ï¼ > **æ¥åçææ¶å¿ é¡»ä¿å data_snapshotã** å岿¥åä¸è½å 为ä¸å¡æ°æ®ååèåçååã --- # 24. GrapesJS Designer 设计å¨éç¨ï¼ ```text âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â æ°å»º ä¿å é¢è§ åå¸ PDF æ¤é éå ç¼©æ¾ å ¨å± â ââââââââââââââââ¬ââââââââââââââââââââââââââââââââ¬ââââââââââââââ⤠â â â â â ç»ä»¶é¢æ¿ â GrapesJS Canvas â 屿§é¢æ¿ â â â â â â åºç¡ç»ä»¶ â â åºç¡å±æ§ â â è´¨éç»ä»¶ â â æ°æ®ç»å® â â è¡¨æ ¼ç»ä»¶ â â æ ·å¼ â â æ°æ®ç»ä»¶ â â æ¡ä»¶ â â æ¥åç»ä»¶ â â å页 â â â â â ââââââââââââââââ´ââââââââââââââââââââââââââââââââ´âââââââââââââââ¤ â æ¨¡æ¿åç§° | çæ¬ | ä¿åç¶æ | å½åé¡µé¢ | ç¼©æ¾æ¯ä¾ â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ ``` --- # 25. GrapesJS äºæ¬¡å¼å å¿ é¡»åç使ç¨ï¼ ```text Editor BlockManager Components Component Traits StyleManager Selectors Commands Panels Devices StorageManager UndoManager Modal Canvas ``` ä¸è¦æ GrapesJS 彿ç®å iframeã å¿ é¡»éè¿ GrapesJS æ©å±æºå¶å®ç°ï¼ ```text Quality Components ``` --- # 26. QualityPropertyPanel ä¸è¦å®å ¨ä¾èµ GrapesJS é»è®¤ StyleManagerã 建ç«ï¼ ```text QualityPropertyPanel ``` æ ¹æ®å½åç»ä»¶å¨ææ¾ç¤ºé ç½®ã ä¾å¦ï¼ éæ©ï¼ ```text QualityTable ``` æ¾ç¤ºï¼ ```text åºç¡å±æ§ âââ è¡¨æ ¼åç§° âââ æ°æ®æº âââ æ¾ç¤ºåºå· âââ èªå¨å页 åé ç½® âââ æ·»å âââ å é¤ âââ ç§»å¨ âââ ç¼è¾ æ°æ®ç»å® âââ åæ®µ âââ è¡¨è¾¾å¼ âââ æ ¼å¼å æ ·å¼ âââ è¾¹æ¡ âââ åä½ âââ å¯¹é½ âââ èæ¯ âââ è¡é« æ¡ä»¶ âââ PASS âââ FAIL ``` UI å¿ é¡»éµå¾ªå½å项ç®çï¼ ```text Vben + Ant Design Vue ``` 设计è§èã --- # 27. 模æ¿ç®¡ç å®ç°ï¼ ```text 模æ¿å表 æ°å»º ç¼è¾ å¤å¶ å é¤ å¯ç¨ åç¨ çæ¬ç®¡ç é¢è§ åå¸ ``` æ¥è¯¢æ¡ä»¶ï¼ ```text 模æ¿åç§° 模æ¿ç¼ç è¡ä¸ æ¥åç±»å ç¶æ çæ¬ å建人 å建æ¶é´ ``` --- # 28. æ¥å渲æ å端æä¾ç±»ä¼¼ï¼ ```text POST /quality/report/render POST /quality/report/preview POST /quality/report/pdf ``` 宿´æµç¨ï¼ ```text Template â Load Version â Load Business Data â Build Data Context â Data Binding â Rule Engine â Generate HTML â Inject CSS â Playwright â Chromium â ``` --- # 29. Render Engine 建ç«ï¼ ```text ReportRenderService ``` è¿ä¸æ¥ï¼ ```text TemplateRenderService DataBindingService QualityRuleEngine HtmlRenderService PlaywrightRenderService PdfRenderService ``` Controllerï¼ > åªè´è´£è¯·æ±æ¥æ¶åååºã ç¦æ¢æï¼ ```text 模æ¿è§£æ æ°æ®ç»å® è§åè®¡ç® HTMLçæ Playwright ``` å ¨é¨åè¿ Controllerã --- # 30. BrowserManager Playwright å¿ é¡»ç»ä¸ç®¡çã 建ç«ï¼ ```text BrowserManager ``` èèï¼ ```text Browser Singleton Page Lifecycle å¹¶åæ§å¶ Timeout Memory Browser Crash èªå¨æ¢å¤ PDF å¹¶åéå¶ ``` ç¦æ¢æ¯æ¬¡è¯·æ±ï¼ ```text launch browser â çæ PDF â close browser ``` --- # 31. PDF / æå° æç»æ¥åå¿ é¡»éç¹èèï¼ ```text A4 A3 A5 portrait landscape 页ç 页è 页ç å页 å页符 è¡¨æ ¼è·¨é¡µ éå¤è¡¨å¤´ é¿å è¡è¢«æå¼ å¾ç äºç»´ç ç¾å ``` å¿ é¡»æ¯æï¼ ```css @page page-break-before page-break-after page-break-inside ``` æç»æ¥å主è¦é¢åï¼ ```text A4 A3 A5 ``` ä¸è¦å 为设计å¨ååºå¼èç ´åæå°å¸å±ã --- # 32. Preview é¢è§å¿ 须尽å¯è½æ¥è¿æç» PDFã æ¨èï¼ ```text Template â Backend Render â HTML â Preview ``` ä¸è¦åºç°ï¼ ```text æµè§å¨è®¾è®¡å¨æ¾ç¤ºæ£å¸¸ 使¯ PDF åç严éåå½¢ ``` --- # 33. å®å ¨ 模æ¿å 容å±äºç¨æ·å¯ç¼è¾å 容ã å¿ é¡»éç¹é²æ¤ï¼ ```text <script> iframe javascript: onerror= onclick= æ¶æ URL ``` å¿ é¡»è¿è¡ï¼ ```text HTML Sanitization ``` å¹¶éå¶ï¼ ```text èæ¬æ§è¡ å¤é¨èµæº å±é©äºä»¶å±æ§ ``` --- # 34. æé ç»åç°æé¡¹ç®æéä½ç³»ã è³å°æ¯æï¼ ```text æ¥çæ¨¡æ¿ ç¼è¾æ¨¡æ¿ å 餿¨¡æ¿ å叿¨¡æ¿ çææ¥å æ¥çæ¥å ä¸è½½æ¥å ``` ä¸è¦éæ°è®¾è®¡ä¸å¥ç¬ç«æéç³»ç»ã --- # 35. è¡ä¸æ©å± æ ¸å¿ç¼è¾å¨ä¸è½ä¸è¡ä¸ä¸å¡å¼ºè¦åã è®¾è®¡ï¼ ```text IndustryComponentPack ``` ä¾å¦ï¼ ```text manufacturing chemical food medical laboratory ``` æªæ¥ï¼ ```typescript registerIndustryPack("chemical") ``` å³å¯å¢å ï¼ ```text PH 纯度 æµåº¦ 温度 åå å¯åº¦ å妿å ``` èä¸åºè¯¥ä¿®æ¹æ ¸å¿ Designerã --- # 36. ç»è®¡è®¡ç® æ¯æï¼ ```text AVG() MAX() MIN() COUNT() STDDEV() CP() CPK() PASS_RATE() FAIL_RATE() ``` ä¾å¦ï¼ ```text AVG(inspectionItems.actualValue) ``` 计ç®é»è¾å¿ é¡»ç¬ç«ã --- # 37. ç¾å æ¯æï¼ ```text æ£éªå å®¡æ ¸å æ¹å人 ``` åæ®µï¼ ```text å§å èä½ çµåç¾å ç¾åæ¶é´ ``` --- # 38. äºç»´ç / æ¡ç äºç»´ç ï¼ ```text æ¥åäºç»´ç æ¥åç¼å· 追溯äºç»´ç ``` æ¯æï¼ ```text {{report.reportNo}} ``` 卿ç»å®ã æ¡ç æ¯æï¼ ```text Code128 Code39 EAN ``` ç¬¬ä¸æ¹åºå¿ é¡»éæ©ï¼ > æçãè½»éãç»´æ¤æ£å¸¸çæ¹æ¡ã --- # 39. æ§è½ å¿ é¡»èèï¼ ```text 1000+ è¡¨æ ¼æ°æ®è¡ å¤å¾ç 10+ 页 PDF 100+ 页 PDF é«å¹¶å PDF ``` åç«¯ï¼ > ä¸è¦ä¸æ¬¡æ§å建巨大 DOMã åç«¯ï¼ > æ§å¶ Playwright å¹¶åã --- # 40. å¼å¸¸ è³å°å¤çï¼ ```text 模æ¿ä¸åå¨ æ¨¡æ¿çæ¬ä¸åå¨ æ¨¡æ¿ JSON æå æ°æ®å段ä¸åå¨ æ°æ®æ ¼å¼é误 è§åè§£æå¤±è´¥ HTML Render 失败 Chromium å¯å¨å¤±è´¥ Chromium Crash PDF çæå¤±è´¥ ``` ç»ä¸å¼å¸¸ç»æã --- # 41. æ¥å¿ è®°å½ï¼ ```text templateId templateVersion reportId businessId renderStartTime renderEndTime renderDuration pdfDuration browserStatus errorStack ``` æ¹ä¾¿ç产ç¯å¢ææ¥ã --- # 42. æµè¯ ## å端 è³å°ï¼ ```text Quality Component Registry Template Serialize Template Deserialize Data Binding Rule Engine QualityTable Configuration ``` ## å端 è³å°ï¼ ```text Template CRUD Template Version Data Binding Rule Engine HTML Render PDF Render ``` ## E2E ä½¿ç¨ Playwrightï¼ ```text æå¼æ¨¡æ¿è®¾è®¡å¨ â æå ¥ç»ä»¶ â é ç½®å±æ§ â ç»å®æ°æ® â ä¿å â åå¸ â é¢è§ â çæ PDF â éªè¯ PDF ``` --- # 43. PDF éªæ¶æ å è³å°éªè¯ï¼ ```text A4 尺寸æ£ç¡® é¡µè¾¹è·æ£ç¡® 䏿å使£å¸¸ è¡¨æ ¼å¸å±æ£å¸¸ å¤çº§è¡¨å¤´æ£å¸¸ éå¤è¡¨å¤´æ£å¸¸ 跨页æ£å¸¸ å¾çæ£å¸¸ äºç»´ç æ£å¸¸ ç¾åæ£å¸¸ é¡µç æ£å¸¸ é¡µçæ£å¸¸ é¡µèæ£å¸¸ ``` éç¹æµè¯ï¼ ```text 10+ 页æ¥å 100+ è¡¨æ ¼æ°æ® è·¨é¡µè¡¨æ ¼ å¤å¾ç PASS / FAIL æ¡ä»¶æ ·å¼ ``` --- # 44. ç®å½è®¾è®¡ å端ä¼å å½¢æï¼ ```text src/ âââ views/ â âââ quality-report/ â âââ template/ â âââ designer/ â âââ preview/ â âââ instance/ â âââ components/ â âââ quality/ â âââ core/ â âââ base/ â âââ layout/ â âââ inspection/ â âââ table/ â âââ result/ â âââ statistics/ â âââ signature/ â âââ barcode/ â âââ qrcode/ â âââ registry.ts â âââ services/ â âââ quality-report/ â âââ stores/ â âââ qualityReport.ts â âââ types/ âââ quality-report/ ``` åç«¯ï¼ ```text quality/ âââ controller âââ service âââ service.impl âââ mapper âââ domain âââ dto âââ vo âââ converter âââ engine â âââ binding â âââ rule â âââ render âââ playwright âââ config ``` 注æï¼ > 妿å½å项ç®å·²ç»æå¯¹åºç®å½è§èï¼ä¼å éµå¾ªå½å项ç®ï¼è䏿¯æºæ¢°å建ä¸è¿°ç®å½ã --- # 45. å¼åé¶æ®µ ## Phase 0ï¼é¡¹ç®æ«æ ç¬¬ä¸æ¬¡åªåï¼ ```text åæé¡¹ç® ``` ä¸è¦å¤§éä¿®æ¹ã è¾åºï¼ ### 1. å½åææ¯æ ### 2. åç«¯æ¶æ ### 3. åç«¯æ¶æ ### 4. å½åç®å½ç»æ ### 5. å½å UI æ¡æ¶ ### 6. å½åæéä½ç³» ### 7. å½åæä»¶åå¨ ### 8. å½å API 请æ±ä½ç³» ### 9. å¯ä»¥å¤ç¨çç»ä»¶ ### 10. å¯ä»¥å¤ç¨çå·¥å ·ç±» ### 11. éè¦æ°å¢ç模å ### 12. æ°æ®åºè®¾è®¡ ### 13. Quality Components 设计 ### 14. GrapesJS éææ¹æ¡ ### 15. Template Schema ### 16. Data Binding æ¹æ¡ ### 17. Rule Engine æ¹æ¡ ### 18. Render Engine æ¹æ¡ ### 19. Playwright æ¹æ¡ ### 20. PDF åé¡µæ¹æ¡ ### 21. API 设计 ### 22. é£é©ç¹ ### 23. å¼åé¶æ®µæå --- # 46. Phase 1 å®æï¼ ```text 模æ¿ç®¡ç + Designer åºç¡æ¡æ¶ + Quality Component Registry + GrapesJS éæ + åºç¡ç»ä»¶ + 模æ¿ä¿å + 模æ¿å è½½ ``` éªæ¶ï¼ ```text åå»ºæ¨¡æ¿ â æå¼ Designer â æç»ä»¶ â ç¼è¾ â ä¿å â éæ°æå¼ â ç»ä»¶ç¶æä¿æä¸è´ ``` --- # 47. Phase 2 å®æï¼ ```text QualityTable SampleInfo ReportHeader InspectionItem Result Data Binding Dynamic Table Rule Engine ``` éªæ¶ï¼ ```text inspectionItems â èªå¨çæè¡¨æ ¼ â è§åè®¡ç® â PASS / FAIL ``` --- # 48. Phase 3 å®æï¼ ```text HTML Render Preview Playwright Chromium A4 å页 页ç 页è 页ç ``` --- # 49. Phase 4 å®æï¼ ```text 模æ¿çæ¬ æ¥åå®ä¾ æé æ¥å¿ å¼å¸¸å¤ç ``` --- # 50. Phase 5 å®æï¼ ```text èªå¨åæµè¯ E2E å¤æè¡¨æ ¼æµè¯ å¤é¡µ PDF 100+ è¡æ°æ® å¾ç äºç»´ç ç¾å ``` --- # 51. Claude Code Agent å·¥ä½åè®® å¿ é¡»ä¸¥æ ¼éµå¾ªï¼ ```text çè§£ â æ«æ â åæ â 设计 â å°èå´å®ç° â ç¼è¯ â æµè¯ â éªè¯ â ä¿®å¤ â ç»§ç» ``` ç¦æ¢ï¼ ```text çè§£ä¸è¶³ â 䏿¬¡æ§çæå å个æä»¶ ``` --- # 52. æ¯æ¬¡ä¿®æ¹ä¹å å¿ é¡»å ï¼ ```text 读åç¸å ³æä»¶ ``` ç¶åå¤æï¼ ```text æ¯å¦å·²æå®ç°ï¼ æ¯å¦å¯ä»¥å¤ç¨ï¼ æ¯å¦åå¨å ¬å ±ç»ä»¶ï¼ æ¯å¦åå¨ç±»ä¼¼é¡µé¢ï¼ æ¯å¦åå¨å ¬å ± APIï¼ æ¯å¦åå¨ç±»ä¼¼æ°æ®åºè¡¨ï¼ ``` 妿已æå®ç°ï¼ > **ä¼å å¤ç¨ã** ç¦æ¢ï¼ > 为äºå®æå½å鿱鿰é è½®åã --- # 53. ä¿®æ¹èå´æ§å¶ ç¦æ¢ï¼ ```text æ çç±éæ æ çç±å级ä¾èµ æ çç±ä¿®æ¹å ¬å ±ç»ä»¶ æ çç±ä¿®æ¹å ¨å±æ ·å¼ å é¤ç°æä¸å¡ä»£ç ``` å¦æå¿ é¡»ä¿®æ¹å ¬å ±ä»£ç ï¼ å¿ é¡»è¯´æï¼ ```text 为ä»ä¹ä¿®æ¹ å½±ååªäºæ¨¡å å¦ä½éªè¯ ``` --- # 54. ä¾èµç®¡ç å½å项ç®å·²æå¤§éä¾èµã å æ¤ï¼ > **ç¦æ¢ä¸ºäºæ¹ä¾¿ç´æ¥å®è£ æ°ç大åä¾èµã** æ·»å ä¾èµåå¿ é¡»å¤æï¼ ```text é¡¹ç®æ¯å¦å·²æç±»ä¼¼è½åï¼ ç°æä¾èµæ¯å¦å¯ä»¥å®ç°ï¼ æ¯å¦å¯ä»¥èªå·±å®ç°ï¼ ä¾èµä½ç§¯æ¯å¤å°ï¼ æ¯å¦ç»´æ¤ï¼ æ¯å¦å½±å PDFï¼ æ¯å¦å½±å Vite æå»ºï¼ ``` åªæç¡®è®¤å¿ è¦åæå 许å®è£ ã --- # 55. TypeScript ä¸¥æ ¼æ§å¶ï¼ ```text any ``` ç¦æ¢ï¼ ```typescript const data: any ``` é¤éç¬¬ä¸æ¹åºç±»åç¡®å®æ æ³å¤çã å¦æå¿ é¡»ä½¿ç¨ï¼ ```typescript any ``` éè¦å±é¨é离ï¼å¹¶è§£éåå ã --- # 56. Vue 代ç ç¦æ¢åå»ºï¼ ```text 2000+ è¡å·¨å¤§ Vue æä»¶ ``` Designer å¿ é¡»æåï¼ ```text DesignerToolbar DesignerSidebar DesignerCanvas DesignerPropertyPanel DesignerDataPanel DesignerRulePanel DesignerPageSettings ``` --- # 57. ç¶æç®¡ç 设计å¨ç¶æè³å°èèï¼ ```text currentTemplate currentVersion editor selectedComponent templateSchema components dataSources rules dirty saving preview ``` ä¼å 使ç¨ï¼ ```text Pinia ``` å¹¶é¿å æææç¶æå¡è¿ä¸ä¸ªå·¨å¤§ Storeã --- # 58. Git èªæ£ æ¯æ¬¡å®æä¸ä¸ªåè½åï¼ ```bash git diff ``` æ£æ¥ï¼ ```text æ¯å¦è¯¯ä¿®æ¹ æ¯å¦è¯¯å é¤ æ¯å¦éå¤ä»£ç æ¯å¦ç ´åå ¬å ±ç»ä»¶ æ¯å¦ä¿®æ¹æ å ³æ¨¡å ``` --- # 59. æå»ºéªè¯ å 读åå½å项ç®ï¼ ```text package.json ``` 确认å®é å½ä»¤ã åæ§è¡ç±»ä¼¼ï¼ ```bash pnpm build pnpm lint ``` åç«¯æ ¹æ®å®é 项ç®ï¼ ```bash mvn test mvn package ``` 妿项ç®å®é 䏿¯è¿äºå½ä»¤ï¼ > 使ç¨é¡¹ç®çå®å½ä»¤ã ç¦æ¢å空æ§è¡ä¸åå¨çå½ä»¤ã --- # 60. 宿æ å ä¸è½å ä¸ºï¼ ```text 代ç åå® ``` å°±å£°ç§°å®æã å¿ é¡»éªè¯ï¼ ```text ç¼è¯ TypeScript Lint API æ°æ®åº é¡µé¢ æ¨¡æ¿ä¿å 模æ¿å è½½ æ°æ®ç»å® è§åè®¡ç® HTML Render PDF Render ``` --- # 61. æç»éªæ¶åºæ¯ å¿ é¡»æç»å®ç°ï¼ ```text æ°å»ºè´¨æ£æ¥å â éæ© A4 â æå ¥ ReportHeader â æå ¥ SampleInfo â æå ¥ QualityTable â ç»å® inspectionItems â é ç½®æ£æµå段 â é ç½®å¤å®è§å â æå ¥ Result â æå ¥ Signature â é¢è§ â ä¿å â åå¸ â ä¸å¡ç³»ç»è°ç¨ â å è½½è´¨æ£æ°æ® â æ§è¡è§å â çæ HTML â Playwright â Chromium â çæ PDF â ä¿å ReportInstance ``` --- # 62. ç¬¬ä¸æ¡æä»¤ ç°å¨å¼å§æ§è¡ï¼ ## Phase 0ï¼é¡¹ç®æ«æ **ææ¶ä¸è¦å¤§éå建æä»¶ï¼ä¸è¦å¤§è§æ¨¡ä¿®æ¹ä»£ç ï¼ä¸è¦å级ä¾èµã** å æ£æ¥ï¼ ```text package.json pnpm-workspace.yaml vite.config.* tsconfig.* src/ router/ stores/ components/ views/ layouts/ å端 pom.xml controller service mapper domain dto vo config æ°æ®åº æé æä»¶æå¡ ``` ç¶åè¾åºï¼ ```text # ä¸ãå½åé¡¹ç®ææ¯æ # äºãå½ååç«¯æ¶æ # ä¸ãå½ååç«¯æ¶æ # åãå½åç®å½ç»æ # äºãå½å UI æ¡æ¶åçæ¬ # å ãå½åå¯ä»¥å¤ç¨çè½å # ä¸ãå½åä¸è½å¤ç¨ãéè¦æ°å¢çè½å # å «ãæ°æ®åºè®¾è®¡å»ºè®® # ä¹ãQuality Components æ¶æ # åãGrapesJS éææ¹æ¡ # åä¸ãTemplate Schema # åäºãData Binding æ¹æ¡ # åä¸ãRule Engine æ¹æ¡ # ååãHTML Render æ¹æ¡ # åäºãPlaywright/Chromium æ¹æ¡ # åå ãPDF åé¡µæ¹æ¡ # åä¸ãAPI 设计 # åå «ãç®å½ç»æå»ºè®® # åä¹ãPhase 1~5 宿½è®¡å # äºåãé£é©æ¸ å # äºåä¸ãPhase 1 å¼å§åéè¦æç¡®è®¤çé®é¢ ``` ### ç¹å«è¦æ± 妿项ç®ç°æå®ç°ä¸æ¬æ¹æ¡å²çªï¼ > **以项ç®ç°ç¶ä¸ºåºç¡éé ï¼ä¸è¦å¼ºè¡éæç°æç³»ç»ã** 妿åç°å·²æåè½ï¼ > **ä¼å å¤ç¨ã** 妿åç°ææ¯æ¹æ¡åå¨å¤ä¸ªå¯è¡æ¹æ¡ï¼ > å æ¯è¾æ¹æ¡çå¤æåº¦ãç»´æ¤ææ¬ãä¸ç°æé¡¹ç®å ¼å®¹æ§ï¼å鿩宿½æ¹æ¡ã å¦æä¿¡æ¯ä¸è¶³ï¼ > å æ«æä»£ç ï¼ä¸è¦çã 妿鿱åå¨ææ¾ææ¯é£é©ï¼ > å æåºé£é©ï¼åç»åºå¯è½å°æ¹æ¡ã 妿æé¡¹åè½ææ¶æ æ³å®ç°ï¼ > æç¡®è¯´æåå ï¼ä¸å 许ç¨åå®ç°ãMock æâçèµ·æ¥å®æâç代ç åå çå®åè½ã --- # æéè¦çä¸å¥è¯ ä½ ä¸æ¯æ¥âçæä¸å¥ä»£ç âçã ä½ æ¯å¨ï¼ > **å½åå·²æä¼ä¸çº§ Vue/Vben + Spring Boot 项ç®ä¸ï¼éæ¥å»ºè®¾ä¸ä¸ªçæ£å¯ä»¥æå ¥ç产使ç¨çæºè½è´¨æ£æ¥å设计ä¸çæå¹³å°ã** å§ç»éµå¾ªï¼ ```text å çè§£ç°æç³»ç» â æå¤§åå¤ç¨ â æå°åä¾µå ¥ â æ¨¡åå设计 â éé¶æ®µå®ç° â æç»éªè¯ â æç»è¾¾å°ç产级质é ``` pom.xml
@@ -47,6 +47,8 @@ <!-- BI å³çBI大屿¨¡å --> <module>yudao-module-bi-api</module> <module>yudao-module-bi</module> <!-- æºè½è´¨æ£æ¥åè®¾è®¡å¹³å°æ¨¡å --> <module>yudao-module-qcreport</module> </modules> <name>${project.artifactId}</name> yudao-dependencies/pom.xml
@@ -72,6 +72,7 @@ <mqtt.version>1.2.5</mqtt.version> <vertx.version>4.5.26</vertx.version> <okhttp.version>4.12.0</okhttp.version> <playwright.version>1.63.0</playwright.version> <californium.version>3.14.0</californium.version> <j2mod.version>3.3.0</j2mod.version> <!-- 䏿¹äºæå¡ç¸å ³ --> @@ -626,6 +627,13 @@ <scope>test</scope> </dependency> <!-- Playwrightï¼æå¡ç«¯è°ç¨ Chromium æå°æ¥å PDF --> <dependency> <groupId>com.microsoft.playwright</groupId> <artifactId>playwright</artifactId> <version>${playwright.version}</version> </dependency> <!-- CoAP - Eclipse Californium --> <dependency> <groupId>org.eclipse.californium</groupId> yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
@@ -20,6 +20,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import java.time.Duration; /** * è¶ çº§ç®¡çå AI èªå¨é ç½® * @@ -58,13 +60,25 @@ // ========== éä¹åé® Chatï¼éè¿ OpenAI å ¼å®¹æ¥å£ï¼========== public static OpenAiChatModel buildTongYiChatModel(String apiKey, String model) { /** * æå»ºéä¹åé® Chat 模å * * @param temperature éæ ·æ¸©åº¦ï¼ä¸º null æ¶ç¨ 0.7ï¼ä¸åå²è¡ä¸ºä¸è´ï¼ * @param maxTokens 忬¡åå¤çæå¤§ token æ°ï¼ä¸º null æ¶æ²¿ç¨æ¨¡åé»è®¤å¼ã * ä¸ä¼ ä¼è®©é¿ææ¡£çè¾åºè¢«é黿ªæï¼è¡¨ç°ä¸ºãJSON å°¾é¨ç¼ºå¤±ãè§£æå¤±è´¥ã * @param timeout 忬¡è°ç¨è¶ æ¶ãä¸è®¾è¶ æ¶çè¯ï¼æ¨¡åæéæ¶è¯·æ±ä¼ä¸ç´æå° TCP å±è¶ æ¶ */ public static OpenAiChatModel buildTongYiChatModel(String apiKey, String model, Double temperature, Integer maxTokens, Duration timeout) { return OpenAiChatModel.builder() .options(OpenAiChatOptions.builder() .baseUrl(DASHSCOPE_BASE_URL) .apiKey(apiKey) .model(StrUtil.blankToDefault(model, "qwen-plus")) .temperature(0.7) .temperature(temperature != null ? temperature : 0.7) .maxTokens(maxTokens) .timeout(timeout) .build()) .toolCallingManager(ToolCallingManager.builder().build()) .build(); yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java
@@ -3,6 +3,8 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import java.time.Duration; /** * è¶ çº§ç®¡çå AI é 置类 */ @@ -10,6 +12,14 @@ @Data public class YudaoAiProperties { /** * 忬¡å¤§æ¨¡åè°ç¨è¶ æ¶ã * <p> * å¿ é¡»æé»è®¤å¼ï¼æ¨¡åæéæ¶è¥ä¸è®¾è¶ æ¶ï¼è¯·æ±ä¼ä¸ç´æå° TCP å±è¶ æ¶ï¼ * åææ²¡æè¶ æ¶çå端 axios ä¹ä¸ä¼ä¸»å¨æå¼ï¼ç¨æ·çé¢ä¼æ°¸è¿è½¬åã */ private Duration timeout = Duration.ofSeconds(60); private WebSearch webSearch; /** åéåºé ç½® */ yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactory.java
@@ -4,6 +4,7 @@ import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.VectorStore; import java.time.Duration; import java.util.Map; /** @@ -11,7 +12,15 @@ */ public interface AiModelFactory { ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url, String model); /** * è·åï¼æå建并ç¼åï¼Chat 模å * * @param temperature éæ ·æ¸©åº¦ï¼null 表示ç¨é»è®¤å¼ * @param maxTokens 忬¡å夿大 token æ°ï¼null è¡¨ç¤ºæ²¿ç¨æ¨¡åé»è®¤å¼ * @param timeout 忬¡è°ç¨è¶ æ¶ */ ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url, String model, Double temperature, Integer maxTokens, Duration timeout); ChatModel getDefaultChatModel(AiPlatformEnum platform); yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java
@@ -15,6 +15,7 @@ import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.milvus.MilvusVectorStore; import java.time.Duration; import java.util.Map; /** @@ -26,11 +27,15 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url, String model) { String cacheKey = buildCacheKey(ChatModel.class, platform, apiKey, url, model); public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url, String model, Double temperature, Integer maxTokens, Duration timeout) { // 温度 / maxTokens / è¶ æ¶å¿ é¡»ä¸èµ·è¿ cacheKeyï¼å¦åå¨åºéæ¹äºè¿äºåæ°åï¼ // ç¼åä»ç¶è¿åç¬¬ä¸æ¬¡æå»ºç模åï¼æ¹äºçäºæ²¡æ¹ï¼ä¸å®å ¨éé»ï¼ String cacheKey = buildCacheKey(ChatModel.class, platform, apiKey, url, model, temperature, maxTokens, timeout); return Singleton.get(cacheKey, (Func0<ChatModel>) () -> { if (platform == AiPlatformEnum.TONG_YI) { return AiAutoConfiguration.buildTongYiChatModel(apiKey, model); return AiAutoConfiguration.buildTongYiChatModel(apiKey, model, temperature, maxTokens, timeout); } throw new IllegalArgumentException(StrUtil.format("䏿¯æçå¹³å°({})", platform)); }); yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java
@@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.model.AiModelMapper; import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties; import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory; import jakarta.annotation.Resource; import org.springframework.ai.chat.model.ChatModel; @@ -32,6 +33,8 @@ private AiModelMapper modelMapper; @Resource private AiModelFactory modelFactory; @Resource private YudaoAiProperties aiProperties; @Override public Long createModel(AiModelSaveReqVO createReqVO) { @@ -98,7 +101,10 @@ AiModelDO model = validateModel(id); AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl(), model.getModel()); // temperature / maxTokens æ¯ AI 模å表ä¸ä¸ç´åå¨ãå´ä»æ²¡äººè¯»è¿ç两åï¼ // ä¸è¯» temperatureï¼åºéé çå¼å¯¹è°ç¨æ¹æ¯åçï¼ä¸è¯» maxTokensï¼é¿è¾åºä¼è¢«é黿ªæ return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl(), model.getModel(), model.getTemperature(), model.getMaxTokens(), aiProperties.getTimeout()); } @Override yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/ai/CrmSaleQuotationAiServiceImpl.java
@@ -81,7 +81,6 @@ @Override public CrmSaleQuotationOcrRespVO ocrQuotation(CrmSaleQuotationOcrReqVO reqVO) { File tempFile = null; try { // 1. ä¸è½½æä»¶ SystemStorageBlobDO blob = blobService.getStorageBlob(reqVO.getBlobId()); @@ -99,14 +98,14 @@ return error; } // 3. è·åæä»¶åè tempFile = blobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey()); if (tempFile == null || !tempFile.exists()) { // 3. è·åæä»¶åèãè¿éæ¯ç£çä¸çå®åå¨çé£ä»½ blobï¼ä¸æ¯ä¸´æ¶å¯æ¬ï¼è¯»å®ä¸è½å ã File blobFile = blobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey()); if (blobFile == null || !blobFile.exists()) { CrmSaleQuotationOcrRespVO error = new CrmSaleQuotationOcrRespVO(); error.setRawText("æä»¶è¯»å失败"); return error; } byte[] fileBytes = Files.readAllBytes(tempFile.toPath()); byte[] fileBytes = Files.readAllBytes(blobFile.toPath()); // 4. AI è¯å«ï¼å¾çèµ°å¤æ¨¡æï¼ææ¬èµ°çº¯ææ¬ï¼ boolean isImage = IMAGE_EXTENSIONS.contains(ext); @@ -137,10 +136,6 @@ CrmSaleQuotationOcrRespVO fallback = new CrmSaleQuotationOcrRespVO(); fallback.setRawText("AI è¯å«ææ¶ä¸å¯ç¨ï¼è¯·æå¨å½å ¥"); return fallback; } finally { if (tempFile != null && tempFile.exists()) { tempFile.delete(); } } } yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ai/ErpPurchaseInvoiceAiServiceImpl.java
@@ -90,7 +90,6 @@ @Override public ErpPurchaseInvoiceOcrRespVO ocrInvoice(ErpPurchaseInvoiceOcrReqVO reqVO) { File tempFile = null; try { // 1. ä¸è½½æä»¶ SystemStorageBlobDO blob = blobService.getStorageBlob(reqVO.getBlobId()); @@ -105,12 +104,12 @@ return errorResp("䏿¯æçæä»¶ç±»åï¼è¯·ä¸ä¼ PDF/å¾çæä»¶"); } // 3. è·åæä»¶åè tempFile = blobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey()); if (tempFile == null || !tempFile.exists()) { // 3. è·åæä»¶åèãè¿éæ¯ç£çä¸çå®åå¨çé£ä»½ blobï¼ä¸æ¯ä¸´æ¶å¯æ¬ï¼è¯»å®ä¸è½å ã File blobFile = blobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey()); if (blobFile == null || !blobFile.exists()) { return errorResp("æä»¶è¯»å失败"); } byte[] fileBytes = Files.readAllBytes(tempFile.toPath()); byte[] fileBytes = Files.readAllBytes(blobFile.toPath()); if (fileBytes.length == 0) { return errorResp("æä»¶å å®¹ä¸ºç©ºï¼æ æ³è¯å«"); } @@ -139,10 +138,6 @@ } catch (Exception e) { log.warn("AI OCR è¯å«å票失败ï¼blobId={}", reqVO.getBlobId(), e); return errorResp("AI è¯å«ææ¶ä¸å¯ç¨ï¼è¯·æå¨å½å ¥"); } finally { if (tempFile != null && tempFile.exists()) { tempFile.delete(); } } } yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/qc/MesQcReportApi.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,28 @@ package cn.iocoder.yudao.module.mes.api.qc; import cn.iocoder.yudao.module.mes.api.qc.dto.MesQcReportRespDTO; /** * MES è´¨æ£åï¼æ¥æ/è¿ç¨/åºè´§/éè´§æ£éªï¼æ°æ® API æ¥å£ * <p> * 便ºè½è´¨æ£æ¥åå¹³å°ï¼qcreport 模åï¼æè´¨æ£åæ¸²æææ¥å使ç¨ã * <p> * åªæ´é²ãä¸å¼ è´¨æ£åç宿´å¯æ¥åæ°æ®ãï¼ä¸æ´é² MES çä»»ä½ DO / Mapperï¼ * è°ç¨æ¹æ¿å°çæ¯èªè§£éçä¸å¡æ°æ®ï¼å称已解ææä¸æï¼ï¼ä¸éè¦è®¤è¯ mes_qc_* è¡¨ç»æã * æ¢å¥è¯è¯´ï¼MES æ°æ®æ¨¡åæä¹æ¹ï¼é½åªå½±åæ¬æ¥å£çå®ç°ï¼ä¸å½±åè°ç¨æ¹ã */ public interface MesQcReportApi { /** * è·åä¸å¼ è´¨æ£åç坿¥åæ°æ® * <p> * ä¸å¨è¿éåãè½ä¸è½åºæ¥åãçä¸å¡æ ¡éªï¼æªå®æãæªå¤å®çï¼ï¼åªå¦å®è¿åæ°æ®ï¼ * æ ¡éªä¸æç»ææ¡å±äºæ¥åå¹³å°çå£å¾ï¼ç±è°ç¨æ¹æ status / checkResult 夿ã * * @param qcType è´¨æ£ç±»åï¼è§ MesQcTypeEnumï¼1 æ¥æ 2 è¿ç¨ 3 åºè´§ 4 éè´§ * @param qcId è´¨æ£åç¼å· * @return è´¨æ£åæ°æ®ï¼åæ®ä¸å卿¶è¿å null */ MesQcReportRespDTO getQcReportData(Integer qcType, Long qcId); } yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/qc/dto/MesQcReportItemRespDTO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,89 @@ package cn.iocoder.yudao.module.mes.api.qc.dto; import lombok.Data; import lombok.experimental.Accessors; import java.math.BigDecimal; /** * è´¨æ£åçå个æ£éªé¡¹ï¼ä¸ä¸ªãæ ·å Ã ææ ãç»åä¸è¡ï¼ * <p> * 䏿¸¸æãä¸ä¸ªææ 被å½äºå¤æ¬¡å®æµå¼ããä¸å¼ åæå¤ä»½æ ·åã齿平æå¤è¡ï¼ * æ¥åä¾§ä¸éè¦ç¥éæ ·åä¸å®æµå¼çå±çº§å ³ç³»ï¼éè¡æ¸²æå³å¯ã * <p> * ä½ãç¶é¡¹ + å项ãè¿å±åç»å ³ç³»å¿ 须带ä¸ï¼æ¥åè¦ç¨åå¹¶åå æ ¼æç»åè·¨ä½æ´ç»ï¼ * èæ¸²ææç®ä¸åºãè¿ä¸ç»æå è¡ãï¼ç»å®çè·¯å¾å¨æ°ç»ä¸å屿§ä¼æå ç´ pluckï¼ * `group.children.length` 读ä¸å°ï¼ï¼åªè½ç±è¿è¾¹é项ç»åºã */ @Data @Accessors(chain = true) public class MesQcReportItemRespDTO { /** è¡åºå·ï¼ä» 1 å¼å§ï¼ä¸åæ®æç»é¡ºåºä¸è´ */ private Integer index; /** MES ææ ç¼å· */ private Long indicatorId; private String indicatorCode; private String indicatorName; /** æ£æµè¦æ±ï¼ååæ®è¡ï¼å¼åæ¶ä»æ£éªæ¨¡æ¿å¤å¶ï¼ */ private String checkMethod; /** æ£æµå·¥å · */ private String tool; /** æ åå¼ï¼ææ¬ï¼å¯è½ä¸æ¯æ°åï¼å¦ãæ¾æ¸ éææ¶²ä½ãï¼ */ private String standardValue; /** 宿µå¼ï¼åæ ·åç¬¦ä¸²ï¼ */ private String actualValue; /** 计éåä½åç§° */ private String unit; /** * æ¥åãæ£æµè¦æ±ãåçææ¬ã * <p> * åç»é¡¹ï¼ç»å¤´ï¼= ææ ä¸çå ¬å¼ï¼å ¶ä½ = åæ®è¡ä¸çæ åè¦æ±ã * ææè¡é½å¿ é¡»æå¼ï¼åªææ¯ç©ºä¸²ï¼ï¼ç»å®çåæ®µç¼ºå¸æ¶è¯¥åå æ ¼ä¼æ¸²æä¸ºç©ºå¹¶è®°ä¸æ¡æ°æ®ç¼ºå£ã */ private String requirement; /** * æ¥åãå项ãåçææ¬ï¼ç»å å项 = èªå·±çååï¼ç»å¤´è¡ä¸ç¬ç«é¡¹ = 空串ã * <p> * ç»å¤´è¡ä¸ç¬ç«é¡¹çååèµ°ãæ£éªé¡¹ç®ãåï¼è¿ä¸åç空ã */ private String groupChildName; /** * ãæ£éªé¡¹ç®ãåç纵ååå¹¶è¡æ°ï¼ç»å¤´è¡ = æ´ç»çè¡æ°ï¼1 + ç»å åé¡¹è¡æ°ï¼ï¼ç»å å ¶ä½è¡ä¸ç¬ç«é¡¹ = 1ã * <p> * ç»å 第äºè¡èµ·è¿ä¸æ ¼ä¼è¢«éèï¼è§ {@link #groupHidden}ï¼ï¼ç±ç»å¤´é£ä¸è¡ç rowspan è·¨ä½ï¼ * æä»¥åªæç»å¤´è¡éè¦çæ£çè¡æ°ã */ private Integer groupSpan; /** * ãæ£éªé¡¹ç®ãæ ¼åçéèå¼ï¼ç»å 第äºè¡èµ· = `hidden`ï¼ç»å¤´è¡ä¸ç¬ç«é¡¹ = 空串ã * <p> * ç¨å符串è䏿¯å¸å°æ¯å 为è¿ä¸ªå±æ§é ãå¨ä¸å¨ãèµ·ä½ç¨ï¼`hidden="false"` ç §æ ·éèï¼ï¼ * åªæç©ºä¸²è½è¡¨è¾¾ãä¸è¾åºè¿ä¸ªå±æ§ = è¿ä¸æ ¼å¯è§ãã */ private String groupHidden; /** è§æ ¼ä¸éï¼ç¼ºå¤±ä¸º null */ private BigDecimal upperLimit; /** è§æ ¼ä¸éï¼ç¼ºå¤±ä¸º null */ private BigDecimal lowerLimit; /** å¼ç±»åï¼è§ MesQcResultValueTypeEnumï¼1 æµ®ç¹ 2 æ´æ° 3 ææ¬ 4 åå ¸ 5 æä»¶ */ private Integer valueType; /** å¼ç±»ååç§°ï¼å¦ãæµ®ç¹ã */ private String valueTypeName; /** æ ·åå·ï¼åæ®æªå¡«æ ·åå·æ¶ä¸ºç©ºï¼ */ private String sampleNo; /** 夿³¨ */ private String remark; } yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/qc/dto/MesQcReportRespDTO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,116 @@ package cn.iocoder.yudao.module.mes.api.qc.dto; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; /** * è´¨æ£åç坿¥åæ°æ®ï¼å头 + æ£éªé¡¹ï¼ * <p> * å类质æ£åï¼æ¥æ/è¿ç¨/åºè´§/éè´§ï¼å ±ç¨æ¬ç»æï¼åèªç¬æçåæ®µï¼ä¾åºåã客æ·ãå·¥åãæ¹æ¬¡ï¼ * 齿¾è¿åä¸ç»å段éï¼ç¨ä¸ä¸çç空ï¼è°ç¨æ¹ä¸å¿ æç±»å忝ã */ @Data public class MesQcReportRespDTO { // ==================== åæ®æ è¯ ==================== /** è´¨æ£ç±»åï¼1 æ¥æ 2 è¿ç¨ 3 åºè´§ 4 éè´§ */ private Integer qcType; /** è´¨æ£ç±»ååç§°ï¼å¦ãæ¥ææ£éªã */ private String qcTypeName; private Long qcId; /** åå·ï¼å¦ IQC_20260917001 */ private String qcCode; /** åæ®åç§°ï¼å¦ãç²¾èæ¥ææ£éª-0917ã */ private String qcName; /** åæ®ç¶æï¼è§ MesQcStatusEnum */ private Integer status; private String statusName; /** * æ¯å¦å·²å®ææ£éªã * <p> * ç´æ¥ç»ç»è®ºè䏿¯è®©è°ç¨æ¹å»æ¯ {@code status} çæ°å¼ï¼é£ä¸ªæ°å¼çå«ä¹åªå¨ MES ä¾§æç«ï¼ * è°ç¨æ¹ä¸è¯¥ä¸ºäºå¤æãè½ä¸è½åºæ¥åãå»è®¤è¯ MES çç¶æç 表ã */ private boolean finished; /** æ£éªå¤å®ï¼1 åæ ¼ 2 ç¹é 3 ä¸åæ ¼éè´§ 4 ä¸åæ ¼æ¥åºï¼æªå¤å®ä¸º null */ private Integer checkResult; private String checkResultText; /** 对åºç MES æ£éªæ¨¡æ¿ç¼å·ï¼ä¸æ¯æ¥å模æ¿ï¼ */ private Long templateId; /** æ¥æºåæ®ç±»åä¸åå· */ private Integer sourceDocType; private String sourceDocCode; // ==================== ç©æ ==================== private Long itemId; private String itemCode; private String itemName; private String itemSpecification; /** ç©æç计éåä½åç§° */ private String unitName; // ==================== åç±»åç¬æ ==================== /** ä¾åºååç§°ï¼æ¥ææ£éªï¼ */ private String vendorName; /** ä¾åºåæ¹æ¬¡ï¼æ¥ææ£éªï¼ */ private String vendorBatch; /** 客æ·åç§°ï¼åºè´§æ£éªï¼ */ private String clientName; /** æ¹æ¬¡å·ï¼åºè´§ / éè´§æ£éªï¼ */ private String batchCode; /** ç产工åå·ï¼è¿ç¨æ£éªï¼ */ private String workOrderCode; // ==================== æ°éä¸å¤å® ==================== private BigDecimal checkQuantity; private BigDecimal qualifiedQuantity; private BigDecimal unqualifiedQuantity; private LocalDateTime inspectDate; /** æ£éªåå§å */ private String inspectorName; private String remark; // ==================== æç» ==================== /** * æ£éªé¡¹å表ï¼å·²æãæ ·å Ã ææ ãæå¹³ï¼å¹¶æåç»ç»ææå¥½åº * ï¼ç»å¤´è¡å¨åãç»å å项紧è·å ¶åï¼ç»ä¸ç¬ç«é¡¹ä¹é´æææ çæåºå·ï¼ã */ private List<MesQcReportItemRespDTO> items = new ArrayList<>(); /** * 卿¬åéæ²¡æå项ãå èæ²¡è½åå¹¶æç»çåç»é¡¹åç§°ï¼å¦ãæ£ä¸éå«éãä½åªéäºå®èªå·±ï¼ã * <p> * è¿ç±»ææ æç¬ç«æ£éªé¡¹ç §å¸¸åºå¨æ¥åéï¼åªæ¯ç»åè·¨ä¸ä½ä»»ä½è¡ï¼ * ä½ãæ¬è¯¥æ¯ç»å´æ²¡ç»èµ·æ¥ãå¿ é¡»è®©è°ç¨æ¹ç¥éã */ private List<String> childlessGroupNames = new ArrayList<>(); /** * 卿¬åéæ²¡æèªå·±çæ£éªè¡ã被补åºä¸è¡æ¥æ¾ç»åä¸å ¬å¼çåç»é¡¹åç§°ã * <p> * æ£éªè¡æ¯ä»æ£éªæ¨¡æ¿çæçï¼æ¨¡æ¿åªéäºå项没éç»å¤´æ¶å°±ä¼è¿æ ·ã * è¡¥ä¸è¡æ¯ä¸ºäºä¸æç»ååå ¬å¼ä¸¢æï¼é£æ£æ¯åç»æ£éªé¡¹è¦è¿åçä¸è¥¿ï¼ï¼ * ä½ãè¿ä¸è¡ä¸æ¯åæ®ä¸çåå§è¡ãå¾è®©è°ç¨æ¹ç¥éã */ private List<String> synthesizedGroupNames = new ArrayList<>(); /** åæ®è¡å¼ç¨äºå·²è¢«å é¤çææ ãå èè·³è¿çæ¡æ° */ private int missingIndicatorCount; } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/qc/MesQcReportApiImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,632 @@ package cn.iocoder.yudao.module.mes.api.qc; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi; import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO; import cn.iocoder.yudao.module.mes.api.qc.dto.MesQcReportItemRespDTO; import cn.iocoder.yudao.module.mes.api.qc.dto.MesQcReportRespDTO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDetailDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcLineDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcLineDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcLineDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.rqc.MesQcRqcDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.rqc.MesQcRqcLineDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO; import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum; import cn.iocoder.yudao.module.mes.enums.qc.MesQcIndicatorItemTypeEnum; import cn.iocoder.yudao.module.mes.enums.qc.MesQcResultValueTypeEnum; import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService; import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService; import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcLineService; import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService; import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcLineService; import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService; import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcLineService; import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService; import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcLineService; import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcService; import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi; import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; /** * MES è´¨æ£åæ°æ® API å®ç°ç±» * <p> * å类质æ£åçè¡è¡¨ç»æå®å ¨ä¸è´ï¼ä» ç¶é®åä¸åï¼iqc_id / ipqc_id / oqc_id / rqc_idï¼ï¼ * æä»¥ãåè¡ãåªåå个èéé æ¹æ³ï¼æ å°é»è¾ä¸ä»½å ±ç¨ã */ @Service @Validated public class MesQcReportApiImpl implements MesQcReportApi { /** * ç»ååå¹¶æ ¼éãè¿ä¸æ ¼è®©ä½ç»ä¸é¢é£ä¸è¡ãçæ è®°å¼ã * <p> * æ¥åä¾§æå®åæ ·åæ HTML ç {@code hidden} 屿§ï¼æä»¥å¼æ¬èº«æ¯ä»ä¹ä¸éè¦ï¼ * éè¦çæ¯å®é空 ââ 空串å¨ä¸¤ä¾§æ¸²æå¨éé½è¡¨ç¤ºãä¸è¾åºè¿ä¸ªå±æ§ãï¼ä¹å°±æ¯è¿ä¸æ ¼ç §å¸¸å¯è§ã */ private static final String GROUP_CELL_HIDDEN = "hidden"; @Resource private MesQcIqcService iqcService; @Resource private MesQcIpqcService ipqcService; @Resource private MesQcOqcService oqcService; @Resource private MesQcRqcService rqcService; @Resource private MesQcIqcLineService iqcLineService; @Resource private MesQcIpqcLineService ipqcLineService; @Resource private MesQcOqcLineService oqcLineService; @Resource private MesQcRqcLineService rqcLineService; @Resource private MesQcIndicatorResultService indicatorResultService; @Resource private MesQcIndicatorService indicatorService; @Resource private MesMdItemService itemService; @Resource private MesMdUnitMeasureService unitMeasureService; @Resource private MesProWorkOrderService workOrderService; @Resource private AdminUserApi adminUserApi; @Resource private SrmSupplierApi srmSupplierApi; @Resource private CrmCustomerApi customerApi; @Override public MesQcReportRespDTO getQcReportData(Integer qcType, Long qcId) { if (qcType == null || qcId == null) { return null; } MesQcReportRespDTO resp; List<LineRow> lines; Long inspectorUserId; if (Objects.equals(qcType, MesQcTypeEnum.IQC.getType())) { MesQcIqcDO order = iqcService.getIqc(qcId); if (order == null) { return null; } resp = common(qcType, order.getId(), order.getCode(), order.getName(), order.getTemplateId(), order.getSourceDocType(), order.getSourceDocCode(), order.getItemId(), order.getCheckQuantity(), order.getQualifiedQuantity(), order.getUnqualifiedQuantity(), order.getCheckResult(), order.getInspectDate(), order.getStatus(), order.getRemark()); resp.setVendorBatch(defaultStr(order.getVendorBatch())); resp.setVendorName(supplierName(order.getVendorId())); lines = loadIqcLines(qcId); inspectorUserId = order.getInspectorUserId(); } else if (Objects.equals(qcType, MesQcTypeEnum.IPQC.getType())) { MesQcIpqcDO order = ipqcService.getIpqc(qcId); if (order == null) { return null; } resp = common(qcType, order.getId(), order.getCode(), order.getName(), order.getTemplateId(), order.getSourceDocType(), order.getSourceDocCode(), order.getItemId(), order.getCheckQuantity(), order.getQualifiedQuantity(), order.getUnqualifiedQuantity(), order.getCheckResult(), order.getInspectDate(), order.getStatus(), order.getRemark()); resp.setWorkOrderCode(workOrderCode(order.getWorkOrderId())); lines = loadIpqcLines(qcId); inspectorUserId = order.getInspectorUserId(); } else if (Objects.equals(qcType, MesQcTypeEnum.OQC.getType())) { MesQcOqcDO order = oqcService.getOqc(qcId); if (order == null) { return null; } resp = common(qcType, order.getId(), order.getCode(), order.getName(), order.getTemplateId(), order.getSourceDocType(), order.getSourceDocCode(), order.getItemId(), order.getCheckQuantity(), order.getQualifiedQuantity(), order.getUnqualifiedQuantity(), order.getCheckResult(), order.getInspectDate(), order.getStatus(), order.getRemark()); resp.setBatchCode(defaultStr(order.getBatchCode())); resp.setClientName(clientName(order.getClientId())); lines = loadOqcLines(qcId); inspectorUserId = order.getInspectorUserId(); } else if (Objects.equals(qcType, MesQcTypeEnum.RQC.getType())) { MesQcRqcDO order = rqcService.getRqc(qcId); if (order == null) { return null; } resp = common(qcType, order.getId(), order.getCode(), order.getName(), order.getTemplateId(), order.getSourceDocType(), order.getSourceDocCode(), order.getItemId(), order.getCheckQuantity(), order.getQualifiedQuantity(), order.getUnqualifiedQuantity(), order.getCheckResult(), order.getInspectDate(), order.getStatus(), order.getRemark()); resp.setBatchCode(defaultStr(order.getBatchCode())); lines = loadRqcLines(qcId); inspectorUserId = order.getInspectorUserId(); } else { throw new IllegalArgumentException("æä¸æ¯æ qcType=" + qcType); } // ç©æä¸åä½ï¼åä½ä¼å åè¡ä¸çï¼æ£éªæ¶å®é ç¨çåä½ï¼ï¼ç¼ºå¤±æåéç©æçé»è®¤åä½ MesMdItemDO item = resp.getItemId() == null ? null : itemService.getItemMap(Collections.singletonList(resp.getItemId())).get(resp.getItemId()); if (item != null) { resp.setItemCode(defaultStr(item.getCode())); resp.setItemName(defaultStr(item.getName())); resp.setItemSpecification(defaultStr(item.getSpecification())); } Map<Long, MesMdUnitMeasureDO> unitMap = unitMapOf(lines, item); if (item != null) { MesMdUnitMeasureDO itemUnit = unitMap.get(item.getUnitMeasureId()); resp.setUnitName(itemUnit == null ? "" : defaultStr(itemUnit.getName())); } resp.setInspectorName(userName(inspectorUserId)); BuiltItems built = buildItems(resp.getQcId(), qcType, lines, unitMap, item); resp.setItems(built.items()); resp.setChildlessGroupNames(built.childlessGroupNames()); resp.setSynthesizedGroupNames(built.synthesizedGroupNames()); resp.setMissingIndicatorCount(built.missingIndicatorCount()); return resp; } /** åç±»åæ®å ±æçå头忮µ */ private MesQcReportRespDTO common(Integer qcType, Long qcId, String code, String name, Long templateId, Integer sourceDocType, String sourceDocCode, Long itemId, BigDecimal checkQuantity, BigDecimal qualifiedQuantity, BigDecimal unqualifiedQuantity, Integer checkResult, LocalDateTime inspectDate, Integer status, String remark) { return new MesQcReportRespDTO() .setQcType(qcType) .setQcTypeName(qcTypeName(qcType)) .setQcId(qcId) .setQcCode(defaultStr(code)) .setQcName(defaultStr(name)) .setTemplateId(templateId) .setSourceDocType(sourceDocType) .setSourceDocCode(defaultStr(sourceDocCode)) .setItemId(itemId) .setCheckQuantity(checkQuantity) .setQualifiedQuantity(qualifiedQuantity) .setUnqualifiedQuantity(unqualifiedQuantity) .setCheckResult(checkResult) .setCheckResultText(checkResultName(checkResult)) .setInspectDate(inspectDate) .setStatus(status) .setStatusName(statusName(status)) .setFinished(Objects.equals(status, MesQcStatusEnum.FINISHED.getStatus())) .setRemark(defaultStr(remark)); } /** * æ£éªé¡¹ = ãæ ·å Ã ææ ãä¸è¡ï¼æåç»ç»ææå¥½åºã * <p> * å½å±åªçææ ç {@code parent_id}ï¼è¢«æ¬åéå«çè¡ç¨ parent_id æè®¤çåç»é¡¹å°±æ¯ç»å¤´ï¼ * å®èªå·±é£ä¸è¡è¿åææå项è¡åæä¸ç»ï¼ç»å卿¥åé纵ååå¹¶ãä¸å¤å»æä¸è¿ä¹åï¼ * ä¸ç¨ {@code result_type IS NULL} å¤åç»ï¼æ¹åæ¯åçï¼ä¸¢æçæ°å¥½æ¯å¸¦å ¬å¼çç»åã * çä¸çæ°å¥½æ¯è¿ç¨åæ°ï¼ï¼ä¸é è¡åºï¼è¡è¡¨éç»å¤´ä¸å®çå项并ä¸ç¸é»ï¼ï¼ä¹ä¸æç»å¤´ä¸¢æ * ï¼ç»åä¸å ¬å¼æ¯åç»æ£éªé¡¹æä¸è¯¥ä¸¢çä¸¤æ ·ä¸è¥¿ï¼ã */ private BuiltItems buildItems(Long qcId, Integer qcType, List<LineRow> lines, Map<Long, MesMdUnitMeasureDO> unitMap, MesMdItemDO item) { if (CollUtil.isEmpty(lines)) { return new BuiltItems(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), 0); } Map<Long, MesQcIndicatorDO> indicatorMap = indicatorService.getIndicatorMap( convertSet(lines, LineRow::indicatorId)); // æ ·å â è¯¥æ ·åç宿µå¼æç» Map<Long, List<MesQcIndicatorResultDetailDO>> detailMap = Collections.emptyMap(); List<MesQcIndicatorResultDO> samples = indicatorResultService.getIndicatorResultListByQcIdAndType(qcId, qcType); if (CollUtil.isNotEmpty(samples)) { List<MesQcIndicatorResultDetailDO> details = indicatorResultService.getIndicatorResultDetailListByResultIds( new ArrayList<>(convertSet(samples, MesQcIndicatorResultDO::getId))); detailMap = new LinkedHashMap<>(); for (MesQcIndicatorResultDetailDO detail : details) { detailMap.computeIfAbsent(detail.getResultId(), k -> new ArrayList<>()).add(detail); } } // â è¡ â ææ ï¼ææ è¢«å æçè¡è¿åä¸åºä»»ä½ä¸è¥¿ï¼åªè®¡æ° List<LineRow> liveLines = new ArrayList<>(lines.size()); List<MesQcIndicatorDO> liveIndicators = new ArrayList<>(lines.size()); int missingIndicatorCount = 0; for (LineRow line : lines) { MesQcIndicatorDO indicator = indicatorMap.get(line.indicatorId()); if (indicator == null) { missingIndicatorCount++; continue; } liveLines.add(line); liveIndicators.add(indicator); } // â¡ ç»å¤´ææ å¯è½åæ ¹æ²¡æè¿æ¬åï¼æ¨¡æ¿åªéäºå项ï¼ï¼è¡¥ååºæ¥ï¼å¦åæ´ç»çç»åä¸å ¬å¼ä¼ä¸¢ Map<Long, MesQcIndicatorDO> allIndicators = new LinkedHashMap<>(indicatorMap); Set<Long> extraIds = new LinkedHashSet<>(); for (MesQcIndicatorDO indicator : liveIndicators) { Long parentId = indicator.getParentId(); if (parentId != null && parentId != 0 && !allIndicators.containsKey(parentId)) { extraIds.add(parentId); } } if (CollUtil.isNotEmpty(extraIds)) { allIndicators.putAll(indicatorService.getIndicatorMap(extraIds)); } // ⢠æ¯ä¸è¡å½å°ãç»å¤´èªå·±çè¡ãæãæä¸ªç»çå项è¡ã Map<Long, List<Integer>> ownLinesOf = new LinkedHashMap<>(); Map<Long, List<Integer>> childLinesOf = new LinkedHashMap<>(); for (int i = 0; i < liveLines.size(); i++) { Long headId = groupHeadId(liveIndicators.get(i), allIndicators); if (headId == null) { ownLinesOf.computeIfAbsent(liveLines.get(i).indicatorId(), k -> new ArrayList<>()).add(i); } else { childLinesOf.computeIfAbsent(headId, k -> new ArrayList<>()).add(i); } } // â£ é¡¶å±æ¡ç® = ç» + ç¬ç«é¡¹ï¼ç»å¤´é£ä¸è¡ä»ç¬ç«é¡¹éæåºæ¥å½ä½ç»çéç¹è¡ List<String> childlessGroupNames = new ArrayList<>(); List<String> synthesizedGroupNames = new ArrayList<>(); List<Entry> entries = new ArrayList<>(); for (Map.Entry<Long, List<Integer>> each : childLinesOf.entrySet()) { Long headId = each.getKey(); MesQcIndicatorDO head = allIndicators.get(headId); List<Integer> children = sortByIndicatorOrder(each.getValue(), liveIndicators); List<Integer> own = ownLinesOf.remove(headId); if (CollUtil.isEmpty(own)) { synthesizedGroupNames.add(defaultStr(head.getName())); } entries.add(new Entry(head, own == null ? Collections.emptyList() : own, children, children.get(0))); } for (Map.Entry<Long, List<Integer>> each : ownLinesOf.entrySet()) { MesQcIndicatorDO indicator = allIndicators.get(each.getKey()); if (MesQcIndicatorItemTypeEnum.isGroup(indicator.getItemType())) { // ææ ä¸»æ°æ®éæ¯åç»é¡¹ï¼æ¬åå´æ²¡æå±äºå®çå项ï¼ç §å¸¸åºæç¬ç«æ£éªé¡¹ï¼åªæ¯ç»ä¸èµ·æ¥ childlessGroupNames.add(defaultStr(indicator.getName())); } entries.add(new Entry(indicator, each.getValue(), Collections.emptyList(), each.getValue().get(0))); } // æåºå·ç¼ºå¤±çææåï¼åå¼ä¿æè¡åºï¼List.sort 稳å®ï¼æ¯è¾é®éå带ä¸é¦è¡ä¸æ å åºï¼ entries.sort(Comparator.comparingInt((Entry entry) -> sortKey(entry.indicator())) .thenComparingInt(Entry::firstLine)); // â¤ éæ¡ç®äº§åºè¡ï¼å¹¶æåç»ä¿¡æ¯ä¸åå°æ¯ä¸è¡ List<MesQcReportItemRespDTO> items = new ArrayList<>(); int index = 0; for (Entry entry : entries) { List<MesQcReportItemRespDTO> groupRows = new ArrayList<>(); for (Integer lineIndex : entry.ownLines()) { groupRows.addAll(expandRows(liveIndicators.get(lineIndex), liveLines.get(lineIndex), samples, detailMap, unitMap, item)); } boolean grouped = CollUtil.isNotEmpty(entry.childLines()); if (grouped && groupRows.isEmpty()) { // ç»å¤´æ²¡æèªå·±çè¡ï¼è¡¥ä¸è¡ï¼å¦åç»åä¸å ¬å¼æ å¤å¯æ¾ groupRows.add(toItem(entry.indicator(), emptyLine(entry.indicator()), "", "", "", unitMap, item)); } int anchorCount = groupRows.size(); List<MesQcReportItemRespDTO> entryRows = new ArrayList<>(groupRows); for (Integer lineIndex : entry.childLines()) { for (MesQcReportItemRespDTO row : expandRows(liveIndicators.get(lineIndex), liveLines.get(lineIndex), samples, detailMap, unitMap, item)) { row.setGroupChildName(defaultStr(liveIndicators.get(lineIndex).getName())); entryRows.add(row); } } int span = grouped ? entryRows.size() : 1; for (int i = 0; i < entryRows.size(); i++) { MesQcReportItemRespDTO row = entryRows.get(i); row.setIndex(++index); // ç»åçåå¹¶æ ¼åªæå¨ç»ç第ä¸è¡ï¼ç»å å ¶ä½è¡æ´æ ¼éèã让ä½ç»å® row.setGroupSpan(i == 0 ? span : 1); row.setGroupHidden(i == 0 ? "" : GROUP_CELL_HIDDEN); row.setRequirement(i < anchorCount ? anchorRequirement(entry.indicator(), row) : defaultStr(row.getStandardValue())); items.add(row); } } return new BuiltItems(items, childlessGroupNames, synthesizedGroupNames, missingIndicatorCount); } /** è£ é ç»æï¼æ£éªé¡¹æ¬èº«ï¼ä»¥åæ²¡è½æç»ã被补äºè¡èå¿ é¡»è®©è°ç¨æ¹ç¥éçé£äº */ private record BuiltItems(List<MesQcReportItemRespDTO> items, List<String> childlessGroupNames, List<String> synthesizedGroupNames, int missingIndicatorCount) { } /** * ä¸ä¸ªé¡¶å±æ¡ç®ï¼ä¸ä¸ªåç»ï¼ç»å¤´ææ + å®çå项è¡ï¼ï¼æä¸ä¸ªç¬ç«æ£éªé¡¹ã * * @param indicator ç»å¤´çææ / ç¬ç«é¡¹çææ * @param ownLines ç»å¤´èªå·±é£äºè¡ï¼ç¬ç«é¡¹æ¶å°±æ¯å®å ¨é¨çè¡ï¼å¨è¡è¡¨éç䏿 * @param childLines ç»å å项è¡ç䏿 ï¼ç¬ç«é¡¹æä¸ºç©º * @param firstLine æ¡ç®ç¬¬ä¸è¡å¨è¡è¡¨éç䏿 ï¼æåºå·ç¸åæ¶é å®ä¿æè¡åº */ private record Entry(MesQcIndicatorDO indicator, List<Integer> ownLines, List<Integer> childLines, int firstLine) { } /** * è¿ä¸è¡çç»å¤´ææ ç¼å·ï¼{@code parent_id} æåä¸ä¸ªãåç»é¡¹ãæç®æ°ã * <p> * æåå½å ¥é¡¹å±äºææ ä¸»æ°æ®é ç½®æé®é¢ï¼ä¸è½æ¿å®å»åå¹¶åå æ ¼ï¼ * æåçææ 已被å é¤åæ ·ä¸æç»ã */ private static Long groupHeadId(MesQcIndicatorDO indicator, Map<Long, MesQcIndicatorDO> allIndicators) { Long parentId = indicator.getParentId(); if (parentId == null || parentId == 0) { return null; } MesQcIndicatorDO head = allIndicators.get(parentId); return head != null && MesQcIndicatorItemTypeEnum.isGroup(head.getItemType()) ? parentId : null; } /** ç»å¤´çãæ£æµè¦æ±ãæ¾å ¬å¼ï¼åç»é¡¹åªæå ¬å¼ã没æè§æ ¼ï¼ï¼æ²¡é å ¬å¼å°±ç §å¸¸æ¾æ åè¦æ± */ private static String anchorRequirement(MesQcIndicatorDO head, MesQcReportItemRespDTO row) { String formula = defaultStr(head.getFormulaText()); return formula.isEmpty() ? defaultStr(row.getStandardValue()) : formula; } /** ç»å¤´æ²¡æè¿æ¬åæ¶è¡¥åºæ¥çé£ä¸è¡ï¼åªæææ æ¬èº«ï¼æ²¡æåæ®è¡ */ private static LineRow emptyLine(MesQcIndicatorDO head) { return new LineRow(head.getId(), head.getTool(), null, null, null, null, null, null); } /** * ä¸ä¸ªæ£éªé¡¹æå¹³æè¥å¹²è¡ï¼å¤æ ·åãä¸ä¸ªææ 被å½äºå¤æ¬¡å®æµå¼é½ä¼æå¹³ã * ä¸è¡é½æ²¡æï¼å°æªå½å ¥å®æµå¼ï¼æ¶ä»åºä¸è¡ï¼é¿å æ¥åæ¼é¡¹ã */ private List<MesQcReportItemRespDTO> expandRows(MesQcIndicatorDO indicator, LineRow line, List<MesQcIndicatorResultDO> samples, Map<Long, List<MesQcIndicatorResultDetailDO>> detailMap, Map<Long, MesMdUnitMeasureDO> unitMap, MesMdItemDO item) { List<MesQcReportItemRespDTO> rows = new ArrayList<>(); for (MesQcIndicatorResultDO sample : samples) { for (MesQcIndicatorResultDetailDO detail : detailMap.getOrDefault(sample.getId(), Collections.emptyList())) { if (Objects.equals(detail.getIndicatorId(), line.indicatorId())) { rows.add(toItem(indicator, line, sample.getCode(), detail.getValue(), detail.getRemark(), unitMap, item)); } } } if (rows.isEmpty()) { rows.add(toItem(indicator, line, "", "", "", unitMap, item)); } return rows; } /** æåºé®ï¼ææ 没设æåºå·çä¸å¾ææå */ private static int sortKey(MesQcIndicatorDO indicator) { Integer sortOrder = indicator.getSortOrder(); return sortOrder == null ? Integer.MAX_VALUE : sortOrder; } /** æåè¡çææ æåºå·æï¼åå¼ä¿æè¡åº */ private static List<Integer> sortByIndicatorOrder(List<Integer> lineIndexes, List<MesQcIndicatorDO> indicators) { List<Integer> sorted = new ArrayList<>(lineIndexes); sorted.sort(Comparator.comparingInt((Integer i) -> sortKey(indicators.get(i))) .thenComparingInt(Integer::intValue)); return sorted; } private MesQcReportItemRespDTO toItem(MesQcIndicatorDO indicator, LineRow line, String sampleCode, String value, String detailRemark, Map<Long, MesMdUnitMeasureDO> unitMap, MesMdItemDO item) { MesMdUnitMeasureDO unit = unitMap.get(line.unitMeasureId()); if (unit == null && item != null) { unit = unitMap.get(item.getUnitMeasureId()); } return new MesQcReportItemRespDTO() .setIndicatorId(indicator.getId()) .setIndicatorCode(defaultStr(indicator.getCode())) .setIndicatorName(defaultStr(indicator.getName())) .setCheckMethod(defaultStr(line.checkMethod())) .setTool(defaultStr(line.tool())) .setStandardValue(plain(line.standardValue())) .setActualValue(defaultStr(value)) .setUnit(unit == null ? "" : defaultStr(unit.getName())) .setUpperLimit(line.maxThreshold()) .setLowerLimit(line.minThreshold()) .setValueType(indicator.getResultType()) .setValueTypeName(valueTypeName(indicator.getResultType())) .setSampleNo(defaultStr(sampleCode)) // é»è®¤ã䏿¯å项ãä¸åä¸åå¹¶ãï¼çå项ç±è£ é é£ä¸æ¥æ¹å .setGroupChildName("") .setGroupSpan(1) .setGroupHidden("") .setRemark(mergeRemark(line.remark(), detailRemark)); } // ==================== åè¡ï¼åç±»åæï¼ ==================== private List<LineRow> loadIqcLines(Long qcId) { List<MesQcIqcLineDO> lines = iqcLineService.getIqcLineListByIqcId(qcId); if (CollUtil.isEmpty(lines)) { return Collections.emptyList(); } List<LineRow> rows = new ArrayList<>(lines.size()); for (MesQcIqcLineDO l : lines) { rows.add(new LineRow(l.getIndicatorId(), l.getTool(), l.getCheckMethod(), l.getStandardValue(), l.getUnitMeasureId(), l.getMaxThreshold(), l.getMinThreshold(), l.getRemark())); } return rows; } private List<LineRow> loadIpqcLines(Long qcId) { List<MesQcIpqcLineDO> lines = ipqcLineService.getIpqcLineListByIpqcId(qcId); if (CollUtil.isEmpty(lines)) { return Collections.emptyList(); } List<LineRow> rows = new ArrayList<>(lines.size()); for (MesQcIpqcLineDO l : lines) { rows.add(new LineRow(l.getIndicatorId(), l.getTool(), l.getCheckMethod(), l.getStandardValue(), l.getUnitMeasureId(), l.getMaxThreshold(), l.getMinThreshold(), l.getRemark())); } return rows; } private List<LineRow> loadOqcLines(Long qcId) { List<MesQcOqcLineDO> lines = oqcLineService.getOqcLineListByOqcId(qcId); if (CollUtil.isEmpty(lines)) { return Collections.emptyList(); } List<LineRow> rows = new ArrayList<>(lines.size()); for (MesQcOqcLineDO l : lines) { rows.add(new LineRow(l.getIndicatorId(), l.getTool(), l.getCheckMethod(), l.getStandardValue(), l.getUnitMeasureId(), l.getMaxThreshold(), l.getMinThreshold(), l.getRemark())); } return rows; } private List<LineRow> loadRqcLines(Long qcId) { List<MesQcRqcLineDO> lines = rqcLineService.getRqcLineListByRqcId(qcId); if (CollUtil.isEmpty(lines)) { return Collections.emptyList(); } List<LineRow> rows = new ArrayList<>(lines.size()); for (MesQcRqcLineDO l : lines) { rows.add(new LineRow(l.getIndicatorId(), l.getTool(), l.getCheckMethod(), l.getStandardValue(), l.getUnitMeasureId(), l.getMaxThreshold(), l.getMinThreshold(), l.getRemark())); } return rows; } // ==================== å ³èåç§° ==================== private Map<Long, MesMdUnitMeasureDO> unitMapOf(List<LineRow> lines, MesMdItemDO item) { List<Long> unitIds = new ArrayList<>(); for (LineRow line : lines) { if (line.unitMeasureId() != null) { unitIds.add(line.unitMeasureId()); } } if (item != null && item.getUnitMeasureId() != null) { unitIds.add(item.getUnitMeasureId()); } return unitIds.isEmpty() ? Collections.emptyMap() : unitMeasureService.getUnitMeasureMap(unitIds); } private String supplierName(Long vendorId) { if (vendorId == null) { return ""; } List<SrmSupplierRespDTO> suppliers = srmSupplierApi.getSupplierList(Collections.singleton(vendorId)) .getCheckedData(); return CollUtil.isEmpty(suppliers) ? "" : defaultStr(suppliers.get(0).getName()); } private String clientName(Long clientId) { if (clientId == null) { return ""; } CrmCustomerRespDTO client = customerApi.getCustomerMap(Collections.singleton(clientId)).get(clientId); return client == null ? "" : defaultStr(client.getName()); } private String workOrderCode(Long workOrderId) { if (workOrderId == null) { return ""; } MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId); return workOrder == null ? "" : defaultStr(workOrder.getCode()); } private String userName(Long userId) { if (userId == null) { return ""; } AdminUserRespDTO user = adminUserApi.getUserMap(Collections.singleton(userId)).get(userId); return user == null ? "" : defaultStr(user.getNickname()); } // ==================== å°å·¥å · ==================== /** å»ææ æä¹çå°æ°å°¾é¶ï¼é¿å æ¥åéåºç° 5.0000 è¿ç§ç±æ°æ®åºç²¾åº¦å¸¦åºæ¥ç尾巴 */ private static String plain(BigDecimal value) { return value == null ? "" : value.stripTrailingZeros().toPlainString(); } private static String mergeRemark(String lineRemark, String detailRemark) { if (StrUtil.isBlank(lineRemark)) { return defaultStr(detailRemark); } return StrUtil.isBlank(detailRemark) ? lineRemark : lineRemark + "ï¼" + detailRemark; } private static String defaultStr(String value) { return value == null ? "" : value; } private static String qcTypeName(Integer qcType) { for (MesQcTypeEnum e : MesQcTypeEnum.values()) { if (Objects.equals(e.getType(), qcType)) { return e.getName(); } } return ""; } private static String statusName(Integer status) { for (MesQcStatusEnum e : MesQcStatusEnum.values()) { if (Objects.equals(e.getStatus(), status)) { return e.getName(); } } return ""; } private static String checkResultName(Integer checkResult) { for (MesQcCheckResultEnum e : MesQcCheckResultEnum.values()) { if (Objects.equals(e.getType(), checkResult)) { return e.getName(); } } return ""; } private static String valueTypeName(Integer resultType) { for (MesQcResultValueTypeEnum e : MesQcResultValueTypeEnum.values()) { if (Objects.equals(e.getType(), resultType)) { return e.getName(); } } return ""; } /** åç±»è¡è¡¨å段å®å ¨ä¸è´ï¼æ½æç»ä¸å½¢æåå ±ç¨æ å° */ private record LineRow(Long indicatorId, String tool, String checkMethod, BigDecimal standardValue, Long unitMeasureId, BigDecimal maxThreshold, BigDecimal minThreshold, String remark) { } } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/indicator/MesQcIndicatorController.java
@@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.mes.controller.admin.qc.indicator; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageParam; @@ -23,9 +24,13 @@ import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Set; import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @Tag(name = "管çåå° - MES è´¨æ£ææ ") @RestController @@ -74,7 +79,17 @@ @PreAuthorize("@ss.hasPermission('mes:qc-indicator:query')") public CommonResult<PageResult<MesQcIndicatorRespVO>> getIndicatorPage(@Valid MesQcIndicatorPageReqVO pageReqVO) { PageResult<MesQcIndicatorDO> pageResult = indicatorService.getIndicatorPage(pageReqVO); return success(BeanUtils.toBean(pageResult, MesQcIndicatorRespVO.class)); PageResult<MesQcIndicatorRespVO> voPageResult = BeanUtils.toBean(pageResult, MesQcIndicatorRespVO.class); fillParentName(voPageResult.getList()); return success(voPageResult); } @GetMapping("/group-list") @Operation(summary = "è·å¾åç»é¡¹ç²¾ç®å表", description = "åªå å«åç»é¡¹ï¼ä¸»è¦ç¨äºãæå±åç»ãç䏿é项") @PreAuthorize("@ss.hasPermission('mes:qc-indicator:query')") public CommonResult<List<MesQcIndicatorRespVO>> getIndicatorGroupList() { List<MesQcIndicatorDO> list = indicatorService.getIndicatorGroupList(); return success(BeanUtils.toBean(list, MesQcIndicatorRespVO.class)); } @GetMapping("/export-excel") @@ -85,9 +100,25 @@ HttpServletResponse response) throws IOException { pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List<MesQcIndicatorDO> list = indicatorService.getIndicatorPage(pageReqVO).getList(); // ç»è£ å¯¼åº VOï¼è¡¥é½æå±åç»åç§°ï¼ List<MesQcIndicatorRespVO> excelList = BeanUtils.toBean(list, MesQcIndicatorRespVO.class); fillParentName(excelList); // å¯¼åº Excel ExcelUtils.write(response, "è´¨æ£ææ .xls", "æ°æ®", MesQcIndicatorRespVO.class, BeanUtils.toBean(list, MesQcIndicatorRespVO.class)); ExcelUtils.write(response, "è´¨æ£ææ .xls", "æ°æ®", MesQcIndicatorRespVO.class, excelList); } /** * åå¡«ãæå±åç»ãåç§°ãç¶ææ å¯è½ä¸å¨å½å页ï¼çè³ä¸å¨çéç»æå ï¼ï¼æä»¥åç¬æ id æ¹éå䏿¬¡ */ private void fillParentName(List<MesQcIndicatorRespVO> list) { Set<Long> parentIds = convertSet(list, MesQcIndicatorRespVO::getParentId, vo -> vo.getParentId() != null && !MesQcIndicatorDO.PARENT_ID_ROOT.equals(vo.getParentId())); if (CollUtil.isEmpty(parentIds)) { return; } Map<Long, String> parentNameMap = convertMap(indicatorService.getIndicatorList(parentIds), MesQcIndicatorDO::getId, MesQcIndicatorDO::getName); list.forEach(vo -> vo.setParentName(parentNameMap.get(vo.getParentId()))); } } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/indicator/vo/MesQcIndicatorPageReqVO.java
@@ -24,4 +24,7 @@ @Schema(description = "ç»æå¼ç±»å", example = "1") private Integer resultType; @Schema(description = "æ¡ç®ç±»åï¼1=å½å ¥é¡¹ï¼2=åç»é¡¹", example = "2") private Integer itemType; } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/indicator/vo/MesQcIndicatorRespVO.java
@@ -36,7 +36,25 @@ @ExcelProperty("æ£æµå·¥å ·") private String tool; @Schema(description = "ç»æå¼ç±»å", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "æ¡ç®ç±»åï¼1=å½å ¥é¡¹ï¼2=åç»é¡¹", example = "1") private Integer itemType; @Schema(description = "ç¶ææ ç¼å·ï¼0=顶级ï¼", example = "0") private Long parentId; @Schema(description = "ç¶ææ åç§°ï¼å³æå±åç»åï¼é¡¶çº§æ¶ä¸ºç©ºï¼", example = "æ°´å") @ExcelProperty("æå±åç»") private String parentName; @Schema(description = "å ¬å¼å±ç¤ºææ¬ï¼åç»é¡¹çãæ£æµè¦æ±ãå°±æ¯è¿æ¡å ¬å¼ï¼", example = "w = (m1 - m0) / m à 100%") @ExcelProperty("å ¬å¼") private String formulaText; @Schema(description = "å级æåºå·", example = "10") @ExcelProperty("æåºå·") private Integer sortOrder; @Schema(description = "ç»æå¼ç±»åãåç»é¡¹æ²¡æç»æå¼ï¼å¯ä¸ºç©º", example = "1") @ExcelProperty(value = "ç»æå¼ç±»å", converter = DictConvert.class) @DictFormat(DictTypeConstants.MES_QC_RESULT_TYPE) private Integer resultType; yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/indicator/vo/MesQcIndicatorSaveReqVO.java
@@ -27,8 +27,19 @@ @Schema(description = "æ£æµå·¥å ·", example = "å¡å°º") private String tool; @Schema(description = "ç»æå¼ç±»å", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "ç»æå¼ç±»åä¸è½ä¸ºç©º") @Schema(description = "æ¡ç®ç±»åï¼1=å½å ¥é¡¹ï¼2=åç»é¡¹", example = "1") private Integer itemType; @Schema(description = "ç¶ææ ç¼å·ï¼0=顶级ï¼", example = "0") private Long parentId; @Schema(description = "å ¬å¼å±ç¤ºææ¬ï¼åç»é¡¹çãæ£æµè¦æ±ãå°±æ¯è¿æ¡å ¬å¼ï¼", example = "w = (m1 - m0) / m à 100%") private String formulaText; @Schema(description = "å级æåºå·ï¼ç空åèªå¨æå°å级æ«å°¾", example = "10") private Integer sortOrder; @Schema(description = "ç»æå¼ç±»åãåç»é¡¹æ²¡æç»æå¼ï¼å¯ä¸å¡«", example = "1") private Integer resultType; @Schema(description = "ç»æå¼å±æ§", example = "IMG") yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/indicator/MesQcIndicatorDO.java
@@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; @@ -22,6 +24,8 @@ @AllArgsConstructor public class MesQcIndicatorDO extends BaseDO { public static final Long PARENT_ID_ROOT = 0L; /** * ç¼å· */ @@ -35,6 +39,35 @@ * æ£æµé¡¹åç§° */ private String name; /** * ç¶ææ ç¼å·ï¼0=é¡¶çº§ï¼ * * æ¥åä¾§é å®è¿åãç»ååå¹¶ãå项åè¡ãç两å±ç»æ ââ å½å±åªçè¿ä¸ªåæ®µï¼ * ä¸ç {@link #type}ãä¹ä¸çè¡åºï¼è¡è¡¨éç¶é¡¹é£ä¸è¡ä¸å®çå项并ä¸ç¸é»ï¼ã */ private Long parentId; /** * æ¡ç®ç±»åï¼1=å½å ¥é¡¹ï¼2=åç»é¡¹ * * åªç¨æ¥å¤æãè¿ä¸ªææ å¤ä¸å¤æ ¼å½ç»å¤´ãï¼å½å±æ¬èº«ä»ç {@link #parentId} */ private Integer itemType; /** * å ¬å¼å±ç¤ºææ¬ï¼æå°æ¥åç¨ï¼ä¸åä¸è®¡ç®ï¼ * * åç»é¡¹æ²¡æè§æ ¼ä¸ä¸éï¼å®çãæ£æµè¦æ±ãå°±æ¯è¿æ¡å ¬å¼ */ private String formulaText; /** * å使æ¬ï¼å¦ kg/m³ã/%ï¼æå°å±ç¤ºç¨ï¼ */ private String unitText; /** * å级æåºå· * * æ¥åçæ£éªé¡¹é¡ºåºæå®æï¼åç»é¡¹ä¸ç¬ç«é¡¹åå±ãå级ãå ä¸ä¸ªä½ç½®ï¼ç»å å项åèªåæ */ private Integer sortOrder; /** * æ£æµé¡¹ç±»å * @@ -50,14 +83,21 @@ * * åå ¸ {@link DictTypeConstants#MES_QC_RESULT_TYPE} * æä¸¾ {@link cn.iocoder.yudao.module.mes.enums.qc.MesQcResultValueTypeEnum} * * updateStrategy=ALWAYSï¼å½å ¥é¡¹æ¹æåç»é¡¹æ¶è¦è½æè¿ä¸ªå¼åå NULLï¼ * é»è®¤ç NOT_NULL çç¥ä¼æ null åæ®µä» UPDATE éåé¤ï¼å¯¼è´æ§çç»æå¼æ®ç */ @TableField(updateStrategy = FieldStrategy.ALWAYS) private Integer resultType; /** * ç»æå¼å±æ§ * * 1. FILE æ¶ï¼å IMG/FILE * 2. DICT æ¶ï¼ååå ¸ç±»åå * * updateStrategy å {@link #resultType} */ @TableField(updateStrategy = FieldStrategy.ALWAYS) private String resultSpecification; /** * 夿³¨ yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/indicator/MesQcIndicatorMapper.java
@@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Mapper; import java.util.List; import java.util.Objects; /** * MES è´¨æ£ææ Mapper @@ -23,6 +24,7 @@ .likeIfPresent(MesQcIndicatorDO::getName, reqVO.getName()) .eqIfPresent(MesQcIndicatorDO::getType, reqVO.getType()) .eqIfPresent(MesQcIndicatorDO::getResultType, reqVO.getResultType()) .eqIfPresent(MesQcIndicatorDO::getItemType, reqVO.getItemType()) .orderByDesc(MesQcIndicatorDO::getId)); } @@ -39,4 +41,34 @@ .orderByDesc(MesQcIndicatorDO::getId)); } default List<MesQcIndicatorDO> selectListByItemType(Integer itemType) { return selectList(new LambdaQueryWrapperX<MesQcIndicatorDO>() .eq(MesQcIndicatorDO::getItemType, itemType) .orderByAsc(MesQcIndicatorDO::getSortOrder) .orderByAsc(MesQcIndicatorDO::getId)); } default List<MesQcIndicatorDO> selectListByParentId(Long parentId) { return selectList(new LambdaQueryWrapperX<MesQcIndicatorDO>() .eq(MesQcIndicatorDO::getParentId, parentId) .orderByAsc(MesQcIndicatorDO::getSortOrder) .orderByAsc(MesQcIndicatorDO::getId)); } default Long selectCountByParentId(Long parentId) { return selectCount(new LambdaQueryWrapperX<MesQcIndicatorDO>() .eq(MesQcIndicatorDO::getParentId, parentId)); } /** * åå级æå¤§çæåºå·ï¼å级没æè®°å½æ¶è¿å null */ default Integer selectMaxSortOrder(Long parentId) { return selectListByParentId(parentId).stream() .map(MesQcIndicatorDO::getSortOrder) .filter(Objects::nonNull) .max(Integer::compareTo) .orElse(null); } } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -372,6 +372,12 @@ ErrorCode QC_INDICATOR_CODE_DUPLICATE = new ErrorCode(1_040_601_001, "è´¨æ£ææ ç¼ç å·²åå¨"); ErrorCode QC_INDICATOR_NAME_DUPLICATE = new ErrorCode(1_040_601_002, "è´¨æ£ææ åç§°å·²åå¨"); ErrorCode QC_INDICATOR_RESULT_SPECIFICATION_REQUIRED = new ErrorCode(1_040_601_003, "ç»æå¼å±æ§ä¸è½ä¸ºç©º"); ErrorCode QC_INDICATOR_PARENT_NOT_EXISTS = new ErrorCode(1_040_601_004, "ç¶ææ ä¸åå¨"); ErrorCode QC_INDICATOR_PARENT_NOT_GROUP = new ErrorCode(1_040_601_005, "ç¶ææ 䏿¯åç»é¡¹"); ErrorCode QC_INDICATOR_PARENT_SELF = new ErrorCode(1_040_601_006, "ç¶ææ ä¸è½æ¯èªå·±"); ErrorCode QC_INDICATOR_GROUP_CANNOT_NEST = new ErrorCode(1_040_601_007, "åç»é¡¹ä¸è½æå¨å ¶å®ææ ä¸"); ErrorCode QC_INDICATOR_HAS_CHILDREN = new ErrorCode(1_040_601_008, "è¯¥ææ ä¸å·²æå项"); ErrorCode QC_INDICATOR_RESULT_TYPE_REQUIRED = new ErrorCode(1_040_601_009, "å½å ¥é¡¹çç»æå¼ç±»åä¸è½ä¸ºç©º"); // ========== MES è´¨é管ç-缺é·ç±»åï¼1-040-602-000ï¼ ========== ErrorCode QC_DEFECT_NOT_EXISTS = new ErrorCode(1_040_602_000, "缺é·ç±»åä¸åå¨"); yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/MesQcIndicatorItemTypeEnum.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,44 @@ package cn.iocoder.yudao.module.mes.enums.qc; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; import java.util.Objects; /** * MES è´¨æ£ææ æ¡ç®ç±»åæä¸¾ * <p> * ä¸ä¸ªåç»çç»åä¸å ¬å¼æå¨ãåç»é¡¹ãä¸ï¼å®æµå¼æå¨å®ä¸é¢çãå½å ¥é¡¹ãä¸ã * * @author è¶ çº§ç®¡çå */ @Getter @AllArgsConstructor public enum MesQcIndicatorItemTypeEnum implements ArrayValuable<Integer> { ENTRY(1, "å½å ¥é¡¹"), GROUP(2, "åç»é¡¹"); public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesQcIndicatorItemTypeEnum::getType).toArray(Integer[]::new); /** * ç±»åå¼ */ private final Integer type; /** * ç±»åå */ private final String name; @Override public Integer[] array() { return ARRAYS; } public static boolean isGroup(Integer type) { return Objects.equals(GROUP.getType(), type); } } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicator/MesQcIndicatorService.java
@@ -65,6 +65,13 @@ List<MesQcIndicatorDO> getIndicatorList(); /** * è·å¾åç»é¡¹åè¡¨ï¼æ¡ç®ç±»å为åç»é¡¹ï¼ï¼ä¸»è¦ç¨äºãæå±åç»ã䏿é项 * * @return åç»é¡¹å表 */ List<MesQcIndicatorDO> getIndicatorGroupList(); /** * è·å¾è´¨æ£ææ å表 * * @param ids ç¼å·æ°ç» yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicator/MesQcIndicatorServiceImpl.java
@@ -3,6 +3,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; @@ -10,6 +11,7 @@ import cn.iocoder.yudao.module.mes.controller.admin.qc.indicator.vo.MesQcIndicatorSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO; import cn.iocoder.yudao.module.mes.dal.mysql.qc.indicator.MesQcIndicatorMapper; import cn.iocoder.yudao.module.mes.enums.qc.MesQcIndicatorItemTypeEnum; import cn.iocoder.yudao.module.mes.enums.qc.MesQcResultValueTypeEnum; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; @@ -19,8 +21,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; @@ -43,6 +47,7 @@ // æå ¥ MesQcIndicatorDO indicator = BeanUtils.toBean(createReqVO, MesQcIndicatorDO.class); normalizeHierarchy(indicator); indicatorMapper.insert(indicator); return indicator.getId(); } @@ -56,6 +61,7 @@ // æ´æ° MesQcIndicatorDO updateObj = BeanUtils.toBean(updateReqVO, MesQcIndicatorDO.class); normalizeHierarchy(updateObj); indicatorMapper.updateById(updateObj); } @@ -63,6 +69,11 @@ public void deleteIndicator(Long id) { // æ ¡éªåå¨ validateIndicatorExists(id); // 级èå é¤åé¡¹ï¼ææ åªæ¯æä¸¤å±ï¼æå¨å®ä¸é¢ç齿¯å®çæç»é¡¹ï¼ç¶å äºåæ å¤å¯æ List<MesQcIndicatorDO> children = indicatorMapper.selectListByParentId(id); if (CollUtil.isNotEmpty(children)) { indicatorMapper.deleteByIds(convertList(children, MesQcIndicatorDO::getId)); } // å é¤ indicatorMapper.deleteById(id); } @@ -72,8 +83,100 @@ validateIndicatorCodeUnique(id, saveReqVO.getCode()); // æ ¡éªåç§°å¯ä¸ validateIndicatorNameUnique(id, saveReqVO.getName()); // æ ¡éªå±çº§å½å±ï¼åªå 许两å±ï¼ validateHierarchy(id, saveReqVO); // æ ¡éªç»æå¼å±æ§ validateResultSpecification(saveReqVO.getResultType(), saveReqVO.getResultSpecification()); validateResultSpecification(saveReqVO.getItemType(), saveReqVO.getResultType(), saveReqVO.getResultSpecification()); } /** * æ ¡éªå±çº§å½å±ãåªå 许两å±ï¼åç»é¡¹å¿ 须顶级ï¼å½å ¥é¡¹å¯æå°åç»é¡¹ä¸ã * <p> * ãç¶å¿ é¡»æ¯åç»é¡¹ãè¿æ¡æ¬èº«å°±ææ»äºç¬¬ä¸å±ï¼æä»¥ä¸ååç¬å䏿¡ãç¶å¿ 须顶级ãçæ ¡éªã * æä¸å¡ä¼å çº§åªæç¬¬ä¸å¤ä¸æ»¡è¶³ï¼é¿å 䏿¬¡æå¤æ¡è®©ç¨æ·æä¸ä½éç¹ã */ private void validateHierarchy(Long id, MesQcIndicatorSaveReqVO reqVO) { Long parentId = ObjectUtils.defaultIfNull(reqVO.getParentId(), MesQcIndicatorDO.PARENT_ID_ROOT); Integer itemType = ObjectUtils.defaultIfNull(reqVO.getItemType(), MesQcIndicatorItemTypeEnum.ENTRY.getType()); // 1. åç»é¡¹åªè½é¡¶çº§ if (MesQcIndicatorItemTypeEnum.isGroup(itemType) && !MesQcIndicatorDO.PARENT_ID_ROOT.equals(parentId)) { throw new ServiceException(QC_INDICATOR_GROUP_CANNOT_NEST.getCode(), String.format("åç»é¡¹[%s]å¿ é¡»ä½ä¸ºé¡¶çº§ææ ï¼ä¸è½æå°å ¶å®ææ ä¸ï¼å½åéæ©äºç¶ææ [%s]ï¼ã" + "å¦éè°æ´å±çº§ï¼è¯·æãæ¡ç®ç±»åãæ¹ä¸ºå½å ¥é¡¹ååéæ©æå±åç»ã", defaultStr(reqVO.getName()), nameOf(parentId))); } // 2. ç¶ææ å¿ é¡»åå¨ä¸æ¯åç»é¡¹ if (!MesQcIndicatorDO.PARENT_ID_ROOT.equals(parentId)) { if (Objects.equals(id, parentId)) { throw exception(QC_INDICATOR_PARENT_SELF); } MesQcIndicatorDO parent = indicatorMapper.selectById(parentId); if (parent == null) { throw new ServiceException(QC_INDICATOR_PARENT_NOT_EXISTS.getCode(), String.format("æå±åç»ä¸åå¨ï¼ææ ç¼å· %d æ¥æ æ¤ææ ï¼å¯è½å·²è¢«å é¤ï¼ãè¯·éæ°éæ©æå±åç»ï¼ææ¸ 空该项ä½ä¸ºé¡¶çº§ææ ã", parentId)); } if (!MesQcIndicatorItemTypeEnum.isGroup(parent.getItemType())) { throw new ServiceException(QC_INDICATOR_PARENT_NOT_GROUP.getCode(), String.format("æéç¶ææ [%s]䏿¯åç»é¡¹ï¼å½åæ¡ç®ç±»å为[%s]ï¼ï¼ä¸è½ä½ä¸ºæå±åç»ã" + "请æ¹éä¸ä¸ªåç»é¡¹ï¼æå 卿æ å表æ[%s]çæ¡ç®ç±»åæ¹ä¸ºåç»é¡¹ã", defaultStr(parent.getName()), itemTypeNameOf(parent.getItemType()), defaultStr(parent.getName()))); } } // 3. å½å ¥é¡¹å¿ é¡»æç»æå¼ç±»åï¼åç»é¡¹æ²¡æç»æå¼ï¼ç°ç½åç»è¡ result_type å°±æ¯ NULLï¼ if (!MesQcIndicatorItemTypeEnum.isGroup(itemType) && reqVO.getResultType() == null) { throw exception(QC_INDICATOR_RESULT_TYPE_REQUIRED); } // 4. èªå·±å·²ç»æå项æ¶ï¼ä¸è½å被å«äººæ¶ç¼ãä¹ä¸è½åè¿æ¥åæå项 if (id == null) { return; } Long childCount = indicatorMapper.selectCountByParentId(id); if (childCount <= 0) { return; } if (!MesQcIndicatorDO.PARENT_ID_ROOT.equals(parentId)) { throw new ServiceException(QC_INDICATOR_HAS_CHILDREN.getCode(), String.format("ææ [%s]ä¸å·²ææ %d 个å½å ¥é¡¹ï¼ä¸è½åæå°å ¶å®åç»ä¸ï¼ææ åªæ¯æä¸¤å±ï¼ã" + "请å å é¤æç§»èµ°è¿ %d 个å项ï¼åä¿®æ¹å®çæå±åç»ã", defaultStr(reqVO.getName()), childCount, childCount)); } if (!MesQcIndicatorItemTypeEnum.isGroup(itemType)) { throw new ServiceException(QC_INDICATOR_HAS_CHILDREN.getCode(), String.format("ææ [%s]ä¸å·²ææ %d 个å½å ¥é¡¹ï¼ä¸è½åæå®çæ¡ç®ç±»åæ¹ä¸ºå½å ¥é¡¹ï¼å¦åè¿äºå项ä¼å¤±å»æå±åç»ã" + "请å å é¤æç§»èµ°è¿ %d 个å项ï¼åä¿®æ¹æ¡ç®ç±»åã", defaultStr(reqVO.getName()), childCount, childCount)); } } /** * è½åºåè¡¥é½å±çº§å段ï¼åç»é¡¹æ¸ ç©ºç»æå¼ï¼ç°ç½åç»è¡æ²¡æç»æå¼ï¼ï¼æåºå·ç空åæå°å级æ«å°¾ */ private void normalizeHierarchy(MesQcIndicatorDO indicator) { indicator.setParentId(ObjectUtils.defaultIfNull(indicator.getParentId(), MesQcIndicatorDO.PARENT_ID_ROOT)); indicator.setItemType(ObjectUtils.defaultIfNull(indicator.getItemType(), MesQcIndicatorItemTypeEnum.ENTRY.getType())); if (MesQcIndicatorItemTypeEnum.isGroup(indicator.getItemType())) { indicator.setResultType(null); indicator.setResultSpecification(null); } if (indicator.getSortOrder() == null) { Integer max = indicatorMapper.selectMaxSortOrder(indicator.getParentId()); indicator.setSortOrder((max == null ? 0 : max) + (MesQcIndicatorDO.PARENT_ID_ROOT.equals(indicator.getParentId()) ? 10 : 1)); } } private String nameOf(Long id) { MesQcIndicatorDO indicator = id == null ? null : indicatorMapper.selectById(id); return indicator == null ? String.valueOf(id) : defaultStr(indicator.getName()); } private static String itemTypeNameOf(Integer itemType) { return MesQcIndicatorItemTypeEnum.isGroup(itemType) ? MesQcIndicatorItemTypeEnum.GROUP.getName() : MesQcIndicatorItemTypeEnum.ENTRY.getName(); } private static String defaultStr(String value) { return StrUtil.blankToDefault(value, ""); } private void validateIndicatorExists(Long id) { @@ -102,7 +205,11 @@ } } private void validateResultSpecification(Integer resultType, String resultSpecification) { private void validateResultSpecification(Integer itemType, Integer resultType, String resultSpecification) { // åç»é¡¹æ²¡æç»æå¼ï¼è½åºåä¼è¢«æ¸ 空ï¼è¿é䏿 ¡éª if (MesQcIndicatorItemTypeEnum.isGroup(ObjectUtils.defaultIfNull(itemType, MesQcIndicatorItemTypeEnum.ENTRY.getType()))) { return; } if (ObjectUtils.equalsAny(resultType, MesQcResultValueTypeEnum.FILE.getType(), MesQcResultValueTypeEnum.DICT.getType())) { if (StrUtil.isBlank(resultSpecification)) { @@ -127,6 +234,11 @@ } @Override public List<MesQcIndicatorDO> getIndicatorGroupList() { return indicatorMapper.selectListByItemType(MesQcIndicatorItemTypeEnum.GROUP.getType()); } @Override public List<MesQcIndicatorDO> getIndicatorList(Collection<Long> ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/indicator/MesQcIndicatorServiceImplTest.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,323 @@ package cn.iocoder.yudao.module.mes.service.qc.indicator; import cn.iocoder.yudao.framework.common.exception.ErrorCode; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.mes.controller.admin.qc.indicator.vo.MesQcIndicatorSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO; import cn.iocoder.yudao.module.mes.dal.mysql.qc.indicator.MesQcIndicatorMapper; import cn.iocoder.yudao.module.mes.enums.qc.MesQcIndicatorItemTypeEnum; import cn.iocoder.yudao.module.mes.enums.qc.MesQcResultValueTypeEnum; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import org.springframework.context.annotation.Import; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; /** * {@link MesQcIndicatorServiceImpl} çåå æµè¯ * <p> * è¦çåç»å±çº§ï¼å±çº§æ ¡éªãæåºå·è¡¥é½ãç»æå¼åæ®µæ¸ ç©ºã级èå é¤ã * * @author è¶ çº§ç®¡çå */ @Import(MesQcIndicatorServiceImpl.class) public class MesQcIndicatorServiceImplTest extends BaseDbUnitTest { /** * æ£æµé¡¹ç±»åï¼åå ¸ mes_indicator_type çãæ§è½ãï¼ç°ç½åç»è¡ç¨çå°±æ¯è¿ä¸ªå¼ï¼ */ private static final Integer TYPE_PERFORMANCE = 4; @Resource private MesQcIndicatorServiceImpl indicatorService; @Resource private MesQcIndicatorMapper indicatorMapper; // ==================== å建ï¼åç»é¡¹ ==================== @Test public void testCreateGroup_clearsResultFields() { // åå¤åæ°ï¼åç»é¡¹å³ä½¿å¸¦äº FILE ç±»åä¸ç©ºçç»æå¼å±æ§ï¼ä¹ä¸åºè¢«ãç»æå¼å±æ§ä¸è½ä¸ºç©ºãæ¦ä½ MesQcIndicatorSaveReqVO reqVO = buildReqVO("T01-G", "æ°´å", MesQcIndicatorItemTypeEnum.GROUP.getType(), null, MesQcResultValueTypeEnum.FILE.getType()); reqVO.setFormulaText("w = (m1 - m0) / m à 100%"); // è°ç¨ Long id = indicatorService.createIndicator(reqVO); // æè¨ï¼åç»é¡¹è½åºæ¶ç»æå¼åæ®µè¢«æ¸ ç©ºï¼ç¶çº§å½ 0ï¼æåºå·èªå¨è¡¥ 10ï¼é¦ä¸ªé¡¶çº§ï¼ MesQcIndicatorDO dbIndicator = indicatorMapper.selectById(id); assertNotNull(dbIndicator); assertEquals(MesQcIndicatorItemTypeEnum.GROUP.getType(), dbIndicator.getItemType()); assertEquals(MesQcIndicatorDO.PARENT_ID_ROOT, dbIndicator.getParentId()); assertNull(dbIndicator.getResultType()); assertNull(dbIndicator.getResultSpecification()); assertEquals("w = (m1 - m0) / m à 100%", dbIndicator.getFormulaText()); assertEquals(10, dbIndicator.getSortOrder()); } @Test public void testCreateGroup_cannotNest() { // mock æ°æ®ï¼ä¸ä¸ªå·²åå¨çåç» MesQcIndicatorDO parent = insertIndicator("T02-G1", "æ°´å", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 10); // åå¤åæ°ï¼åç»é¡¹æå°å¦ä¸ä¸ªåç»ä¸ MesQcIndicatorSaveReqVO reqVO = buildReqVO("T02-G2", "ç°å", MesQcIndicatorItemTypeEnum.GROUP.getType(), parent.getId(), null); // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertMessageException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_GROUP_CANNOT_NEST, "ç°å", "å¿ é¡»ä½ä¸ºé¡¶çº§ææ ", "æ°´å"); // æè¨ï¼æªå ¥åº assertEquals(1, indicatorMapper.selectCount()); } // ==================== å建ï¼å½å ¥é¡¹ ==================== @Test public void testCreateEntry_defaultSortOrder() { // mock æ°æ®ï¼ä¸ä¸ªé¡¶çº§åç»ï¼æåºå· 30 MesQcIndicatorDO group = insertIndicator("T03-G", "æ°´å", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 30); // è°ç¨ï¼ç»å ç¬¬ä¸æ¡åé¡¹ï¼æåºå·ç空 Long firstId = indicatorService.createIndicator(buildReqVO("T03-E1", "游离水", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType())); // è°ç¨ï¼ç»å ç¬¬äºæ¡åé¡¹ï¼æåºå·ç空 Long secondId = indicatorService.createIndicator(buildReqVO("T03-E2", "ç»åæ°´", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType())); // æè¨ï¼å项æ¥é¿ä¸º 1ï¼ä¾æ¬¡æå°å级æ«å°¾ assertEquals(1, indicatorMapper.selectById(firstId).getSortOrder()); assertEquals(2, indicatorMapper.selectById(secondId).getSortOrder()); assertEquals(group.getId(), indicatorMapper.selectById(firstId).getParentId()); } @Test public void testCreateIndicator_defaultSortOrder_topLevelBlockStep() { // mock æ°æ®ï¼é¡¶çº§åç»æåºå· 30 insertIndicator("T04-G", "æ°´å", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 30); // è°ç¨ï¼é¡¶çº§æ°ææ æåºå·ç空ï¼é¡¶çº§æ¥é¿ä¸º 10 Long id = indicatorService.createIndicator(buildReqVO("T04-E", "å¤è§", MesQcIndicatorItemTypeEnum.ENTRY.getType(), null, MesQcResultValueTypeEnum.TEXT.getType())); // æè¨ MesQcIndicatorDO dbIndicator = indicatorMapper.selectById(id); assertEquals(MesQcIndicatorDO.PARENT_ID_ROOT, dbIndicator.getParentId()); assertEquals(40, dbIndicator.getSortOrder()); } @Test public void testCreateEntry_parentNotGroup() { // mock æ°æ®ï¼ä¸ä¸ªå½å ¥é¡¹è¢«æ¿æ¥å½ç¶çº§ MesQcIndicatorDO entry = insertIndicator("T05-E1", "å¤è§", MesQcIndicatorItemTypeEnum.ENTRY.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.TEXT.getType(), 10); // åå¤åæ° MesQcIndicatorSaveReqVO reqVO = buildReqVO("T05-E2", "è²æ³½", MesQcIndicatorItemTypeEnum.ENTRY.getType(), entry.getId(), MesQcResultValueTypeEnum.TEXT.getType()); // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertMessageException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_PARENT_NOT_GROUP, "å¤è§", "䏿¯åç»é¡¹", "å½å ¥é¡¹"); } @Test public void testCreateEntry_parentNotExists() { // åå¤åæ°ï¼ç¶çº§ç¼å·ä¸åå¨ Long notExistsParentId = randomLongId(); MesQcIndicatorSaveReqVO reqVO = buildReqVO("T06-E", "è²æ³½", MesQcIndicatorItemTypeEnum.ENTRY.getType(), notExistsParentId, MesQcResultValueTypeEnum.TEXT.getType()); // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertMessageException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_PARENT_NOT_EXISTS, "æå±åç»ä¸åå¨", String.valueOf(notExistsParentId)); } @Test public void testCreateEntry_resultTypeRequired() { // åå¤åæ°ï¼å½å ¥é¡¹æ²¡æç»æå¼ç±»å MesQcIndicatorSaveReqVO reqVO = buildReqVO("T07-E", "è²æ³½", MesQcIndicatorItemTypeEnum.ENTRY.getType(), null, null); // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertServiceException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_RESULT_TYPE_REQUIRED); // æè¨ï¼æªå ¥åº assertEquals(0, indicatorMapper.selectCount()); } @Test public void testCreateEntry_fileResultSpecificationRequired() { // åå¤åæ°ï¼å½å ¥é¡¹ä¸º FILE ç±»å使²¡å¡«ç»æå¼å±æ§ MesQcIndicatorSaveReqVO reqVO = buildReqVO("T08-E", "å¤è§ç §ç", MesQcIndicatorItemTypeEnum.ENTRY.getType(), null, MesQcResultValueTypeEnum.FILE.getType()); // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertServiceException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_RESULT_SPECIFICATION_REQUIRED); } // ==================== ä¿®æ¹ ==================== @Test public void testUpdateEntry_parentSelf() { // mock æ°æ®ï¼ä¸ä¸ªå½å ¥é¡¹ MesQcIndicatorDO entry = insertIndicator("T09-E", "å¤è§", MesQcIndicatorItemTypeEnum.ENTRY.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.TEXT.getType(), 10); // åå¤åæ°ï¼æèªå·±å½ç¶çº§ MesQcIndicatorSaveReqVO reqVO = buildReqVO("T09-E", "å¤è§", MesQcIndicatorItemTypeEnum.ENTRY.getType(), entry.getId(), MesQcResultValueTypeEnum.TEXT.getType()); reqVO.setId(entry.getId()); // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertServiceException(() -> indicatorService.updateIndicator(reqVO), QC_INDICATOR_PARENT_SELF); } @Test public void testUpdateIndicator_toGroup_clearsResultFields() { // mock æ°æ®ï¼ä¸ä¸ª FILE ç±»åçå½å ¥é¡¹ MesQcIndicatorDO entry = insertIndicator("T10-E", "é ¸å«é", MesQcIndicatorItemTypeEnum.ENTRY.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.FILE.getType(), 10); entry.setResultSpecification("IMG"); indicatorMapper.updateById(entry); // åå¤åæ°ï¼æå®æ¹æåç»é¡¹ MesQcIndicatorSaveReqVO reqVO = buildReqVO("T10-E", "é ¸å«é", MesQcIndicatorItemTypeEnum.GROUP.getType(), null, MesQcResultValueTypeEnum.FILE.getType()); reqVO.setId(entry.getId()); reqVO.setFormulaText("c = V à N / m"); // è°ç¨ indicatorService.updateIndicator(reqVO); // æè¨ï¼æ¡ç®ç±»åå·²æ¹ï¼ç»æå¼åæ®µè¢«æ¸ ç©ºï¼å ¬å¼åå ¥ MesQcIndicatorDO dbIndicator = indicatorMapper.selectById(entry.getId()); assertEquals(MesQcIndicatorItemTypeEnum.GROUP.getType(), dbIndicator.getItemType()); assertNull(dbIndicator.getResultType()); assertNull(dbIndicator.getResultSpecification()); assertEquals("c = V à N / m", dbIndicator.getFormulaText()); } @Test public void testUpdateIndicator_hasChildrenCannotChangeToEntry() { // mock æ°æ®ï¼ä¸ä¸ªå¸¦å项çåç» MesQcIndicatorDO group = insertIndicator("T11-G", "æ°´å", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 10); insertIndicator("T11-E1", "游离水", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType(), 1); // åå¤åæ°ï¼æåç»æ¹æå½å ¥é¡¹ MesQcIndicatorSaveReqVO reqVO = buildReqVO("T11-G", "æ°´å", MesQcIndicatorItemTypeEnum.ENTRY.getType(), null, MesQcResultValueTypeEnum.FLOAT.getType()); reqVO.setId(group.getId()); // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertMessageException(() -> indicatorService.updateIndicator(reqVO), QC_INDICATOR_HAS_CHILDREN, "ä¸è½åæå®çæ¡ç®ç±»åæ¹ä¸ºå½å ¥é¡¹", "æ°´å"); // æè¨ï¼æ¡ç®ç±»åæªè¢«æ¹å¨ assertEquals(MesQcIndicatorItemTypeEnum.GROUP.getType(), indicatorMapper.selectById(group.getId()).getItemType()); } @Test public void testUpdateIndicator_hasChildrenCannotReparent() { // mock æ°æ®ï¼ä¸ä¸ªå¸¦åé¡¹çæ¡ç®ï¼åå²èæ°æ®ï¼å½å ¥é¡¹ä¹å¯è½è¢«æè¿åé¡¹ï¼æ¥åä¾§åªè®¤ parent_idï¼ MesQcIndicatorDO entry = insertIndicator("T12-E", "æè´¨", MesQcIndicatorItemTypeEnum.ENTRY.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.FLOAT.getType(), 10); insertIndicator("T12-E1", "䏿º¶ç©", MesQcIndicatorItemTypeEnum.ENTRY.getType(), entry.getId(), MesQcResultValueTypeEnum.FLOAT.getType(), 1); MesQcIndicatorDO group = insertIndicator("T12-G", "æ°´å", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 20); // åå¤åæ°ï¼æå¸¦åé¡¹çææ æå°åç»ä¸ MesQcIndicatorSaveReqVO reqVO = buildReqVO("T12-E", "æè´¨", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType()); reqVO.setId(entry.getId()); // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertMessageException(() -> indicatorService.updateIndicator(reqVO), QC_INDICATOR_HAS_CHILDREN, "ä¸è½åæå°å ¶å®åç»ä¸", "æè´¨"); // æè¨ï¼ç¶çº§æªè¢«æ¹å¨ assertEquals(MesQcIndicatorDO.PARENT_ID_ROOT, indicatorMapper.selectById(entry.getId()).getParentId()); } // ==================== å é¤ ==================== @Test public void testDeleteGroup_cascadeChildren() { // mock æ°æ®ï¼ä¸ä¸ªåç» + 两个å项 + ä¸ä¸ªä¸ç¸å ³çé¡¶çº§ææ MesQcIndicatorDO group = insertIndicator("T13-G", "æ°´å", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 10); Long child1Id = insertIndicator("T13-E1", "游离水", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType(), 1).getId(); Long child2Id = insertIndicator("T13-E2", "ç»åæ°´", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType(), 2).getId(); Long otherId = insertIndicator("T13-E3", "å¤è§", MesQcIndicatorItemTypeEnum.ENTRY.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.TEXT.getType(), 20).getId(); // è°ç¨ indicatorService.deleteIndicator(group.getId()); // æè¨ï¼åç»ä¸å ¶å项ä¸å¹¶å é¤ï¼ä¸ç¸å ³çææ ä¿ç assertNull(indicatorMapper.selectById(group.getId())); assertNull(indicatorMapper.selectById(child1Id)); assertNull(indicatorMapper.selectById(child2Id)); assertNotNull(indicatorMapper.selectById(otherId)); } @Test public void testDeleteIndicator_notExists() { // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertServiceException(() -> indicatorService.deleteIndicator(randomLongId()), QC_INDICATOR_NOT_EXISTS); } // ==================== è¾ å©æ¹æ³ ==================== private MesQcIndicatorSaveReqVO buildReqVO(String code, String name, Integer itemType, Long parentId, Integer resultType) { MesQcIndicatorSaveReqVO reqVO = new MesQcIndicatorSaveReqVO(); reqVO.setCode(code); reqVO.setName(name); reqVO.setType(TYPE_PERFORMANCE); reqVO.setItemType(itemType); reqVO.setParentId(parentId); reqVO.setResultType(resultType); return reqVO; } private MesQcIndicatorDO insertIndicator(String code, String name, Integer itemType, Long parentId, Integer resultType, Integer sortOrder) { MesQcIndicatorDO indicator = new MesQcIndicatorDO(); indicator.setCode(code); indicator.setName(name); indicator.setType(TYPE_PERFORMANCE); indicator.setItemType(itemType); indicator.setParentId(parentId); indicator.setResultType(resultType); indicator.setSortOrder(sortOrder); indicatorMapper.insert(indicator); return indicator; } /** * æè¨æåºçä¸å¡å¼å¸¸éè¯¯ç æ£ç¡®ï¼ä¸æç¤ºææ¡å å«ç»å®çå ³é®ç段ã * <p> * å±çº§æ ¡éªçææ¡å¸¦åç»å / ç¶ææ å / åé¡¹æ¡æ°ï¼æ æ³ç¨ * {@link cn.iocoder.yudao.framework.test.core.util.AssertUtils#assertServiceException} é忝坹ã */ private void assertMessageException(Executable executable, ErrorCode errorCode, String... messageFragments) { ServiceException exception = assertThrows(ServiceException.class, executable); assertEquals(errorCode.getCode(), exception.getCode(), "é误ç ä¸å¹é "); for (String fragment : messageFragments) { assertTrue(exception.getMessage().contains(fragment), String.format("é误æç¤º[%s]æªå å«[%s]", exception.getMessage(), fragment)); } } } yudao-module-mes/src/test/resources/sql/clean.sql
@@ -21,3 +21,4 @@ DELETE FROM "mes_pro_task"; DELETE FROM "mes_pro_route_process"; DELETE FROM "mes_qc_indicator_result"; DELETE FROM "mes_qc_indicator"; yudao-module-mes/src/test/resources/sql/create_tables.sql
@@ -830,3 +830,29 @@ "tenant_id" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id") ); -- ---------------------------- -- MES è´¨æ£ææ -- ---------------------------- CREATE TABLE IF NOT EXISTS "mes_qc_indicator" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "code" varchar(100) DEFAULT NULL, "name" varchar(100) DEFAULT NULL, "parent_id" bigint NOT NULL DEFAULT 0, "item_type" tinyint NOT NULL DEFAULT 1, "formula_text" varchar(500) DEFAULT NULL, "unit_text" varchar(50) DEFAULT NULL, "sort_order" int NOT NULL DEFAULT 0, "type" int DEFAULT NULL, "tool" varchar(255) DEFAULT NULL, "result_type" int DEFAULT NULL, "result_specification" varchar(255) DEFAULT NULL, "remark" varchar(255) DEFAULT NULL, "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id") ); yudao-module-qcreport/pom.xml
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,98 @@ <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>cn.iocoder.boot</groupId> <artifactId>yudao</artifactId> <version>${revision}</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>yudao-module-qcreport</artifactId> <packaging>jar</packaging> <name>${project.artifactId}</name> <description> qcreport 模åä¸ï¼æºè½è´¨æ£æ¥å设计平å°ã å¯è§åæ¥å模æ¿è®¾è®¡å¨ + æ¨¡æ¿ Schema + æ°æ®ç»å®å¼æ + å¤å®è§å弿 + HTML/PDF 渲æå¼æã </description> <dependencies> <!-- Systemï¼æéãç¨æ·ãæä»¶åå¨ï¼storage-blob / storage-attachmentï¼ --> <dependency> <groupId>cn.iocoder.boot</groupId> <artifactId>yudao-module-system</artifactId> <version>${revision}</version> </dependency> <!-- MESï¼è´¨æ£åï¼æ¥æ/è¿ç¨/åºè´§/éè´§ï¼å¯æ¥åæ°æ®æ¥å£ --> <dependency> <groupId>cn.iocoder.boot</groupId> <artifactId>yudao-module-mes-api</artifactId> <version>${revision}</version> </dependency> <!-- Web ç¸å ³ --> <dependency> <groupId>cn.iocoder.boot</groupId> <artifactId>yudao-spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>cn.iocoder.boot</groupId> <artifactId>yudao-spring-boot-starter-security</artifactId> </dependency> <!-- DB ç¸å ³ --> <dependency> <groupId>cn.iocoder.boot</groupId> <artifactId>yudao-spring-boot-starter-mybatis</artifactId> </dependency> <dependency> <groupId>cn.iocoder.boot</groupId> <artifactId>yudao-spring-boot-starter-redis</artifactId> </dependency> <!-- å·¥å ·ç±»ç¸å ³ --> <dependency> <groupId>cn.iocoder.boot</groupId> <artifactId>yudao-spring-boot-starter-excel</artifactId> </dependency> <!-- PDF åºä»¶ï¼æå¡ç«¯è°ç¨ Chromium ææ¥å HTML æå°æ PDF --> <dependency> <groupId>com.microsoft.playwright</groupId> <artifactId>playwright</artifactId> </dependency> <!-- AI å¯¼å ¥ï¼åªä¾èµ -apiï¼éé¢åªæ AiChatApiï¼ï¼ä¸ä¾èµ yudao-module-ai æ¬ä½ --> <dependency> <groupId>cn.iocoder.boot</groupId> <artifactId>yudao-module-ai-api</artifactId> <version>${revision}</version> </dependency> <!-- AI å¯¼å ¥ï¼PDF ææ¬æ½å䏿«æé¡µæ¸²æ --> <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>3.0.4</version> </dependency> <!-- AI å¯¼å ¥ï¼Word(.docx) / Excel(.xlsx) ææ¬æ½å --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.4.0</version> </dependency> <!-- Test æµè¯ç¸å ³ --> <dependency> <groupId>cn.iocoder.boot</groupId> <artifactId>yudao-spring-boot-starter-test</artifactId> </dependency> </dependencies> </project> yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/config/QcReportAiImportProperties.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,66 @@ package cn.iocoder.yudao.module.qcreport.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * AI å¯¼å ¥æ¨¡æ¿è稿é ç½® * <p> * å¯¹åº application.yml ä¸ç yudao.qcreport.ai-import åç¼é ç½®ã * <p> * è¿éçæ¯ä¸é¡¹é½æ¯**硬ä¸é**ï¼ä¸æ¯æ§è½è°ä¼åæ°ï¼æ¯æ¬¡å¯¼å ¥ä¼ççååº 1ï½5 次大模åè°ç¨ï¼ * æ«æä»¶æå¾è®¡è´¹ï¼æ ä¸éæå³ç䏿¬¡è¯¯æä½å°±è½ç§æä¸æ´å¤©çé¢åº¦ãæä»¥å®å¯æ¥éè®©ç¨æ·åæ¹ï¼ * ä¹ä¸è¦é黿ªæââé黿ªæè¿ä¼è®©ç¨æ·ä»¥ä¸ºæ´ä»½æä»¶é½è¯å«è¿äºã */ @Component @ConfigurationProperties(prefix = "yudao.qcreport.ai-import", ignoreUnknownFields = true) @Data public class QcReportAiImportProperties { /** æ¯å¦å¯ç¨ AI å¯¼å ¥ãéè¦è½åºç½è®¿é®å¤§æ¨¡åæå¡ï¼å ç½é¨ç½²æ¶å ³æå®ï¼æ¥å£ä¼æç¡®æ¥ãæªå¯ç¨ãè䏿¯æä½ */ private Boolean enabled = true; /** 䏿¬¡è¯·æ±æå¤å 个æä»¶ */ private Integer maxFiles = 3; /** å个æä»¶æå¤å 页ãè¶ åºç´æ¥æ¥éï¼ä¸é黿ªæ */ private Integer maxPagesPerFile = 5; /** 䏿¬¡è¯·æ±ç累计页æ°ä¸éã夿件å å åçæ»é¸ï¼é²æ¢ãæ¯ä¸ªæä»¶é½æ²¡è¶ ãå èµ·æ¥å´å¾å¤ã */ private Integer maxPagesPerRequest = 8; /** * 䏿¬¡å¯¼å ¥çæ»èæ¶ä¸éï¼ç§ï¼ï¼å¨ä¸¤æ¬¡æ¨¡åè°ç¨ä¹é´æ£æ¥ã * <p> * 页æ°ä¸æä»¶æ°ä¸éåªçº¦æäºè°ç¨**次æ°**ï¼æ²¡çº¦æ**æ¶é¿**ï¼æ¨¡åæéæ¶æ¯æ¬¡è°ç¨é½è½è满 * {@code yudao.ai.timeout}ï¼8 次å å è½æä¸æ¬¡è¯¯æä½ææå¥½å åéçä»è´¹è°ç¨ã * <p> * 忬¡è°ç¨æ æ³ä¸éææï¼{@code AiChatApi} 䏿¥åè¶ æ¶åæ°ï¼ï¼æä»¥åªè½å¨ä¸¤æ¬¡è°ç¨ä¹é´å ³é¨ï¼ * æåä¼å¤åºä¸ä¸ªå次è°ç¨è¶ æ¶ãå端 axios è¶ æ¶å¿ é¡»æ¯ãæ¬é¡¹ + yudao.ai.timeoutãæ´å®½ï¼ * å¦åç¨æ·å çå°æµè§å¨æå¼ï¼è䏿¯è¿éçæç¡®æç¤ºãå 180 ç§ï¼å¸¸è§ 8 次以å çè°ç¨é½è·å¾å®ï¼ * åªææ¨¡åææ¾åæ ¢æ¶æä¼è§¦åã */ private Integer maxDurationSeconds = 180; /** å个æä»¶å¤§å°ä¸éï¼MBï¼ */ private Integer maxFileSizeMb = 20; /** è¯å«åºçç»ä»¶æ»æ°ä¸éï¼è¶ åºæªæå¹¶å¨ warnings éæç¤ºè¡¥å½ */ private Integer maxComponents = 200; /** * PDF å¤å®èµ°ææ¬ééè¿æ¯å¾çééçéå¼ï¼å¹³åæ¯é¡µå符æ°ã * <p> * 便®æ¯ãè¿ä»½ PDF ææ²¡æææ¬å±ãè䏿¯ã宿¯ä¸æ¯ PDFããéå¼å 30 æ¯ç»éªå¼ï¼ * çæ£æææ¬å±çæ¥åæ¯é¡µè³å°å ååï¼èæ«æä»¶å³ä¾¿å¸¦ä¸ OCR å±ä¹å¾å¾åªæé¶æå 个å符ã */ private Integer pdfTextPageThreshold = 30; /** æ«æé¡µè½¬å¾æ¶ç DPIãä½äº 200 æ¶ä¸æåå½¢ä¼ç³ï¼å¤æ¨¡ææ¨¡å容æè®¤éï¼è°é«åæ¯æ¬¡è°ç¨æ´è´µ */ private Integer renderDpi = 200; /** Excel å表æå¤è¯»åçè¡æ°ï¼é²æ¢è¶ 大表ææç¤ºè¯æç */ private Integer maxXlsxRows = 300; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/config/QcReportPdfProperties.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,44 @@ package cn.iocoder.yudao.module.qcreport.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; /** * PDF åºä»¶é ç½® * <p> * å¯¹åº application.yml ä¸ç yudao.qcreport.pdf åç¼é ç½®ã */ @Component @ConfigurationProperties(prefix = "yudao.qcreport.pdf", ignoreUnknownFields = true) @Data public class QcReportPdfProperties { /** æ¯å¦å¯ç¨ PDF åºä»¶ãé¨ç½²ç¯å¢æ²¡ææµè§å¨æ¶å ³æå®ï¼æ¥å£ä¼æç¡®æ¥ãæªå¯ç¨ãè䏿¯æ 500 */ private Boolean enabled = true; /** * æµè§å¨ééï¼å¦ chrome / msedgeï¼ç空åç¨ Playwright èªå¸¦ç Chromium * ï¼åè éè¦å æ§è¡ {@code playwright install chromium} ä¸è½½ï¼çº¦ 150MBï¼ã */ private String channel = "chrome"; /** æµè§å¨å¯æ§è¡æä»¶è·¯å¾ãé äºå°±ä¼å äº {@link #channel}ï¼ç¨äºè£ å¨éæ åä½ç½®çæµè§å¨ */ private String executablePath = ""; /** åæ¶è¿è¡ç PDF 渲æä¸éãChromium å¾åå åï¼å¹¶åå¿ é¡»è®¾é¸ */ private Integer concurrency = 2; /** 忬¡æ¸²æè¶ æ¶ï¼æ¯«ç§ï¼ï¼åæ¶ç¨äºçå¾ é¡µé¢èµæºä¸çå¾ å¹¶ååé¢ */ private Long timeoutMs = 30000L; /** é¡µèæ¯å¦æå°ã第 N 页 / å ± M 页ã */ private Boolean showPageNumber = true; /** é¢å¤çæµè§å¨å¯å¨åæ°ï¼å¦ Linux 以 root è¿è¡éå --no-sandbox */ private List<String> browserArgs = new ArrayList<>(); } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/aiimport/QcReportAiImportController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,44 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.aiimport; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo.QcReportAiDraftReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo.QcReportAiDraftRespVO; import cn.iocoder.yudao.module.qcreport.service.aiimport.QcReportAiImportService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** * æºè½è´¨æ£æ¥å AI å¯¼å ¥ Controllerã * <p> * åªæä¸ä¸ªç«¯ç¹ï¼ä¸<b>䏿¥æ¶æä»¶</b>ï¼æä»¶å ç±åç«¯ä¼ å° system æ¨¡åæ¿å° blobIdï¼åè¿åç»ä»¶æ¸ åä¸èµ·æäº¤ã * ä¸å¡æ¨¡åèªå»ºä¸ä¼ ééä¼ç»å¼ç»ä¸çåå¨é ç½®ä¸éä»¶å½å±ï¼è§ file-upload.mdï¼ï¼ * æä»¥è¿éè¿ {@code MultipartFile} é½ä¸åºç°å¨ç¾åéã */ @Tag(name = "管çåå° - æºè½è´¨æ£æ¥å AI å¯¼å ¥") @RestController @RequestMapping("/qc-report/ai-import") @Validated public class QcReportAiImportController { @Resource private QcReportAiImportService aiImportService; @PostMapping("/draft") @Operation(summary = "è¯å«æä»¶çææ¨¡æ¿è稿", description = "忥è¿åï¼ä¸è½åºãè¯å«åºçåªæ¯è稿ï¼éå¨è®¾è®¡å¨äººå·¥ç¡®è®¤åç±ãä¿åãè½ä¸ºæ£å¼çæ¬") @PreAuthorize("@ss.hasPermission('qc-report:template:ai-import')") public CommonResult<QcReportAiDraftRespVO> generateDraft(@Valid @RequestBody QcReportAiDraftReqVO reqVO) { return success(aiImportService.generateDraft(reqVO)); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/aiimport/vo/QcReportAiDraftReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,52 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.List; /** * AI è¯å«æä»¶çææ¨¡æ¿è稿çå ¥åã * <p> * æä»¶ä¸èµ°è¿éä¸ä¼ ï¼å ç±åç«¯è° system 模åç {@code /system/storage-blob/upload} æ¿å° blobIdï¼ * åæ blobId äº¤ç»æ¬æ¥å£ãä¸å¡æ¨¡åä¸èªå»ºä¸ä¼ ééï¼ä¹ä¸ç¢° MultipartFileï¼è§ file-upload.mdï¼ã * <p> * è¿åçåªæ¯**è稿**ï¼ä¸è½åºã人工å¨è®¾è®¡å¨é确认ãè°æ´å¹¶ç¹ä¿åï¼æä¼çæ£åå ¥æ¨¡æ¿çæ¬ã */ @Schema(description = "管çåå° - æºè½è´¨æ£æ¥å AI 模æ¿è稿 Request VO") @Data public class QcReportAiDraftReqVO { /** * æä»¶æ°éä¸é**ä¸å¨æ³¨è§£éåæ»**ï¼å®ç± {@code yudao.qcreport.ai-import.max-files} å³å®ï¼ * æå¡å±å¨è¯»å任使件ä¹åå æ¯å¯¹é ç½®å¼å¹¶ç»åºå¸¦ä¸¤ä¸ªå®é å¼çæç¤ºã * è¿éååä¸ä¸ªæ°åå°±æ¯ç¬¬äºä¸ªçç¸æ¥æºââå®ä¸é ç½®ä¸åæ¥æ¶ï¼ç¨æ·çå°çä¸éä¸çæ£çæçä¸é对ä¸ä¸ï¼ * è䏿³¨è§£æ°¸è¿åªä¼æ´ä¸¥ï¼é ç½®ä¾¿åªæ¢å¾ä¸è°ã */ @Schema(description = "ä¸ä¼ æä»¶å¯¹åºç blobId å表ï¼é¡ºåºå³ææ¡£é¡µåºã个æ°ä¸éè§ yudao.qcreport.ai-import.max-files", requiredMode = Schema.RequiredMode.REQUIRED, example = "[2048, 2049]") @NotEmpty(message = "请è³å°ä¸ä¼ ä¸ä¸ªæä»¶") private List<Long> blobIds; @Schema(description = "ç®æ 模æ¿ç¼å·ï¼ç¨äºæ¥å¿ççä¸éä»¶å½å±", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") @NotNull(message = "模æ¿ç¼å·ä¸è½ä¸ºç©º") private Long templateId; @Schema(description = "å端ç»ä»¶æ¸ åç Schema çæ¬ã两端ä¸ä¸è´æ¶ç´æ¥æ¥éï¼" + "é¿å 模ååºäºè¿æçç§¯æ¨æ¸ åç¼åºæ³¨å表éä¸åå¨çç»ä»¶", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.1") @NotBlank(message = "Schema çæ¬ä¸è½ä¸ºç©º") private String schemaVersion; @Schema(description = "å¯ç¨ç»ä»¶ç§¯æ¨æ¸ åï¼ç±åç«¯ä»æ´»æ³¨å表çæ", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty(message = "ç»ä»¶æ¸ åä¸è½ä¸ºç©º") private List<QcReportComponentSpecVO> catalog; @Schema(description = "ç¨æ·è¡¥å 说æï¼ä¼æ¼è¿æç¤ºè¯å¸®å©æ¨¡åçè§£ææ¡£", example = "è¿æ¯ IQC æ¥ææ£éªæ¥å") private String hint; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/aiimport/vo/QcReportAiDraftRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,69 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.ReportTemplateSchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; import java.util.Map; /** * AI è¯å«ç»æï¼ä¸ä»½**模æ¿è稿**ï¼ä¸è½åºã * <p> * 廿ä¸è¿å {@link ReportTemplateSchema}ï¼èç¨¿åªæ¯ãä¸ä¸²ç»ä»¶ + åèªç屿§ãè¿ä¸ªæ´å°çç©åå½¢æï¼ * ç±åç«¯è£ é å¨é 䏿´»æ³¨å表æè½ç¼è¯æç»å¸æ°æ®ï¼{@code grapes}ï¼ã让å端产åºå®æ´ Schema * å°±å¾å¨è¿éåå®ç°ä¸éç»ä»¶ç canvas ç»æï¼çäºææ¸²æå¼æçèè´£æè¿ AI 模åï¼è¿ä¼è®© * Schema å¥çº¦ä¸ºäº AI èè¨èââè¯ä¹å±æ¯å»ææ¶çªçåéï¼ä¸è¯¥ä¸ºè¿æ¡è·¯å¼å£åã * <p> * å æ¤èç¨¿çæ¶è´¹è åªæåç«¯è£ é å¨ï¼å®éé¡¹æ¥æ³¨åè¡¨ï¼æªæ³¨åç type ç´æ¥è·³è¿ã * å³ä¾¿æ¨¡åç¼åºæ¸ åå¤çç»ä»¶ï¼ä¹æ°¸è¿å䏿任æ HTMLã * * <h3>为ä»ä¹æ²¡æ rawText åæ®µ</h3> * 没解æåºæ¥æ¶æ¬æ¥å£ç´æ¥æéï¼{@code AI_IMPORT_RESPONSE_UNPARSEABLE}ï¼ï¼ååºä½æ¯éè¯¯ç»æè鿬 VOï¼ * å¡ä¸ä¸ªæä¸º null çåæ®µåªä¼è®©åæ¥äººä»¥ä¸ºãè§£æå¤±è´¥æ¶åç«¯è½æ¿å°åæããæ¨¡ååæåè¿äºæå¡ç«¯æ¥å¿ã */ @Schema(description = "管çåå° - æºè½è´¨æ£æ¥å AI 模æ¿è稿 Response VO") @Data public class QcReportAiDraftRespVO { @Schema(description = "AI è¯å«åºçç»ä»¶èç¨¿ï¼ææ¥åä»ä¸å°ä¸ç顺åºï¼å·²åè¿åå¹¶å»éä¸éæ³é¡¹è¿æ»¤", requiredMode = Schema.RequiredMode.REQUIRED) private List<DraftComponent> components; @Schema(description = "AI çæµççº¸å¼ é ç½®ï¼å¯è½ä¸ºç©ºï¼ä¸ºç©ºè¡¨ç¤ºæå½å模æ¿ççº¸å¼ èµ°ï¼") private ReportTemplateSchema.Page page; @Schema(description = "AI 对è¿ä»½ææ¡£çä¸å¥è¯è¯´æï¼å±ç¤ºå¨é¢è§å¼¹çªé¡¶é¨") private String summary; /** * è½¯å¤±è´¥æ¸ åï¼è¢«è·³è¿çé¡µãæ æ³è¡¨è¾¾çåºåã被丢å¼çæªç¥ç»ä»¶çã * <p> * ä¸ãæ´ä½å¤±è´¥æå¼å¸¸ãæ¯ä¸¤åäºï¼è¿äºæ åµä¸å½±åè稿å¯ç¨ï¼ä½å¿ é¡»è®©ç¨æ·çè§ï¼ * å¦åä¼ä»¥ä¸ºæä»¶éçå 容é½è¯å«å°äºã */ @Schema(description = "è¯å«è¿ç¨ä¸ç软æç¤ºï¼éå¨çé¢ä¸å±ç¤ºç»ç¨æ·") private List<String> warnings; @Schema(description = "è¯å«æ»èæ¶ï¼æ¯«ç§ï¼ï¼å«å¤æ¬¡æ¨¡åè°ç¨", example = "12400") private Long durationMs; /** * ä¸ä¸ªç»ä»¶è稿ã * <p> * ç»æå®å ¨æå¹³ï¼æ²¡æ childrenãæ²¡æåµå¥ãprops ç key ç±åç«¯ç§¯æ¨æ¸ åå®ä¹ï¼ * å æ¤è¿éç¨ {@code Map} è䏿¯å¼ºç±»åââåæ³ key çæå¨å¨å端注å表ï¼å端ä¸è¯¥éå¤å£°æä¸éã */ @Schema(description = "管çåå° - AI è稿éçä¸ä¸ªç»ä»¶") @Data public static class DraftComponent { @Schema(description = "ç»ä»¶ç±»åï¼å¿ é¡»æ¯ç§¯æ¨æ¸ åé声æè¿ç type", example = "QualityTable") private String type; @Schema(description = "ç»ä»¶å±æ§ï¼æ¸ åéæªå£°æç key 已被å端丢å¼") private Map<String, Object> props; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/aiimport/vo/QcReportComponentSpecVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,67 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; /** * ç»ä»¶ç§¯æ¨æ¸ åçä¸é¡¹ï¼å端注å表ç**æ´¾çè§å¾**ï¼åç»å¤§æ¨¡åå½ãå¯ç¨ç§¯æ¨ãã * <p> * ç±åç«¯ä»æ´»æ³¨å表çæï¼å¥æ icon / buildContent / validate è¿äºä¸å¯åºååçæåï¼åé请æ±ä¸ä¼ ã * <b>åç«¯å»æä¸ç¡¬ç¼ç è¿ä»½æ¸ å</b>ï¼æ³¨å表å¨å端æ¯å¯ä¸çç¸æ¥æºï¼å端æä¸ä»½å°±æ¯ç¬¬äºä¸ªçç¸æ¥æºï¼ * å端å äºç»ä»¶ãæ¹äºå¿ å¡«åæ®µæ¶åç«¯å¯æ¬ä¼éé»è¿æï¼å¤§æ¨¡åéå³ä¼ç¼åºæ³¨åè¡¨éæ ¹æ¬ä¸åå¨ç typeã * <p> * ç§¯æ¨æ¸ ååªç¨äºæ¼ promptãå端**䏿®æ¤å任使¸²ææåæ³æ§æ¨æ**ââ * çæ£æå ³çæ¯åç«¯è£ é 卿¥è¯¢æ´»æ³¨åè¡¨ï¼æªæ³¨åç type å¨é£éä¼è¢«ç´æ¥è·³è¿ï¼äº§ä¸åºä»»æ HTMLã */ @Schema(description = "管çåå° - æºè½è´¨æ£æ¥å AI ç§¯æ¨æ¸ å项") @Data public class QcReportComponentSpecVO { @Schema(description = "ç»ä»¶ç±»åï¼å¦ ReportHeader / QualityTable", requiredMode = Schema.RequiredMode.REQUIRED, example = "QualityTable") private String type; @Schema(description = "ç»ä»¶æ¾ç¤ºåï¼ä¾æ¨¡åçè§£è¯ä¹", example = "æ£éªé¡¹ç®è¡¨æ ¼") private String label; @Schema(description = "ç»ä»¶åç±»ï¼å¦ basic / data / result", example = "data") private String category; @Schema(description = "éå说æï¼è¿ä¸ªç»ä»¶ç¨æ¥æ¾ä»ä¹ãä¸è¦ç¨æ¥æ¾ä»ä¹", example = "ææ¡£æé¡¶é¨çæ¬å¤´åï¼å ¬å¸åç§°ãæ¥åæ é¢ãæ¥åç¼å·") private String hint; @Schema(description = "该ç»ä»¶å 许填åç屿§åæ®µï¼æªå¨æ¤å£°æç key ä¸å¾ä¼è¢«ä¸¢å¼") private List<Field> fields; /** * ä¸ä¸ªå¯å¡«å±æ§ãåªä¿ç模åéè¦ç¥éçé¨åï¼å段类ååæ ·ç±å端注å表ç»åºã */ @Schema(description = "管çåå° - AI ç§¯æ¨æ¸ åçåæ®µé¡¹") @Data public static class Field { @Schema(description = "屿§ keyï¼å¦ itemsPath / title", requiredMode = Schema.RequiredMode.REQUIRED, example = "itemsPath") private String key; @Schema(description = "åæ®µæ¾ç¤ºå", example = "æ£éªé¡¹æ°æ®æº") private String label; @Schema(description = "åæ®µç±»åï¼å¦ text / number / boolean / enum / dataPath", example = "dataPath") private String type; @Schema(description = "æ¯å¦å¿ å¡«ï¼å¿ å¡«æªç»æ¶å端ä¼å¨ warnings éæç¤º") private Boolean required; @Schema(description = "æ¯å¦æ¯ææ°æ®ç»å®", example = "true") private Boolean bindable; @Schema(description = "æä¸¾å¯éå¼ï¼ä» å¨ type=enum æ¶ææä¹") private List<Object> enumOptions; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/instance/QcReportInstanceController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,172 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.instance; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo.QcReportInstanceGenerateFromQcReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo.QcReportInstanceGenerateFromQcRespVO; import cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo.QcReportInstanceGenerateReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo.QcReportInstanceGenerateRespVO; import cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo.QcReportInstancePageReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo.QcReportInstancePdfRespVO; import cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo.QcReportInstancePreviewRespVO; import cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo.QcReportInstanceRegenerateRespVO; import cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo.QcReportInstanceRespVO; import cn.iocoder.yudao.module.qcreport.dal.dataobject.instance.QcReportInstanceDO; import cn.iocoder.yudao.module.qcreport.engine.render.RenderOutcome; import cn.iocoder.yudao.module.qcreport.service.instance.GenerateFromQcResult; import cn.iocoder.yudao.module.qcreport.service.instance.GenerateResult; import cn.iocoder.yudao.module.qcreport.service.instance.PdfArchiveResult; import cn.iocoder.yudao.module.qcreport.service.instance.QcReportInstanceService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** * æºè½è´¨æ£æ¥åå®ä¾ Controllerã * <p> * è¿éæ¯ãæ¨¡æ¿ â æ¥åãç对å¤å ¥å£ï¼é¢è§åªæ¸²ä¸åï¼åºä»¶æè½åºå¹¶å»ç»æ°æ®å¿«ç §ã * æ°æ®ç±è°ç¨æ¹ä¼ å ¥ï¼æ¬æ¨¡åä¸è®¤è¯ä»»ä½ä¸å¡åæ®ã */ @Tag(name = "管çåå° - æºè½è´¨æ£æ¥åå®ä¾") @RestController @RequestMapping("/qc-report/instance") @Validated public class QcReportInstanceController { @Resource private QcReportInstanceService instanceService; @PostMapping("/preview") @Operation(summary = "é¢è§æ¥åï¼åªæ¸²æï¼ä¸è½åºã䏿¶èæ¥åç¼å·ï¼") @PreAuthorize("@ss.hasPermission('qc-report:instance:query')") public CommonResult<QcReportInstancePreviewRespVO> preview(@Valid @RequestBody QcReportInstanceGenerateReqVO reqVO) { RenderOutcome outcome = instanceService.preview(reqVO); QcReportInstancePreviewRespVO respVO = new QcReportInstancePreviewRespVO(); respVO.setReportNo(outcome.context().getReport() == null ? null : outcome.context().getReport().getReportNo()); respVO.setHtml(outcome.html()); respVO.setErrors(outcome.errors()); return success(respVO); } @PostMapping("/generate") @Operation(summary = "åºä»¶ï¼æ¸²æ + è½åº + å»ç»æ°æ®å¿«ç §") @PreAuthorize("@ss.hasPermission('qc-report:instance:create')") public CommonResult<QcReportInstanceGenerateRespVO> generate( @Valid @RequestBody QcReportInstanceGenerateReqVO reqVO) { return success(toGenerateRespVO(instanceService.generate(reqVO))); } @PostMapping("/generate-from-qc") @Operation(summary = "ç±è´¨æ£åçææ¥åï¼æ°æ®ä» MES åï¼æ¨¡æ¿ç±ç¨æ·éæ©ï¼") @PreAuthorize("@ss.hasPermission('qc-report:instance:generate')") public CommonResult<QcReportInstanceGenerateFromQcRespVO> generateFromQc( @Valid @RequestBody QcReportInstanceGenerateFromQcReqVO reqVO) { GenerateFromQcResult result = instanceService.generateFromQc(reqVO); QcReportInstanceGenerateFromQcRespVO respVO = new QcReportInstanceGenerateFromQcRespVO(); respVO.setId(result.instance().getId()); respVO.setReportNo(result.instance().getReportNo()); respVO.setTemplateVersion(result.instance().getTemplateVersion()); respVO.setItemCount(result.itemCount()); respVO.setUndecidableCount(result.undecidableCount()); respVO.setWarnings(result.warnings()); respVO.setErrors(result.errors()); return success(respVO); } @GetMapping("/get") @Operation(summary = "è·å¾æ¥åå®ä¾ï¼å«æ¸²æäº§ç© HTMLï¼ä¸å«æ°æ®å¿«ç §ï¼") @Parameter(name = "id", description = "å®ä¾ç¼å·", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('qc-report:instance:query')") public CommonResult<QcReportInstanceRespVO> getInstance(@RequestParam("id") Long id) { QcReportInstanceDO instance = instanceService.getInstance(id); return success(BeanUtils.toBean(instance, QcReportInstanceRespVO.class)); } @GetMapping("/snapshot") @Operation(summary = "è·å¾æ¥åå®ä¾å»ç»çæ°æ®å¿«ç §") @Parameter(name = "id", description = "å®ä¾ç¼å·", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('qc-report:instance:query')") public CommonResult<Map<String, Object>> getSnapshot(@RequestParam("id") Long id) { return success(instanceService.getSnapshot(id)); } @GetMapping("/page") @Operation(summary = "è·å¾æ¥åå®ä¾å页") @PreAuthorize("@ss.hasPermission('qc-report:instance:query')") public CommonResult<PageResult<QcReportInstanceRespVO>> getInstancePage( @Valid QcReportInstancePageReqVO pageReqVO) { PageResult<QcReportInstanceDO> pageResult = instanceService.getInstancePage(pageReqVO); return success(BeanUtils.toBean(pageResult, QcReportInstanceRespVO.class)); } @PostMapping("/regenerate") @Operation(summary = "éæ°çææ¥åï¼ç¨å»ç»çæ°æ®å¿«ç § + å䏿¨¡æ¿çæ¬é渲ï¼è¦çäº§ç© HTMLï¼") @Parameter(name = "id", description = "å®ä¾ç¼å·", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('qc-report:instance:create')") public CommonResult<QcReportInstanceRegenerateRespVO> regenerate(@RequestParam("id") Long id) { GenerateResult result = instanceService.regenerate(id); QcReportInstanceRegenerateRespVO respVO = new QcReportInstanceRegenerateRespVO(); respVO.setId(result.instance().getId()); respVO.setReportNo(result.instance().getReportNo()); respVO.setErrors(result.errors()); return success(respVO); } @PostMapping("/pdf") @Operation(summary = "å¯¼åº PDFï¼æå°å®ä¾å·²åçäº§ç© HTML 并彿¡£å°éä»¶åºï¼å¯éå¤è°ç¨ï¼") @Parameter(name = "id", description = "å®ä¾ç¼å·", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('qc-report:instance:export')") public CommonResult<QcReportInstancePdfRespVO> exportPdf(@RequestParam("id") Long id) { PdfArchiveResult result = instanceService.exportPdf(id); QcReportInstancePdfRespVO respVO = new QcReportInstancePdfRespVO(); respVO.setId(result.instanceId()); respVO.setReportNo(result.reportNo()); respVO.setFileName(result.fileName()); respVO.setByteSize(result.byteSize()); respVO.setBlobId(result.blobId()); respVO.setAttachmentId(result.attachmentId()); respVO.setPreviewURL(result.previewURL()); respVO.setDownloadURL(result.downloadURL()); respVO.setDurationMs(result.durationMs()); respVO.setPdfDurationMs(result.pdfDurationMs()); respVO.setBrowserStatus(result.browserStatus()); return success(respVO); } @DeleteMapping("/delete") @Operation(summary = "å 餿¥åå®ä¾") @Parameter(name = "id", description = "å®ä¾ç¼å·", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('qc-report:instance:delete')") public CommonResult<Boolean> deleteInstance(@RequestParam("id") Long id) { instanceService.deleteInstance(id); return success(true); } private QcReportInstanceGenerateRespVO toGenerateRespVO(GenerateResult result) { QcReportInstanceDO instance = result.instance(); QcReportInstanceGenerateRespVO respVO = new QcReportInstanceGenerateRespVO(); respVO.setId(instance.getId()); respVO.setReportNo(instance.getReportNo()); respVO.setTemplateVersion(instance.getTemplateVersion()); respVO.setStatus(instance.getStatus()); respVO.setErrors(result.errors()); return respVO; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/instance/vo/QcReportInstanceGenerateFromQcReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,37 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; /** * ç±è´¨æ£åçææ¥åçå ¥åã * <p> * ä¸ {@link QcReportInstanceGenerateReqVO} çåºå«ï¼é£ä¸ªæ¶çæ¯å·²ç»æ¼å¥½çæ¥åä¸ä¸æï¼ * è¿ä¸ªåªæ¶ãåªå¼ è´¨æ£å + ç¨åªä¸ªæ¨¡æ¿ãï¼æ°æ®ç±æå¡ç«¯ä» MES åã * äººå·¥éæ¨¡æ¿æ¯æ¢å®å£å¾ ââ åä¸å¼ è´¨æ£åå¯ä»¥ç¨ä¸å模æ¿åºä¸åç¨éçæ¥åï¼å é¨ç / 客æ·çï¼ã */ @Schema(description = "管çåå° - ç±è´¨æ£åçææ¥å Request VO") @Data public class QcReportInstanceGenerateFromQcReqVO { @Schema(description = "è´¨æ£ç±»åï¼1 æ¥æ 2 è¿ç¨ 3 åºè´§ 4 éè´§", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "è´¨æ£ç±»åä¸è½ä¸ºç©º") private Integer qcType; @Schema(description = "è´¨æ£åç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "è´¨æ£åç¼å·ä¸è½ä¸ºç©º") private Long qcId; @Schema(description = "æ¥å模æ¿ç¼å·ï¼ç±ç¨æ·éæ©", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "æ¥å模æ¿ç¼å·ä¸è½ä¸ºç©º") private Long templateId; @Schema(description = "æ¥å模æ¿çæ¬å·ãä¸ä¼ åç¨æ¨¡æ¿å½åå·²åå¸ççæ¬", example = "v1.0") private String version; @Schema(description = "æ¥åç¼å·ãä¸ä¼ åæ QR+æ¥æ+æµæ°´ èªå¨çæ", example = "QR20260919-0001") private String reportNo; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/instance/vo/QcReportInstanceGenerateFromQcRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,41 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.ArrayList; import java.util.List; /** * ç±è´¨æ£åçææ¥åçååºã * <p> * é¤äºãçæäºåªä»½æ¥åãï¼è¿å¿ é¡»æãè¿ä»½æ¥åæä»ä¹æ¯ç ãä¸èµ·å¸¦åæ¥ï¼ * {@link #undecidableCount} æ¯å ¶ä¸æå ³é®ç䏿¡ ââ æ£éªæ¨¡æ¿é没é è§æ ¼ä¸ä¸éæ¶ï¼ * æ£éªé¡¹ä¼æçå¤ä¸åºæ¥ï¼æ¥åçèµ·æ¥ååäºï¼å ¶å®æ¯æ°æ®æ²¡é å ¨ãåç«¯å¿ é¡»æ®æ¤æç¤ºã */ @Schema(description = "管çåå° - ç±è´¨æ£åçææ¥å Response VO") @Data public class QcReportInstanceGenerateFromQcRespVO { @Schema(description = "æ¥åå®ä¾ç¼å·", example = "2048") private Long id; @Schema(description = "æ¥åç¼å·", example = "QR20260919-0001") private String reportNo; @Schema(description = "å®é 使ç¨ç模æ¿çæ¬", example = "v1.0") private String templateVersion; @Schema(description = "æ¬æ¬¡åå ¥çæ£éªé¡¹æ¡æ°", example = "13") private Integer itemCount; @Schema(description = "没æå¤å®ç»è®ºç项æ°ï¼ãæ å¤å®è§åã+ è§åæ§è¡å¤±è´¥çãå¾ å¤å®ãï¼", example = "13") private Integer undecidableCount; @Schema(description = "软æç¤ºï¼æ ·åçåèãè·³è¿çåç»é¡¹ãå°æªå½å ¥å®æµå¼ç项") private List<String> warnings = new ArrayList<>(); @Schema(description = "æ¸²æææ°æ®ç¼ºå£ï¼æ¨¡æ¿ç»å®äºä½å½æ¶æ²¡æå¼çåæ®µ") private List<String> errors = new ArrayList<>(); } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/instance/vo/QcReportInstanceGenerateReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,39 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo; import cn.iocoder.yudao.module.qcreport.engine.context.ReportContext; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; /** * åºä»¶ï¼æ¸²æå¹¶è½åºæ¥åå®ä¾ï¼çå ¥åï¼é¢è§æ¥å£ä¹å¤ç¨å®ã * <p> * {@link #context} ç´æ¥ç¨å¼æç {@link ReportContext}ï¼ä¸å¦é ä¸ä¸ªéå VOï¼ * é£å°±æ¯è¿ä¸ªå¹³å°å¯¹å¤å¯ä¸çæ°æ®å¥çº¦ï¼éååºæ¥åªæ¯å¤ä¸ä»½éè¦åæ¥ç»´æ¤çåæ®µæ¸ åã * åæ®µçº§çè¾¹çæ ¡éªæ¾å¨ Service éåï¼å¼æå®ä½æ¬èº«ä¸ææ ¡éªæ³¨è§£ã */ @Schema(description = "管çåå° - æºè½è´¨æ£æ¥ååºä»¶ Request VO") @Data public class QcReportInstanceGenerateReqVO { @Schema(description = "模æ¿ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") @NotNull(message = "模æ¿ç¼å·ä¸è½ä¸ºç©º") private Long templateId; @Schema(description = "模æ¿çæ¬å·ãä¸ä¼ åç¨æ¨¡æ¿å½åå·²åå¸ççæ¬", example = "v1.0") private String version; @Schema(description = "æ¥åç¼å·ãä¸ä¼ åæ QR+æ¥æ+æµæ°´ èªå¨çæ", example = "QR20260918-0001") private String reportNo; @Schema(description = "ä¸å¡åæ®ç¼å·ï¼ç¨äºå°æ¥æåæ®åæ¥æ¥å", example = "1024") private String businessId; @Schema(description = "ä¸å¡åæ®ç±»åï¼å¦ mes_qc_iqc / mes_qc_ipqc / mes_qc_oqc / mes_qc_rqc", example = "mes_qc_oqc") private String businessType; @Schema(description = "æ¥åæ°æ®ä¸ä¸æï¼æ¥åçº§åæ®µ + æ£éªé¡¹å表", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "æ¥åæ°æ®ä¸è½ä¸ºç©º") private ReportContext context; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/instance/vo/QcReportInstanceGenerateRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,32 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; /** * åºä»¶ç»æã * <p> * ä¸å HTMLï¼å¯è½å¾é¿ï¼ï¼è¦æ£æè¯·æ¿ {@link #id} 廿¥è¯¦æ ã */ @Schema(description = "管çåå° - æºè½è´¨æ£æ¥ååºä»¶ Response VO") @Data public class QcReportInstanceGenerateRespVO { @Schema(description = "å®ä¾ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "æ¥åç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "QR20260918-0001") private String reportNo; @Schema(description = "çææ¶ä½¿ç¨ç模æ¿çæ¬å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1.0") private String templateVersion; @Schema(description = "æ¥åç¶æï¼0çæä¸ 1çææå 2çæå¤±è´¥", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; @Schema(description = "æ°æ®ç¼ºå£æ¸ åï¼æ¥åå·²ç»åºæ¥äºï¼ä½è¿äºåæ®µå½æ¶æ²¡æå¼") private List<String> errors; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/instance/vo/QcReportInstancePageReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,39 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo; import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管çåå° - æºè½è´¨æ£æ¥åå®ä¾å页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class QcReportInstancePageReqVO extends PageParam { @Schema(description = "模æ¿ç¼å·", example = "5") private Long templateId; @Schema(description = "æ¥åç¼å·ï¼æ¨¡ç³å¹é ï¼", example = "QR20260918") private String reportNo; @Schema(description = "ä¸å¡åæ®ç±»å", example = "mes_qc_oqc") private String businessType; @Schema(description = "ä¸å¡åæ®ç¼å·", example = "1024") private String businessId; @Schema(description = "æ¥åç¶æï¼0çæä¸ 1çææå 2çæå¤±è´¥", example = "1") private Integer status; @Schema(description = "å建æ¶é´") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/instance/vo/QcReportInstancePdfRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,46 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** * PDF åºä»¶ç»æï¼æå®ä¾éå·²åçæ¸²æäº§ç© HTML æå°æ PDF 并彿¡£å°éä»¶åºã */ @Schema(description = "管çåå° - æºè½è´¨æ£æ¥å PDF åºä»¶ Response VO") @Data public class QcReportInstancePdfRespVO { @Schema(description = "å®ä¾ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "æ¥åç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "QR20260918-0001") private String reportNo; @Schema(description = "PDF æä»¶å", requiredMode = Schema.RequiredMode.REQUIRED, example = "QR20260918-0001.pdf") private String fileName; @Schema(description = "PDF åèæ°", requiredMode = Schema.RequiredMode.REQUIRED, example = "58214") private Long byteSize; @Schema(description = "æä»¶ç¼å·ï¼system_storage_blob 主é®ï¼", example = "2048") private Long blobId; @Schema(description = "éä»¶å ³èç¼å·ï¼system_storage_attachment 主é®ï¼", example = "4096") private Long attachmentId; @Schema(description = "é¢è§å°åï¼ä¸´æ¶ç¾åï¼ä¼è¿æï¼è¦é¿æå¯ç¨è¯·éæ°æ¥è¯¢éä»¶å表ï¼") private String previewURL; @Schema(description = "ä¸è½½å°åï¼ä¸´æ¶ç¾åï¼ä¼è¿æï¼è¦é¿æå¯ç¨è¯·éæ°æ¥è¯¢éä»¶å表ï¼") private String downloadURL; @Schema(description = "æ¬æ¬¡åºä»¶æ»èæ¶ï¼æ¯«ç§ï¼", example = "1820") private Long durationMs; @Schema(description = "æå°èæ¶ï¼æ¯«ç§ï¼ï¼ä» å«ç页é¢èµæºä¸æå°ï¼ä¸æ»èæ¶ä¹å·®ä¸ºæé/å¯å¨çå¾ ", example = "940") private Long pdfDurationMs; @Schema(description = "æµè§å¨ç¶æï¼READYï¼å¤ç¨å·²æå®ä¾ï¼/ RESTARTEDï¼æ¬æ¬¡éå¯äºæµè§å¨ï¼", example = "READY") private String browserStatus; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/instance/vo/QcReportInstancePreviewRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,27 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; /** * é¢è§ç»æï¼åªæ¸²æï¼ä¸è½åºã * <p> * é¢è§**ä¸çæã䏿¶è**æ¥åç¼å·æµæ°´ï¼æ¿å°ç {@link #reportNo} æ¯å ¥åéå¸¦è¿æ¥çé£ä¸ªï¼ * 没带就æ¯ç©ºãçæ£çç¼å·å¨çææ¶ç¡®å®ã */ @Schema(description = "管çåå° - æºè½è´¨æ£æ¥åé¢è§ Response VO") @Data public class QcReportInstancePreviewRespVO { @Schema(description = "æ¬æ¬¡æ¸²æä½¿ç¨çæ¥åç¼å·ï¼æ¥èªå ¥åï¼é¢è§ä¸çæç¼å·ï¼", example = "QR20260918-0001") private String reportNo; @Schema(description = "渲æäº§ç© HTML") private String html; @Schema(description = "æ°æ®ç¼ºå£æ¸ åï¼æ¨¡æ¿ç»å®äºä½æ°æ®é没æçåæ®µã被æ¦ä¸çä¸å®å ¨å±æ§ç") private List<String> errors; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/instance/vo/QcReportInstanceRegenerateRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,26 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; /** * éæ°çæç»æï¼ç¨å»ç»çæ°æ®å¿«ç § + å䏿¨¡æ¿çæ¬é渲ã * <p> * æ¥åç¼å·ä¸æ°æ®é½ä¸åï¼äº§ç©æå¤ç°æ§åºå½ä¸é¦æ¬¡çæéåç¸åã */ @Schema(description = "管çåå° - æºè½è´¨æ£æ¥åéæ°çæ Response VO") @Data public class QcReportInstanceRegenerateRespVO { @Schema(description = "å®ä¾ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "æ¥åç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "QR20260918-0001") private String reportNo; @Schema(description = "æ°æ®ç¼ºå£æ¸ å") private List<String> errors; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/instance/vo/QcReportInstanceRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,51 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; /** * æ¥åå®ä¾çå±ç¤ºå段ã * <p> * ææä¸å« {@code dataSnapshot}ï¼é£æ¯ææ¥ãè¿ä»½å岿¥å彿¶ç¨çä»ä¹æ°æ®ãç¨çï¼ * ä½ç§¯å¤§ãå端ä¹ä¸éè¦ï¼åå®è¯·èµ° {@code /qc-report/instance/snapshot}ã */ @Schema(description = "管çåå° - æºè½è´¨æ£æ¥åå®ä¾ Response VO") @Data public class QcReportInstanceRespVO { @Schema(description = "å®ä¾ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "æ¥åç¼å·ï¼ä¸å¡å¯ä¸ï¼", requiredMode = Schema.RequiredMode.REQUIRED, example = "QR20260918-0001") private String reportNo; @Schema(description = "模æ¿ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") private Long templateId; @Schema(description = "çææ¶ä½¿ç¨ç模æ¿çæ¬å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1.0") private String templateVersion; @Schema(description = "ä¸å¡åæ®ç¼å·", example = "1024") private String businessId; @Schema(description = "ä¸å¡åæ®ç±»å", example = "mes_qc_oqc") private String businessType; @Schema(description = "渲æäº§ç© HTML") private String renderHtml; @Schema(description = "æ¥åç¶æï¼0çæä¸ 1çææå 2çæå¤±è´¥", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; @Schema(description = "å建人", example = "1") private String creator; @Schema(description = "å建æ¶é´", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; @Schema(description = "æ´æ°æ¶é´") private LocalDateTime updateTime; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/template/QcReportTemplateController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,94 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.template; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.qcreport.controller.admin.template.vo.QcReportTemplatePageReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.template.vo.QcReportTemplateRespVO; import cn.iocoder.yudao.module.qcreport.controller.admin.template.vo.QcReportTemplateSaveReqVO; import cn.iocoder.yudao.module.qcreport.dal.dataobject.template.QcReportTemplateDO; import cn.iocoder.yudao.module.qcreport.service.template.QcReportTemplateService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管çåå° - æºè½è´¨æ£æ¥å模æ¿") @RestController @RequestMapping("/qc-report/template") @Validated public class QcReportTemplateController { @Resource private QcReportTemplateService templateService; @PostMapping("/create") @Operation(summary = "å建æ¥å模æ¿") @PreAuthorize("@ss.hasPermission('qc-report:template:create')") public CommonResult<Long> createTemplate(@Valid @RequestBody QcReportTemplateSaveReqVO createReqVO) { return success(templateService.createTemplate(createReqVO)); } @PutMapping("/update") @Operation(summary = "æ´æ°æ¥å模æ¿") @PreAuthorize("@ss.hasPermission('qc-report:template:update')") public CommonResult<Boolean> updateTemplate(@Valid @RequestBody QcReportTemplateSaveReqVO updateReqVO) { templateService.updateTemplate(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "å 餿¥å模æ¿") @Parameter(name = "id", description = "ç¼å·", required = true) @PreAuthorize("@ss.hasPermission('qc-report:template:delete')") public CommonResult<Boolean> deleteTemplate(@RequestParam("id") Long id) { templateService.deleteTemplate(id); return success(true); } @PostMapping("/copy") @Operation(summary = "å¤å¶æ¥å模æ¿") @Parameter(name = "id", description = "ç¼å·", required = true) @PreAuthorize("@ss.hasPermission('qc-report:template:create')") public CommonResult<Long> copyTemplate(@RequestParam("id") Long id) { return success(templateService.copyTemplate(id)); } @GetMapping("/get") @Operation(summary = "è·å¾æ¥å模æ¿") @Parameter(name = "id", description = "ç¼å·", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('qc-report:template:query')") public CommonResult<QcReportTemplateRespVO> getTemplate(@RequestParam("id") Long id) { QcReportTemplateDO template = templateService.getTemplate(id); return success(BeanUtils.toBean(template, QcReportTemplateRespVO.class)); } @GetMapping("/page") @Operation(summary = "è·å¾æ¥å模æ¿å页") @PreAuthorize("@ss.hasPermission('qc-report:template:query')") public CommonResult<PageResult<QcReportTemplateRespVO>> getTemplatePage(@Valid QcReportTemplatePageReqVO pageReqVO) { PageResult<QcReportTemplateDO> pageResult = templateService.getTemplatePage(pageReqVO); return success(BeanUtils.toBean(pageResult, QcReportTemplateRespVO.class)); } @GetMapping("/selectable") @Operation(summary = "è·å¾å¯ç¨äºåºä»¶ç模æ¿å表", description = "ä¸ä¸ªæ¡ä»¶åæ¶æ»¡è¶³æä¼è¿åï¼æ¥åç±»åä¸è´ã模æ¿å·²å¯ç¨ãå½åçæ¬å·²åå¸ä¸ç»å¸æå¯æ¸²æçå 容ã" + "æå䏿¡æ¡çæ¯ãæå¼è¿è®¾è®¡å¨ä½æ²¡æ¾ç»ä»¶ãççæ¬ââ宿¸²æåºæ¥æ¯ä¸å¼ ç½çº¸ä¸ä¸æ¥éã") @Parameter(name = "reportType", description = "æ¥åç±»åï¼å³ mes_qc_type çæ°å¼å符串", required = true, example = "1") @PreAuthorize("@ss.hasPermission('qc-report:template:query')") public CommonResult<List<QcReportTemplateRespVO>> getSelectableTemplateList( @RequestParam("reportType") String reportType) { List<QcReportTemplateDO> list = templateService.getSelectableTemplates(reportType); return success(BeanUtils.toBean(list, QcReportTemplateRespVO.class)); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/template/vo/QcReportTemplatePageReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,45 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.template.vo; import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管çåå° - æºè½è´¨æ£æ¥å模æ¿å页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class QcReportTemplatePageReqVO extends PageParam { @Schema(description = "模æ¿ç¼ç ", example = "QC_REPORT_IQC") private String templateCode; @Schema(description = "模æ¿åç§°", example = "æ¥ææ£éªæ¥å") private String templateName; @Schema(description = "æå±è¡ä¸", example = "manufacturing") private String industry; @Schema(description = "æ¥åç±»å", example = "IQC") private String reportType; @Schema(description = "模æ¿ç¶æï¼0å¯ç¨ 1åç¨", example = "0") private Integer status; @Schema(description = "å½åçæ¬å·", example = "v1.0") private String currentVersion; @Schema(description = "å建人", example = "1") private String creator; @Schema(description = "å建æ¶é´") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/template/vo/QcReportTemplateRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,51 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.template.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管çåå° - æºè½è´¨æ£æ¥åæ¨¡æ¿ Response VO") @Data public class QcReportTemplateRespVO { @Schema(description = "模æ¿ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "模æ¿ç¼ç ", requiredMode = Schema.RequiredMode.REQUIRED, example = "QC_REPORT_IQC_A4") private String templateCode; @Schema(description = "模æ¿åç§°", requiredMode = Schema.RequiredMode.REQUIRED, example = "æ¥ææ£éªæ¥å") private String templateName; @Schema(description = "æå±è¡ä¸", example = "manufacturing") private String industry; @Schema(description = "æ¥åç±»å", example = "IQC") private String reportType; @Schema(description = "çº¸å¼ å°ºå¯¸", example = "A4") private String pageSize; @Schema(description = "çº¸å¼ æ¹å", example = "portrait") private String orientation; @Schema(description = "模æ¿ç¶æï¼0å¯ç¨ 1åç¨", example = "0") private Integer status; @Schema(description = "å½åçæ¬å·", example = "v1.0") private String currentVersion; @Schema(description = "æ¨¡æ¿æè¿°", example = "ç¨äºæ¥ææ£éªåç A4 æ¥å模æ¿") private String description; @Schema(description = "å建人", example = "1") private String creator; @Schema(description = "å建æ¶é´", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; @Schema(description = "æ´æ°æ¶é´") private LocalDateTime updateTime; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/template/vo/QcReportTemplateSaveReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,57 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.template.vo; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.qcreport.enums.QcReportEnums.OrientationEnum; import cn.iocoder.yudao.module.qcreport.enums.QcReportEnums.PageSizeEnum; import cn.iocoder.yudao.module.qcreport.enums.QcReportEnums.TemplateStatusEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import lombok.Data; @Schema(description = "管çåå° - æºè½è´¨æ£æ¥åæ¨¡æ¿ å建/ä¿®æ¹ Request VO") @Data public class QcReportTemplateSaveReqVO { @Schema(description = "模æ¿ç¼å·", example = "1024") private Long id; @Schema(description = "模æ¿ç¼ç ", requiredMode = Schema.RequiredMode.REQUIRED, example = "QC_REPORT_IQC_A4") @NotEmpty(message = "模æ¿ç¼ç ä¸è½ä¸ºç©º") @Size(max = 64, message = "模æ¿ç¼ç é¿åº¦ä¸è½è¶ è¿ 64 个å符") @Pattern(regexp = "^[A-Za-z0-9_-]+$", message = "模æ¿ç¼ç åªè½å å«åæ¯ãæ°åãä¸å线åçæ¨ªçº¿") private String templateCode; @Schema(description = "模æ¿åç§°", requiredMode = Schema.RequiredMode.REQUIRED, example = "æ¥ææ£éªæ¥å") @NotEmpty(message = "模æ¿åç§°ä¸è½ä¸ºç©º") @Size(max = 128, message = "模æ¿åç§°é¿åº¦ä¸è½è¶ è¿ 128 个å符") private String templateName; @Schema(description = "æå±è¡ä¸", example = "manufacturing") @Size(max = 32, message = "æå±è¡ä¸é¿åº¦ä¸è½è¶ è¿ 32 个å符") private String industry; @Schema(description = "æ¥åç±»å", example = "IQC") @Size(max = 32, message = "æ¥åç±»åé¿åº¦ä¸è½è¶ è¿ 32 个å符") private String reportType; @Schema(description = "çº¸å¼ å°ºå¯¸", requiredMode = Schema.RequiredMode.REQUIRED, example = "A4") @NotEmpty(message = "çº¸å¼ å°ºå¯¸ä¸è½ä¸ºç©º") @InEnum(PageSizeEnum.class) private String pageSize; @Schema(description = "çº¸å¼ æ¹å", requiredMode = Schema.RequiredMode.REQUIRED, example = "portrait") @NotEmpty(message = "çº¸å¼ æ¹åä¸è½ä¸ºç©º") @InEnum(OrientationEnum.class) private String orientation; @Schema(description = "模æ¿ç¶æï¼0å¯ç¨ 1åç¨", example = "0") @InEnum(TemplateStatusEnum.class) private Integer status; @Schema(description = "æ¨¡æ¿æè¿°", example = "ç¨äºæ¥ææ£éªåç A4 æ¥å模æ¿") @Size(max = 512, message = "æ¨¡æ¿æè¿°é¿åº¦ä¸è½è¶ è¿ 512 个å符") private String description; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/version/QcReportTemplateVersionController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,113 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.version; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.qcreport.controller.admin.version.vo.QcReportTemplateVersionPageReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.version.vo.QcReportTemplateVersionRespVO; import cn.iocoder.yudao.module.qcreport.controller.admin.version.vo.QcReportTemplateVersionSaveReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.version.vo.QcReportTemplateVersionUpdateReqVO; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.QcReportTemplateVersionDO; import cn.iocoder.yudao.module.qcreport.service.version.QcReportTemplateVersionService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管çåå° - æºè½è´¨æ£æ¥å模æ¿çæ¬") @RestController @RequestMapping("/qc-report/template-version") @Validated public class QcReportTemplateVersionController { @Resource private QcReportTemplateVersionService versionService; @PostMapping("/create") @Operation(summary = "å建模æ¿çæ¬") @PreAuthorize("@ss.hasPermission('qc-report:template:update')") public CommonResult<Long> createVersion(@Valid @RequestBody QcReportTemplateVersionSaveReqVO createReqVO) { return success(versionService.createVersion(createReqVO)); } @PutMapping("/update") @Operation(summary = "æ´æ°æ¨¡æ¿çæ¬ï¼å·²åå¸ççæ¬ä¸å¯ä¿®æ¹ï¼") @PreAuthorize("@ss.hasPermission('qc-report:template:update')") public CommonResult<Boolean> updateVersion(@Valid @RequestBody QcReportTemplateVersionUpdateReqVO updateReqVO) { versionService.updateVersion(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "å 餿¨¡æ¿çæ¬ï¼ä» è稿å¯å é¤ï¼") @Parameter(name = "id", description = "ç¼å·", required = true) @PreAuthorize("@ss.hasPermission('qc-report:template:delete')") public CommonResult<Boolean> deleteVersion(@RequestParam("id") Long id) { versionService.deleteVersion(id); return success(true); } @PostMapping("/publish") @Operation(summary = "å叿¨¡æ¿çæ¬") @Parameter(name = "id", description = "ç¼å·", required = true) @PreAuthorize("@ss.hasPermission('qc-report:template:publish')") public CommonResult<Boolean> publishVersion(@RequestParam("id") Long id) { versionService.publishVersion(id); return success(true); } @PostMapping("/rollback") @Operation(summary = "忻尿å®åå²çæ¬") @Parameter(name = "id", description = "ç¼å·", required = true) @PreAuthorize("@ss.hasPermission('qc-report:template:publish')") public CommonResult<Boolean> rollbackVersion(@RequestParam("id") Long id) { versionService.rollbackVersion(id); return success(true); } @PostMapping("/disable") @Operation(summary = "åç¨æ¨¡æ¿çæ¬") @Parameter(name = "id", description = "ç¼å·", required = true) @PreAuthorize("@ss.hasPermission('qc-report:template:publish')") public CommonResult<Boolean> disableVersion(@RequestParam("id") Long id) { versionService.disableVersion(id); return success(true); } @GetMapping("/get") @Operation(summary = "è·å¾æ¨¡æ¿çæ¬") @Parameter(name = "id", description = "ç¼å·", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('qc-report:template:query')") public CommonResult<QcReportTemplateVersionRespVO> getVersion(@RequestParam("id") Long id) { QcReportTemplateVersionDO version = versionService.getVersion(id); return success(BeanUtils.toBean(version, QcReportTemplateVersionRespVO.class)); } @GetMapping("/list-by-template") @Operation(summary = "è·å¾æ¨¡æ¿çææçæ¬") @Parameter(name = "templateId", description = "模æ¿ç¼å·", required = true) @PreAuthorize("@ss.hasPermission('qc-report:template:query')") public CommonResult<List<QcReportTemplateVersionRespVO>> getVersionListByTemplateId( @RequestParam("templateId") Long templateId) { List<QcReportTemplateVersionDO> list = versionService.getVersionListByTemplateId(templateId); return success(BeanUtils.toBean(list, QcReportTemplateVersionRespVO.class)); } @GetMapping("/page") @Operation(summary = "è·å¾æ¨¡æ¿çæ¬å页") @PreAuthorize("@ss.hasPermission('qc-report:template:query')") public CommonResult<PageResult<QcReportTemplateVersionRespVO>> getVersionPage( @Valid QcReportTemplateVersionPageReqVO pageReqVO) { PageResult<QcReportTemplateVersionDO> pageResult = versionService.getVersionPage(pageReqVO); return success(BeanUtils.toBean(pageResult, QcReportTemplateVersionRespVO.class)); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/version/vo/QcReportTemplateVersionPageReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,33 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.version.vo; import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管çåå° - æºè½è´¨æ£æ¥å模æ¿çæ¬å页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class QcReportTemplateVersionPageReqVO extends PageParam { @Schema(description = "模æ¿ç¼å·", example = "1024") private Long templateId; @Schema(description = "çæ¬å·", example = "v1.0") private String version; @Schema(description = "çæ¬ç¶æï¼0è稿 1å·²åå¸ 2å·²åç¨", example = "1") private Integer status; @Schema(description = "å建æ¶é´") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/version/vo/QcReportTemplateVersionRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,40 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.version.vo; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.ReportTemplateSchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Schema(description = "管çåå° - æºè½è´¨æ£æ¥å模æ¿çæ¬ Response VO") @Data public class QcReportTemplateVersionRespVO { @Schema(description = "çæ¬ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; @Schema(description = "模æ¿ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long templateId; @Schema(description = "çæ¬å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1.0") private String version; @Schema(description = "æ¨¡æ¿ Schemaï¼è®¾è®¡å¨äº§ç©ï¼") private ReportTemplateSchema schema; @Schema(description = "çæ¬ç¶æï¼0è稿 1å·²åå¸ 2å·²åç¨", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") private Integer status; @Schema(description = "çæ¬è¯´æ", example = "å¢å æ½æ ·æ£æµè¡¨æ ¼") private String description; @Schema(description = "å建人", example = "1") private String creator; @Schema(description = "å建æ¶é´", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; @Schema(description = "æ´æ°æ¶é´") private LocalDateTime updateTime; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/version/vo/QcReportTemplateVersionSaveReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,33 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.version.vo; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.ReportTemplateSchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import lombok.Data; @Schema(description = "管çåå° - æºè½è´¨æ£æ¥å模æ¿çæ¬ å建 Request VO") @Data public class QcReportTemplateVersionSaveReqVO { @Schema(description = "çæ¬ç¼å·", example = "1024") private Long id; @Schema(description = "模æ¿ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "模æ¿ç¼å·ä¸è½ä¸ºç©º") private Long templateId; @Schema(description = "çæ¬å·ï¼çç©ºåæææ°çæ¬èªå¨éå¢ï¼å¦ v1.0 -> v1.1ï¼", example = "v1.0") @Pattern(regexp = "^v\\d+(\\.\\d+)*$", message = "çæ¬å·æ ¼å¼å¿ é¡»å½¢å¦ v1.0") @Size(max = 32, message = "çæ¬å·é¿åº¦ä¸è½è¶ è¿ 32 个å符") private String version; @Schema(description = "æ¨¡æ¿ Schemaï¼è®¾è®¡å¨äº§ç©ï¼") private ReportTemplateSchema schema; @Schema(description = "çæ¬è¯´æ", example = "å¢å æ½æ ·æ£æµè¡¨æ ¼") @Size(max = 512, message = "çæ¬è¯´æé¿åº¦ä¸è½è¶ è¿ 512 个å符") private String description; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/controller/admin/version/vo/QcReportTemplateVersionUpdateReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,29 @@ package cn.iocoder.yudao.module.qcreport.controller.admin.version.vo; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.ReportTemplateSchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; /** * 模æ¿çæ¬ä¿®æ¹ Request VO * <p> * ä¸å建 VO å离ï¼å½å±æ¨¡æ¿ä¸çæ¬å·å¨å建åä¸å¯åæ´ï¼æ´æ°åªå 许æ¹è®¾è®¡å 容ä¸è¯´æ */ @Schema(description = "管çåå° - æºè½è´¨æ£æ¥å模æ¿çæ¬ ä¿®æ¹ Request VO") @Data public class QcReportTemplateVersionUpdateReqVO { @Schema(description = "çæ¬ç¼å·", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "çæ¬ç¼å·ä¸è½ä¸ºç©º") private Long id; @Schema(description = "æ¨¡æ¿ Schemaï¼è®¾è®¡å¨äº§ç©ï¼") private ReportTemplateSchema schema; @Schema(description = "çæ¬è¯´æ", example = "å¢å æ½æ ·æ£æµè¡¨æ ¼") @Size(max = 512, message = "çæ¬è¯´æé¿åº¦ä¸è½è¶ è¿ 512 个å符") private String description; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/dal/dataobject/instance/QcReportInstanceDO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,72 @@ package cn.iocoder.yudao.module.qcreport.dal.dataobject.instance; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.Jackson3TypeHandler; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import java.util.Map; /** * æ¥åå®ä¾ DO * <p> * çææ¥åæ¶å¿ é¡»ä¿å dataSnapshotï¼å岿¥åä¸è½å 为ä¸å¡æ°æ®ååèåçååã */ @TableName(value = "qc_report_instance", autoResultMap = true) @KeySequence("qc_report_instance_seq") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class QcReportInstanceDO extends BaseDO { /** * å®ä¾ç¼å· */ @TableId private Long id; /** * æ¥åç¼å·ï¼ä¸å¡å¯ä¸ï¼ */ private String reportNo; /** * 模æ¿ç¼å· */ private Long templateId; /** * çææ¶ä½¿ç¨ç模æ¿çæ¬å· */ private String templateVersion; /** * ä¸å¡åæ®ç¼å·ï¼å¦è´¨æ£å ID */ private String businessId; /** * ä¸å¡åæ®ç±»åï¼å¦ mes_qc_iqc */ private String businessType; /** * æ°æ®å¿«ç §ï¼æ¸²ææ¶ç宿´æ°æ®ä¸ä¸æï¼ä¿è¯å岿¥åå¯å¤ç° */ @TableField(typeHandler = Jackson3TypeHandler.class) private Map<String, Object> dataSnapshot; /** * 渲æäº§ç© HTML */ private String renderHtml; /** * æ¥åç¶æãè§ QcReportInstanceStatusEnum */ private Integer status; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/dal/dataobject/render/QcReportRenderRecordDO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,63 @@ package cn.iocoder.yudao.module.qcreport.dal.dataobject.render; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import java.time.LocalDateTime; /** * æºè½è´¨æ£æ¥å渲æè®°å½ DO * <p> * åªä¸ºææ¥ç¨ï¼ä¸æ¬¡ PDF åºä»¶å äºå¤ä¹ ãæµè§å¨ä»ä¹ç¶æã失败æ¶å¼å¸¸æ æ¯ä»ä¹ã * æ¥åæ£æä¸å¨è¿éï¼æåä¸å¦ä¹ä¸å½±åæ¥åæ¬èº«ã */ @TableName("qc_report_render_record") @KeySequence("qc_report_render_record_seq") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class QcReportRenderRecordDO extends BaseDO { /** ç¼å· */ @TableId private Long id; /** æ¥åå®ä¾ç¼å· */ private Long reportId; /** 模æ¿ç¼å· */ private Long templateId; /** ä¸å¡åæ®ç¼å· */ private String businessId; /** 渲æå¼å§æ¶é´ */ private LocalDateTime renderStartTime; /** 渲æç»ææ¶é´ */ private LocalDateTime renderEndTime; /** 渲æèæ¶ï¼æ¯«ç§ï¼ï¼ä»å¼å§æå°å° PDF åèå°æ */ private Long renderDuration; /** PDF çæèæ¶ï¼æ¯«ç§ï¼ */ private Long pdfDuration; /** æµè§å¨ç¶æï¼READYï¼å¤ç¨å·²æå®ä¾ï¼/ RESTARTEDï¼æ¬æ¬¡éå¯äºæµè§å¨ï¼/ FAILEDï¼å¯å¨å¤±è´¥ï¼ */ private String browserStatus; /** å¼å¸¸å æ ï¼æåæ¶ä¸ºç©º */ private String errorStack; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/dal/dataobject/template/QcReportTemplateDO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,69 @@ package cn.iocoder.yudao.module.qcreport.dal.dataobject.template; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; /** * æ¥åæ¨¡æ¿ DO */ @TableName("qc_report_template") @KeySequence("qc_report_template_seq") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class QcReportTemplateDO extends BaseDO { /** * 模æ¿ç¼å· */ @TableId private Long id; /** * 模æ¿ç¼ç */ private String templateCode; /** * 模æ¿åç§° */ private String templateName; /** * æå±è¡ä¸ãè§ QcReportIndustryEnum */ private String industry; /** * æ¥åç±»åãè§ QcReportTypeEnum */ private String reportType; /** * çº¸å¼ å°ºå¯¸ãè§ QcReportPageSizeEnumï¼å¦ A4 */ private String pageSize; /** * çº¸å¼ æ¹åãè§ QcReportOrientationEnumï¼portrait / landscape */ private String orientation; /** * 模æ¿ç¶æãè§ QcReportTemplateStatusEnum */ private Integer status; /** * å½åçæ¬å·ï¼å¦ v1.0 */ private String currentVersion; /** * æ¨¡æ¿æè¿° */ private String description; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/dal/dataobject/version/QcReportTemplateVersionDO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,58 @@ package cn.iocoder.yudao.module.qcreport.dal.dataobject.version; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.Jackson3TypeHandler; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; /** * æ¥å模æ¿çæ¬ DO * <p> * çæ¬åå¸åä¸å 许è¦çï¼åªè½æ°å»ºçæ¬ï¼ä¿è¯å岿¥åå¯å¤ç°ã */ @TableName(value = "qc_report_template_version", autoResultMap = true) @KeySequence("qc_report_template_version_seq") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class QcReportTemplateVersionDO extends BaseDO { /** * çæ¬ç¼å· */ @TableId private Long id; /** * 模æ¿ç¼å· */ private Long templateId; /** * çæ¬å·ï¼å¦ v1.0 */ private String version; /** * æ¨¡æ¿ Schemaï¼GrapesJS JSON + Quality Component JSON + æ°æ®æº + ç»å® + è§å + æ ·å¼ï¼ */ @TableField(value = "schema_json", typeHandler = Jackson3TypeHandler.class) private ReportTemplateSchema schema; /** * çæ¬ç¶æãè§ QcReportVersionStatusEnum */ private Integer status; /** * çæ¬è¯´æ */ private String description; } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/dal/dataobject/version/ReportTemplateSchema.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,145 @@ package cn.iocoder.yudao.module.qcreport.dal.dataobject.version; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import java.util.List; import java.util.Map; /** * æ¥åæ¨¡æ¿ Schema ââ 设计å¨äº§ç©ï¼ä¹æ¯æ¥ååºä»¶çå¯ä¸è¾å ¥ã * <p> * 模æ¿ä¸ä¿å裸 HTMLãä¿åçæ¯ãç»å¸æ°æ® + ä¸å¡è¯ä¹ãï¼ç±æ¸²æå¼ææ®æ¤çæ HTMLï¼ * è¿æ ·åä¸ä»½æ¨¡æ¿å¨ä¸å渲æç¯å¢ä¸äº§ç©ä¸è´ï¼ä¹ææå¯è½åæå¡ç«¯ Sanitizationã * * <h3>åæ®µåå·¥</h3> * <table> * <tr><td>{@link #grapes}</td><td>GrapesJS åå§é¡¹ç®æ°æ®ã设计å¨é 宿 æè¿åç»å¸ï¼<b>渲æå¼æä¹è¯»å®</b>ï¼æ£æç»æå¨è¿éï¼</td></tr> * <tr><td>{@link #components}</td><td>è´¨éç»ä»¶ä¸å¡è¯ä¹æ¸ åãæ¸²æä¸è¯»ï¼ä¾è®¾è®¡å¨å®ä½ç»ä»¶ãä¾åç» AI çææ¨¡æ¿æ¶çè§£ç»æ</td></tr> * <tr><td>{@link #rules}</td><td>å¤å®è§åãæ¸²æåç±å¤å®å¨æ±å¼ï¼å³å®æ¯é¡¹ PASS/FAIL 䏿¥åç»è®º</td></tr> * <tr><td>{@link #page}</td><td>çº¸å¼ ä¸é¡µè¾¹è·ï¼æ¸²ææ¶æ¢ç®æ CSS {@code @page}</td></tr> * </table> * * <h3>è¯ä¹å±æ¯<strong>ææçåé</strong>ï¼éè¦ï¼</h3> * {@link #components} åªè¡¨è¾¾ãç¨å·²æ³¨åçè´¨éç»ä»¶æåºæ¥çæå¹³ç»æãï¼ * <b>ä¸è¡¨è¾¾ä»»æ HTML</b>ââç¨æ·æå·¥æè¿ç»å¸çè£¸è¡¨æ ¼ãèªå®ä¹ div ä¸å¨å ¶å ã * è¿æ¯æææ¶çªçè¾¹çï¼ä¸æ¯å®ç°çæ¼ï¼æè¿æ¡è¾¹çï¼AI çææ¨¡æ¿æ¶æè½è¢«çº¦ææ * ãåªè½ç¨å·²æ³¨åç»ä»¶å½ç§¯æ¨ãï¼è䏿¯å䏿®µè°ä¹ä¸æ¢æ¸²æç HTMLã * ä»»ä½ãç»è¯ä¹å±å 个ä¸è½ customHtml åæ®µãçæ¹å¨é½å¨æè¿æ¡è¾¹çï¼éè¦å 讨论ã * * <h3>å ¼å®¹æ§</h3> * <p> * {@code schema_json} åéå·²ç»åçå« 1.0 åæ®µï¼dataSources / bindings / stylesï¼çåå² JSONï¼ * æä»¥è¿éæ¾å¼æ 注 {@link JsonIgnoreProperties} 容éã * <p> * 说æä¸å¥ï¼å å¾å人以为å®å¿ ä¸å¯å°ï¼<b>廿è¿ä¸ªæ³¨è§£ï¼å ¼å®¹æ§æµè¯ä¹æ¯ç»¿ç</b>ââ * 读åºé£æ¡é¾è·¯ç¨ç {@code Jackson3TypeHandler} èªå·± new ç ObjectMapperï¼ * è Jackson 3 ç {@code FAIL_ON_UNKNOWN_PROPERTIES} é»è®¤å°±æ¯å ³çã * çæ£éè¦å®é²çæ¯å¦å¤ä¸¤æ¡ï¼Spring ååºåå请æ±ä½æ¶ç¨ç ObjectMapperï¼é ç½®ä¸åæ¬æ¨¡åæ§å¶ï¼ï¼ * 以åå°æ¥æäººç»ç±»åå¤ç卿¢ä¸ä¸ªæ´ä¸¥æ ¼ç Mapperãççæ¯ææå¾åæï¼ä¸æ¯æè®¾ã * * <h3>çæ¬åå²</h3> * <ul> * <li><b>1.0</b>ï¼åçãå« dataSources / bindings / styles ä¸ä¸ªå段ï¼ä½å®ä»¬å§ç»æ¯ç©ºæ°ç»ï¼ * æ¢æ 人åå ¥ä¹æ 人读åï¼å±æ»æ°æ®ã</li> * <li><b>1.1</b>ï¼å é¤ä¸è¿°ä¸ä¸ªæ»å段ï¼{@code components} ç± {@code Map} æ¹ä¸ºå¼ºç±»å * {@link QualityComponentNode}ãå æ»åæ®µåæ¬å°±æä¸ºç©ºæ°ç»ï¼1.0 â 1.1 æ éæ°æ®è¿ç§»ã</li> * </ul> */ @Data @JsonIgnoreProperties(ignoreUnknown = true) public class ReportTemplateSchema { /** * å½å Schema çæ¬å·ã * <p> * ä¸å端 {@code designer/constants.ts} ç {@code SCHEMA_VERSION} å¿ é¡»ä¸è´ï¼ * 两端ååä¸ä»½æ¯åå²åå ï¼å端ä¸åä¸è®¾è®¡å¨æå»ºï¼ï¼æ¹è¿éå°±è¦åæ¥æ¹é£è¾¹ã */ public static final String SCHEMA_VERSION = "1.1"; /** * Schema çæ¬ï¼ç¨äºåç»å ¼å®¹å级 */ private String schemaVersion; /** * çº¸å¼ ä¸é¡µè¾¹è·é ç½® */ private Page page; /** * GrapesJS åå§ç»å¸æ°æ®ï¼ä¿çä»¥ä¾¿è®¾è®¡å¨æ æè¿åï¼ */ private Map<String, Object> grapes; /** * è´¨éç»ä»¶ä¸å¡è¯ä¹æ¸ åï¼æææ¡£é¡ºåºæå */ private List<QualityComponentNode> components; /** * å¤å®è§åå®ä¹ã * <p> * å ç´ å½¢ç¶ä¸º {@code {id, name, scope, expression, enabled}}ï¼ç± * {@code QualityRuleDefinition.from(Map)} 宽æ¾è¯»åââå岿°æ®éå¯è½æç¼ºå段çè§åï¼ * 读ä¸å¨çè·³è¿è䏿¯è®©æ´ä»½æ¨¡æ¿æä¸å¼ã */ private List<Map<String, Object>> rules; /** * çº¸å¼ ä¸é¡µè¾¹è·é ç½® */ @Data public static class Page { /** * çº¸å¼ å°ºå¯¸ï¼A3 / A4 / A5 / Letter */ private String size; /** * çº¸å¼ æ¹åï¼portrait / landscape */ private String orientation; /** * 页边è·ï¼åä½ mm */ private Margin margin; } /** * 页边è·ï¼åä½ mm */ @Data public static class Margin { private Double top; private Double right; private Double bottom; private Double left; } /** * ç»å¸ä¸çä¸ä¸ªè´¨éç»ä»¶èç¹ã * <p> * åªè®°ãæ¯è°ãå¨åªãä»ä¹å±æ§ãï¼ä¸è®°åèç¹ââç»å¸çåµå¥ç»æä»¥ {@link #grapes} 为åï¼ * è¿éååä¸ä»½æ å°±åºç°ä¸¤ä¸ªçç¸æ¥æºï¼è¿æ©å¯¹ä¸ä¸ã */ @Data public static class QualityComponentNode { /** ç»å¸å ç»ä»¶ id */ private String id; /** ç»ä»¶å¨ææ¡£ä¸ç顺åºï¼ä¾ç»å®ä¸å®ä½ä½¿ç¨ */ private Integer index; /** ç»ä»¶ç±»åï¼å³ç»å¸èç¹ä¸ç data-quality-type */ private String qualityType; /** * ç»ä»¶çä¸å¡å±æ§ã * <p> * åå¼ç»ä¸æ¯ {@code data-qc-*} / {@code data-quality-type} è¿ç±»åç¬¦ä¸²å±æ§ï¼ * ç¨ Object æ¯å 为 GrapesJS ç getAttributes() åæ ·å¸¦åºï¼å¯è½æéå符串å¼ã */ private Map<String, Object> attributes; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/dal/mysql/instance/QcReportInstanceMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,49 @@ package cn.iocoder.yudao.module.qcreport.dal.mysql.instance; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.qcreport.controller.admin.instance.vo.QcReportInstancePageReqVO; import cn.iocoder.yudao.module.qcreport.dal.dataobject.instance.QcReportInstanceDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; @Mapper public interface QcReportInstanceMapper extends BaseMapperX<QcReportInstanceDO> { default PageResult<QcReportInstanceDO> selectPage(QcReportInstancePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX<QcReportInstanceDO>() .eqIfPresent(QcReportInstanceDO::getTemplateId, reqVO.getTemplateId()) .likeIfPresent(QcReportInstanceDO::getReportNo, reqVO.getReportNo()) .eqIfPresent(QcReportInstanceDO::getBusinessType, reqVO.getBusinessType()) .eqIfPresent(QcReportInstanceDO::getBusinessId, reqVO.getBusinessId()) .eqIfPresent(QcReportInstanceDO::getStatus, reqVO.getStatus()) .betweenIfPresent(QcReportInstanceDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(QcReportInstanceDO::getId)); } default QcReportInstanceDO selectByReportNo(String reportNo) { return selectOne(new LambdaQueryWrapperX<QcReportInstanceDO>() .eq(QcReportInstanceDO::getReportNo, reportNo)); } /** * åå½å¤©æµæ°´æå¤§ç飿¡ç¼å·ï¼ç¨äºæ¨åºä¸ä¸ä¸ªåºå·ã * <p> * **å¿ é¡»èµ°æå SQLãæè½¯å é¤çè¡ä¹ç®è¿æ¥**ï¼æ¡ä»¶æé å¨ä¼èªå¨å¸¦ä¸ {@code deleted = 0}ï¼ * è {@code uk_report_no} å¯ä¸ç´¢å¼ä¸è®¤ {@code deleted}ãå æå½å¤©æå¤§å·çé£ä»½æ¥ååï¼ * æ¡ä»¶æé å¨çä¸å°å®ãç®ååä¸ä¸ªå·ãæå ¥å¿ ç¶æå¯ä¸ç´¢å¼ï¼éè¯åæ¯åä¸å¥æ¥è¯¢åä¸ä¸ªç»æï¼ * è¿æ 5 æ¬¡åæ¥ãç¼å·å²çªãââç¨æ·ä¼ä»¥ä¸ºæ¯èªå·±ææ°å·®ï¼å®é æ¯å è¿ä¸ä»½æ¥åå°±ååºä¸äºè¿ä¸å¤©çä»¶ã * <p> * æåº**ä¸è½**åªå {@code ORDER BY report_no DESC}ï¼åºå·è¡¥é¶å° 4 ä½ï¼ * å½å¤©ä¸æ¦è¶ è¿ 9999 æ¡ä½å®½å°±ä¼åé¿ï¼åå ¸åºä¼æ {@code ...-9999} æå° {@code ...-10000} åé¢ã * å æ¯é¿åº¦åæ¯åå ¸åºï¼ä¸¤ç«¯å¯¹é½ææ¯ççãæå¤§ãã * <p> * åªåç¼å·æ¬èº«ï¼è°ç¨æ¹è¦çå°±æ¯å®ç尾鍿µæ°´ï¼ä¸å¿ æ JSON å¿«ç §ä¸æ´ä¸ªåæ½åºæ¥ã */ @Select("SELECT report_no FROM qc_report_instance WHERE report_no LIKE CONCAT(#{prefix}, '%') " + "ORDER BY CHAR_LENGTH(report_no) DESC, report_no DESC LIMIT 1") String selectLatestReportNoByPrefix(@Param("prefix") String prefix); } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/dal/mysql/render/QcReportRenderRecordMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,14 @@ package cn.iocoder.yudao.module.qcreport.dal.mysql.render; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.module.qcreport.dal.dataobject.render.QcReportRenderRecordDO; import org.apache.ibatis.annotations.Mapper; /** * æºè½è´¨æ£æ¥å渲æè®°å½ Mapper * <p> * åªåä¸è¯»ï¼ææ¥æ¶ç´æ¥ç¨ SQL æ¥åºï¼ä¸ä¸ºæ¤åæ¥å£ã */ @Mapper public interface QcReportRenderRecordMapper extends BaseMapperX<QcReportRenderRecordDO> { } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/dal/mysql/template/QcReportTemplateMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,43 @@ package cn.iocoder.yudao.module.qcreport.dal.mysql.template; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.qcreport.controller.admin.template.vo.QcReportTemplatePageReqVO; import cn.iocoder.yudao.module.qcreport.dal.dataobject.template.QcReportTemplateDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface QcReportTemplateMapper extends BaseMapperX<QcReportTemplateDO> { default PageResult<QcReportTemplateDO> selectPage(QcReportTemplatePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX<QcReportTemplateDO>() .likeIfPresent(QcReportTemplateDO::getTemplateCode, reqVO.getTemplateCode()) .likeIfPresent(QcReportTemplateDO::getTemplateName, reqVO.getTemplateName()) .eqIfPresent(QcReportTemplateDO::getIndustry, reqVO.getIndustry()) .eqIfPresent(QcReportTemplateDO::getReportType, reqVO.getReportType()) .eqIfPresent(QcReportTemplateDO::getStatus, reqVO.getStatus()) .eqIfPresent(QcReportTemplateDO::getCurrentVersion, reqVO.getCurrentVersion()) .eqIfPresent(QcReportTemplateDO::getCreator, reqVO.getCreator()) .betweenIfPresent(QcReportTemplateDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(QcReportTemplateDO::getId)); } default List<QcReportTemplateDO> selectListByReportTypeAndStatus(String reportType, Integer status) { return selectList(new LambdaQueryWrapperX<QcReportTemplateDO>() .eq(QcReportTemplateDO::getReportType, reportType) .eq(QcReportTemplateDO::getStatus, status) .orderByDesc(QcReportTemplateDO::getId)); } default QcReportTemplateDO selectByCode(String templateCode) { return selectOne(QcReportTemplateDO::getTemplateCode, templateCode); } default QcReportTemplateDO selectByName(String templateName) { return selectOne(QcReportTemplateDO::getTemplateName, templateName); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/dal/mysql/version/QcReportTemplateVersionMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,55 @@ package cn.iocoder.yudao.module.qcreport.dal.mysql.version; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.qcreport.controller.admin.version.vo.QcReportTemplateVersionPageReqVO; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.QcReportTemplateVersionDO; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; @Mapper public interface QcReportTemplateVersionMapper extends BaseMapperX<QcReportTemplateVersionDO> { default PageResult<QcReportTemplateVersionDO> selectPage(QcReportTemplateVersionPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX<QcReportTemplateVersionDO>() .eqIfPresent(QcReportTemplateVersionDO::getTemplateId, reqVO.getTemplateId()) .likeIfPresent(QcReportTemplateVersionDO::getVersion, reqVO.getVersion()) .eqIfPresent(QcReportTemplateVersionDO::getStatus, reqVO.getStatus()) .betweenIfPresent(QcReportTemplateVersionDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(QcReportTemplateVersionDO::getId)); } default List<QcReportTemplateVersionDO> selectListByTemplateId(Long templateId) { return selectList(new LambdaQueryWrapperX<QcReportTemplateVersionDO>() .eq(QcReportTemplateVersionDO::getTemplateId, templateId) .orderByDesc(QcReportTemplateVersionDO::getId)); } default List<QcReportTemplateVersionDO> selectListByTemplateIdsAndStatus(Collection<Long> templateIds, Integer status) { return selectList(new LambdaQueryWrapperX<QcReportTemplateVersionDO>() .in(QcReportTemplateVersionDO::getTemplateId, templateIds) .eq(QcReportTemplateVersionDO::getStatus, status)); } default QcReportTemplateVersionDO selectByTemplateIdAndVersion(Long templateId, String version) { return selectOne(new LambdaQueryWrapperX<QcReportTemplateVersionDO>() .eq(QcReportTemplateVersionDO::getTemplateId, templateId) .eq(QcReportTemplateVersionDO::getVersion, version)); } /** * æ¥è¯¢æ¨¡æ¿çææ°ä¸ä¸ªçæ¬ï¼æ id ååºåç¬¬ä¸æ¡ï¼ */ default QcReportTemplateVersionDO selectLatestByTemplateId(Long templateId) { List<QcReportTemplateVersionDO> list = selectList(new LambdaQueryWrapperX<QcReportTemplateVersionDO>() .eq(QcReportTemplateVersionDO::getTemplateId, templateId) .orderByDesc(QcReportTemplateVersionDO::getId) .last("LIMIT 1")); return list.isEmpty() ? null : list.get(0); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/Bindings.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,88 @@ package cn.iocoder.yudao.module.qcreport.engine; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * æ°æ®ç»å®ï¼æ¨¡æ¿ææ¬éç {@code {{path}}}ã * <p> * åªååå¼ä¸æ¿æ¢ï¼ä¸åä»»ä½è¡¨è¾¾å¼æ±å¼ï¼éè¦è®¡ç®çåºæ¯äº¤ç»è§å弿ã * åä¸å°çè·¯å¾æ¸²æä¸ºç©ºå符串ââæ¥åä¸ä¸è¯¥åºç°ç¨æ·ç䏿ç {@code {{}}}ã * <p> * ä¸å端 {@code engine/binding.ts} ä¸ä¸å¯¹åºï¼ä¸¤è¾¹å¿ é¡»åè¯ä¹ã */ public final class Bindings { /** ç»å®è¡¨è¾¾å¼ï¼{{ report.reportNo }} */ private static final Pattern BINDING_PATTERN = Pattern.compile("\\{\\{\\s*([^{}]+?)\\s*\\}\\}"); /** æ´æ®µå°±æ¯ä¸ä¸ªç»å®è¡¨è¾¾å¼ */ private static final Pattern SINGLE_BINDING_PATTERN = Pattern.compile("^\\{\\{\\s*([^{}]+?)\\s*\\}\\}$"); private Bindings() { } /** æåææ¬éåºç°çææç»å®è·¯å¾ï¼å»éãä¿æåºç°é¡ºåºï¼ï¼ç¨äºæ¸²æåçæ°æ®ä¾èµæ£æ¥ */ public static List<String> listPaths(String template) { Set<String> paths = new LinkedHashSet<>(); if (template == null) { return List.of(); } Matcher matcher = BINDING_PATTERN.matcher(template); while (matcher.find()) { String path = matcher.group(1).trim(); if (!path.isEmpty()) { paths.add(path); } } return List.copyOf(paths); } /** è§£æå个ç»å®è·¯å¾ï¼åä¸å°è¿å null */ public static Object resolve(String path, Object context) { return path == null ? null : Paths.readPath(context, path.trim()); } /** * æ¿æ¢ææ¬éçå ¨é¨ç»å®è¡¨è¾¾å¼ï¼å¹¶åæ¥åä¸å°å¼çè·¯å¾ã * <p> * ç»å®ä¸ä¸é常æå³çæ°æ®ç¼ºå£ï¼é黿¸²ææç©ºä¼è®©æ¥åçèµ·æ¥ãæ£å¸¸ä½ç¼ºå 容ãï¼ * æä»¥æç¼ºå£äº¤ç»è°ç¨æ¹ï¼ç±æ¸²æå¼ææ±æ»æå¯è§çé®é¢æ¸ åã */ public static String resolveText(String template, Object context, Consumer<String> onMissing) { if (template == null) { return ""; } Matcher matcher = BINDING_PATTERN.matcher(template); StringBuilder result = new StringBuilder(); while (matcher.find()) { String path = matcher.group(1).trim(); Object value = Paths.readPath(context, path); if (value == null && onMissing != null) { onMissing.accept(path); } matcher.appendReplacement(result, Matcher.quoteReplacement(Paths.formatValue(value))); } matcher.appendTail(result); return result.toString(); } /** æ¿æ¢ææ¬éçå ¨é¨ç»å®è¡¨è¾¾å¼ï¼æªç»å®çè·¯å¾æ¸²æä¸ºç©º */ public static String resolveText(String template, Object context) { return resolveText(template, context, null); } /** 夿䏿®µææ¬æ¯å¦æ¯çº¯ç»å®è¡¨è¾¾å¼ï¼{{path}} åå 䏿®µï¼ï¼æ¯åè¿åè·¯å¾ï¼å¦åè¿å null */ public static String asSingleBinding(String template) { if (template == null) { return null; } Matcher matcher = SINGLE_BINDING_PATTERN.matcher(template.trim()); return matcher.matches() ? matcher.group(1).trim() : null; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/JsValues.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,43 @@ package cn.iocoder.yudao.module.qcreport.engine; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; /** * JS å¼è¯ä¹ã * <p> * 渲æä¸è§åæ±å¼ç产ç©è¦è·å端éåä¸è´ï¼è Java çé»è®¤ç±»å转æ¢å JS æå å¤å¯¹ä¸ä¸ * ï¼{@code String.valueOf(60.0D)} æ¯ "60.0"ã{@code String.valueOf(true)} æ¯ "true"ã * æ°ç» toString æ¯ "[1, 2]"ï¼ã塿¯æ¿å¼å½ææ¬ç¨çå°æ¹é½èµ°è¿éï¼å«ç¨ Java çé»è®¤è½¬æ¢ã */ public final class JsValues { private JsValues() { } /** {@code String(value)}ï¼null å¾ç©ºä¸²ãå¸å°å¾ true/falseãæ°ç»æéå·è¿æ¥ */ public static String asText(Object value) { if (value == null) { return ""; } if (value instanceof Boolean bool) { return bool ? "true" : "false"; } if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) { return Numbers.toString(((Number) value).doubleValue()); } if (value instanceof Integer || value instanceof Long) { return String.valueOf(value); } if (value instanceof List<?> list) { List<String> parts = new ArrayList<>(list.size()); for (Object element : list) { parts.add(element == null ? "" : asText(element)); } return String.join(",", parts); } return String.valueOf(value); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/Numbers.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,26 @@ package cn.iocoder.yudao.module.qcreport.engine; /** * æ°å¼è¾åºæ ¼å¼ã * <p> * Java ç {@code String.valueOf(210.0D)} å¾å° "210.0"ï¼è JS ç {@code String(210)} æ¯ "210"ã * 渲æäº§ç©ï¼{@code @page} 尺寸ãç»å®å¼ãæ¯è¾æ¶çææ¬å½¢æï¼å¿ é¡»ä¸å端éåä¸è´ï¼ * å¦ååä¸ä»½æ¨¡æ¿å¨ä¸¤ç«¯ä¼è¾åºä¸åç HTMLã */ public final class Numbers { /** å精度è½ç²¾ç¡®è¡¨ç¤ºææ´æ°çä¸çï¼è¶ è¿å®å°±ä¸å彿´æ°å¤ç */ private static final double MAX_SAFE_INTEGER = 9.007199254740992E15; private Numbers() { } /** æ´æ°å¼çæµ®ç¹æ°æ¶ææ´æ°è¾åºï¼60.0 â "60"ï¼ï¼å ¶ä½åæ ·è¾åº */ public static String toString(double value) { if (Double.isFinite(value) && value == Math.rint(value) && Math.abs(value) <= MAX_SAFE_INTEGER) { return String.valueOf((long) value); } return String.valueOf(value); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/PageMargin.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,16 @@ package cn.iocoder.yudao.module.qcreport.engine; /** * 页边è·ï¼æ¯«ç±³ï¼ï¼åä½ä¸å端 {@code engine/page.ts} ä¸è´ã * <p> * åæ®µç¨å è£ ç±»åï¼ä¸º null 表示该项没é è¿ï¼ç± {@link PageSizes} è½å°é»è®¤è¾¹è·ï¼ * ä¸å端 {@code margin?.top ?? DEFAULT_MARGIN_MM} çè¯ä¹ä¿æä¸è´ã */ public record PageMargin(Double top, Double right, Double bottom, Double left) { /** å ¨é¨æªé ç½® */ public static PageMargin none() { return new PageMargin(null, null, null, null); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/PageSetting.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,15 @@ package cn.iocoder.yudao.module.qcreport.engine; /** * çº¸å¼ é ç½®ï¼ç»æçä»·äº Schema.pageï¼ä½ä¸ä¾èµå ·ä½æ¥å£ç±»åå®ä¹ã * <p> * å端 {@code engine/page.ts} ç PageSetting ä¸ä¹ä¸ä¸å¯¹åºã */ public record PageSetting(String size, String orientation, PageMargin margin) { /** æªé 置任ä½çº¸å¼ ä¿¡æ¯æ¶ä½¿ç¨ */ public static PageSetting defaultSetting() { return new PageSetting(null, null, null); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/PageSizes.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,48 @@ package cn.iocoder.yudao.module.qcreport.engine; import java.util.Map; /** * çº¸å¼ å°ºå¯¸æ¢ç®ã * <p> * ä¸å端 {@code engine/page.ts} ä¸ä¸å¯¹åºï¼åå¼å {@code QcReportEnums.PageSizeEnum}ã * è®¾è®¡å¨æ¢ç®ç»å¸åç´ ãæ¸²æå¼ææ¢ç® {@code @page} é½ç¨è¿ä¸ä»½ï¼é¿å 两å¤ååä¸éå对ä¸ä¸ã */ public final class PageSizes { /** çº¸å¼ å°ºå¯¸ï¼æ¯«ç±³ï¼ï¼æ°ç»ä¸º {宽, é«} */ private static final Map<String, double[]> SIZE_MM = Map.of( "A3", new double[]{297, 420}, "A4", new double[]{210, 297}, "A5", new double[]{148, 210}, "Letter", new double[]{215.9, 279.4}); /** é»è®¤çº¸å¼ ï¼æªååºç尺寸åéå°å® */ public static final String DEFAULT_SIZE = "A4"; /** é»è®¤é¡µè¾¹è·ï¼æ¯«ç±³ï¼ */ public static final double DEFAULT_MARGIN_MM = 10; private PageSizes() { } /** è§£æçº¸å¼ 尺寸ä¸é¡µè¾¹è·ï¼æ¯«ç±³ï¼ï¼æ¨ªåæ¶ç¿»è½¬å®½é« */ public static ResolvedPage resolve(PageSetting page) { String size = page == null || page.size() == null ? DEFAULT_SIZE : page.size(); double[] base = SIZE_MM.getOrDefault(size, SIZE_MM.get(DEFAULT_SIZE)); boolean landscape = page != null && "landscape".equals(page.orientation()); PageMargin margin = page == null ? null : page.margin(); return new ResolvedPage( landscape ? base[1] : base[0], landscape ? base[0] : base[1], marginOf(margin == null ? null : margin.top()), marginOf(margin == null ? null : margin.right()), marginOf(margin == null ? null : margin.bottom()), marginOf(margin == null ? null : margin.left())); } private static double marginOf(Double value) { return value == null ? DEFAULT_MARGIN_MM : value; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/Paths.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,199 @@ package cn.iocoder.yudao.module.qcreport.engine; import java.math.BigDecimal; import java.time.temporal.TemporalAccessor; import java.time.format.DateTimeFormatter; import java.time.ZoneId; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * ä¸ä¸æåå¼ã * <p> * åªåç½ååå¼ç屿§å¯¼èªï¼ä¸è§£æã䏿§è¡ä»»ä½ä»£ç ï¼ * æ¯æ a.b.c ä¸ a.b[0].c 两ç§åæ³ï¼è·¯å¾å¨æ°ç»ä¸ç»§ç»å屿§æ¶æå ç´ é个åå¼ï¼pluckï¼ã * <p> * ä¸å端 {@code src/components/quality/engine/path.ts} ä¸ä¸å¯¹åºï¼ä¸¤è¾¹å¿ é¡»åè¯ä¹ã */ public final class Paths { /** è·¯å¾çæ®µï¼æ®é屿§åææ°ç»ä¸æ */ private static final Pattern TOKEN_PATTERN = Pattern.compile("[^.\\[\\]]+"); /** ç¦æ¢è®¿é®ç屿§åï¼é¿å 顺çååé¾è¯»å°æé å¨ */ private static final Set<String> BLOCKED_KEYS = Set.of("__proto__", "constructor", "prototype"); private static final Pattern INTEGER_TEXT = Pattern.compile("^\\d+$"); private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private Paths() { } /** * æè·¯å¾ææç段ï¼inspectionItems[0].actualValue â [inspectionItems, 0, actualValue] */ public static List<String> parsePath(String path) { List<String> tokens = new ArrayList<>(); if (path == null) { return tokens; } Matcher matcher = TOKEN_PATTERN.matcher(path); while (matcher.find()) { tokens.add(matcher.group()); } return tokens; } /** * æè·¯å¾åå¼ï¼åä¸å°è¿å nullã */ public static Object readPath(Object source, String path) { return readTokens(source, parsePath(path), 0); } private static Object readTokens(Object source, List<String> tokens, int start) { Object current = source; for (int index = start; index < tokens.size(); index++) { String token = tokens.get(index); if (current == null) { return null; } if (current instanceof List<?> list) { Integer arrayIndex = toIndex(token); if (arrayIndex == null) { // æ°ç»ä¸ç»§ç»å屿§ï¼éå ç´ åå¼ï¼å¾å°åé¿åº¦çæ°ç» List<Object> plucked = new ArrayList<>(list.size()); for (Object element : list) { plucked.add(readTokens(element, tokens, index)); } return plucked; } current = arrayIndex < list.size() ? list.get(arrayIndex) : null; continue; } if (current instanceof Map<?, ?> map) { if (BLOCKED_KEYS.contains(token)) { return null; } current = map.get(token); continue; } // æ éåé¢è¿æè·¯å¾ç段ï¼è¯´æè·¯å¾åéäº return null; } return current; } private static Integer toIndex(String token) { return INTEGER_TEXT.matcher(token).matches() ? Integer.valueOf(token) : null; } /** * å±ç¤ºç¨æ ¼å¼åï¼ç©ºå¼æ¸²æä¸ºç©ºå符串ï¼å¸å°å¼è½¬ä¸æï¼æ¥æåªä¿çå°ç§ã * <p> * æ´æ°å¼çå精度æ°ï¼å¦ 60.0ï¼ææ´æ°è¾åºï¼ä¸ JS ç {@code String(60)} ä¿æä¸è´ï¼ * å¦åæ¥åä¸ä¼åºç°ãæ åå¼ 60.0ãè¿ç§ä¸è¯¥æçå°¾æ°ã */ public static String formatValue(Object value) { if (value == null) { return ""; } if (value instanceof Boolean bool) { return bool ? "æ¯" : "å¦"; } if (value instanceof Date date) { return DATE_TIME_FORMAT.format(date.toInstant().atZone(ZoneId.systemDefault())); } if (value instanceof TemporalAccessor temporal) { return DATE_TIME_FORMAT.format(temporal); } if (value instanceof Collection<?> collection) { List<String> parts = new ArrayList<>(collection.size()); for (Object element : collection) { parts.add(formatValue(element)); } return String.join("ã", parts); } return formatNumberAware(value); } private static String formatNumberAware(Object value) { if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) { return Numbers.toString(((Number) value).doubleValue()); } return String.valueOf(value); } /** * æ°å¼åï¼éæ°å¼è¿å NaNï¼ç±è°ç¨æ¹å³å®å¦ä½å¤çã */ public static double toNumber(Object value) { if (value instanceof Number number) { return number.doubleValue(); } if (value instanceof Boolean bool) { return bool ? 1D : 0D; } if (value instanceof String text && !text.isBlank()) { try { return Double.parseDouble(text.trim()); } catch (NumberFormatException ignored) { return Double.NaN; } } return Double.NaN; } /** * æä»»æå弿¶æææ°å¼æ°ç»ï¼ä¾ç»è®¡å½æ°ä½¿ç¨ã */ public static List<Double> toNumberArray(Object value) { List<Double> numbers = new ArrayList<>(); if (value instanceof Collection<?> collection) { flattenInto(collection, numbers); return numbers; } double single = toNumber(value); if (Double.isFinite(single)) { numbers.add(single); } return numbers; } private static void flattenInto(Object value, List<Double> target) { if (value instanceof Collection<?> collection) { for (Object element : collection) { flattenInto(element, target); } return; } double numeric = toNumber(value); if (Double.isFinite(numeric)) { target.add(numeric); } } /** * æä»»æå弿ä¸å±ä¸å±æå¹³æå表ï¼ä¿çåå§å ç´ ï¼ä¸åæ°å¼è½¬æ¢ï¼ï¼ä¾ PASS_RATE è¿ç±»å½æ°ä½¿ç¨ã */ public static List<Object> flatten(Object value) { List<Object> result = new ArrayList<>(); if (value instanceof Collection<?> collection) { for (Object element : collection) { result.addAll(flatten(element)); } return result; } result.add(value); return result; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/QualityReportEngine.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,115 @@ package cn.iocoder.yudao.module.qcreport.engine; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.ReportTemplateSchema; import cn.iocoder.yudao.module.qcreport.engine.context.ReportContext; import cn.iocoder.yudao.module.qcreport.engine.render.HtmlRenderer; import cn.iocoder.yudao.module.qcreport.engine.render.RenderOutcome; import cn.iocoder.yudao.module.qcreport.engine.rule.RuleEngine; import cn.iocoder.yudao.module.qcreport.engine.rule.RuleEvaluator; import cn.iocoder.yudao.module.qcreport.engine.rule.QualityRuleDefinition; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 渲æå¼æå ¥å£ã * <p> * åªåä¸ä»¶äºï¼æè½åºçæ¨¡æ¿ Schema ç¿»è¯æå¼æçå é¨ç»æï¼ç¶å渲æã * 弿æ¬èº«ä¸ä¾èµ Springãä¸è½åºãä¸åæéââå®åªåãSchema + ä¸ä¸æãå HTMLï¼ * è¿æ ·æ¹éçæã宿¶ä»»å¡ãå ¶å®æ¨¡åè°ç¨é½è½èµ°åä¸ä¸ªå ¥å£ã */ public final class QualityReportEngine { private QualityReportEngine() { } /** æ¸²ææ¥åï¼å¤å® â çææ£æ â ç»è£ ææ¡£ */ public static RenderOutcome render(ReportTemplateSchema schema, ReportContext context) { return HtmlRenderer.render(grapesOf(schema), pageOf(schema), context, rulesOf(schema)); } /** * æ¨¡æ¿æ¯å¦å·²ç»æ<b>坿¸²æç</b>ç»å¸å 容ã * <p> * 夿®ä¸æ¸²æå¨è¯»ç»å¸çé£ä¸è¡ä¸¥æ ¼ä¸è´ââ{@link HtmlRenderer#render} åçæ¯ * {@code pages[0].frames[0].component}ï¼è¿éå°±ç¨ {@link Paths#readPath} ååä¸ä¸ªä½ç½®ï¼ * è䏿¯éåæãgrapes è¿ä¸ª Map é空ãã * <p> * <b>为ä»ä¹ä¸è½åªçé空</b>ï¼GrapesJS æãæå¼è¿ä½ä¸ä¸ªç»ä»¶é½æ²¡æ¾ãç项ç®åæ * {@code {"assets":[],"styles":[]}}ï¼å®é空ãå´æ²¡æ framesï¼æ¸²æå¨åä¸å°æ ¹ç»ä»¶æ¶ * ç´æ¥æ body 渲ææç©ºä¸²ââ䏿¥éï¼ç¨æ·æ¿å°ä¸å¼ ç½çº¸ PDFãåºä»¶åçè¿ä¸ªæ£æ¥å°±æ¯ä¸ºäº * æè¿ç§çæ¬æ¡å¨æ¸²æä¹åï¼å¤æ®å¿ 须忏²æå¨å¯¹é½ï¼å¦åæ¡ä¸ä½ã */ public static boolean hasCanvas(ReportTemplateSchema schema) { return Paths.readPath(grapesOf(schema), "pages[0].frames[0].component") instanceof Map<?, ?>; } /** GrapesJS é¡¹ç®æ°æ® */ public static Map<String, Object> grapesOf(ReportTemplateSchema schema) { return schema == null ? null : schema.getGrapes(); } /** çº¸å¼ ä¸é¡µè¾¹è·ï¼æªé ç½®æ¶è½å°é»è®¤ A4 ä¸é»è®¤è¾¹è· */ public static PageSetting pageOf(ReportTemplateSchema schema) { ReportTemplateSchema.Page page = schema == null ? null : schema.getPage(); if (page == null) { return PageSetting.defaultSetting(); } ReportTemplateSchema.Margin margin = page.getMargin(); PageMargin pageMargin = margin == null ? PageMargin.none() : new PageMargin(margin.getTop(), margin.getRight(), margin.getBottom(), margin.getLeft()); return new PageSetting(page.getSize(), page.getOrientation(), pageMargin); } /** å¤å®è§åå®ä¹ï¼Schema éæ²¡é æ¶è¿åç©ºæ¸ å */ public static List<QualityRuleDefinition> rulesOf(ReportTemplateSchema schema) { List<QualityRuleDefinition> rules = new ArrayList<>(); if (schema == null || schema.getRules() == null) { return rules; } for (Map<String, Object> raw : schema.getRules()) { QualityRuleDefinition rule = QualityRuleDefinition.from(raw); if (rule != null) { rules.add(rule); } } return rules; } /** * ä¿ååæ ¡éªæ¨¡æ¿éçå ¨é¨å¤å®è§åï¼è¿åé®é¢æ¸ åï¼ç©ºæ¸ å表示éè¿ã * <p> * é¤äºè¯æ³ï¼è¿æ¦ç½ååå¤ç彿°è°ç¨ï¼è¿ç±»è§å䏿¦åä¸å»ï¼ * æ¯å¼ æ¥åé½ä¼å¸¦ä¸ä¸æ¡ãæ æ³æ§è¡ãï¼ä¸å¦å¨è®¾è®¡é¶æ®µå°±éåå»ã * <p> * <b>空表达å¼çè§åä¹éåå»ã</b>è¿æ¡æ¾ç»æ¯æ¾è¡çââ彿¶è®¾è®¡å¨è¿æ²¡æè§åç¼è¾çé¢ï¼ * æ¥äºéç¨æ·æ å¤å¯æ¹ï¼åªä¼å¡å¨ä¿åä¸äºï¼çäºæã䏿¡åæ°æ®ãå级æãæ´ä»½æ¨¡æ¿ä¸è½ä¿åãã * è§åç¼è¾çé¢ååºæ¥ä¹åè¿ä¸ªçç±ä¸æç«äºï¼ç¨æ·å¨è®¾è®¡å¨éå æè¿æ¡è§åå°±è½ç»§ç»ä¿åï¼ * æä»¥æ¹ææç¡®æ¥éã读åä»ç¶å®½å®¹ï¼{@link #rulesOf} ä¸ä¼å ä¸ºä¸æ¡åè§å让æ´ä»½æ¨¡æ¿æä¸å¼ï¼ï¼ * æ§è¡æä¹ç §å¸¸æå®æ¥åºæ¥ï¼ã第 N 项ãXãçè§åãYãæ æ³æ§è¡ï¼è§å表达å¼ä¸ºç©ºãï¼ã */ public static List<String> validateRules(ReportTemplateSchema schema) { List<String> errors = new ArrayList<>(); for (QualityRuleDefinition rule : rulesOf(schema)) { errors.addAll(RuleEngine.validate(rule)); String expression = rule.expression(); if (expression == null || expression.trim().isEmpty()) { // 空表达å¼ä¸ä¸æ¥å·²ç»æ¥è¿ï¼ä¹æ²¡ä»ä¹å¯éææ£æ¥ç continue; } try { String unknown = RuleEvaluator.findUnknownFunction(RuleEngine.parse(expression)); if (unknown != null) { errors.add("è§åã" + rule.label() + "ãè°ç¨äºä¸æ¯æç彿° " + unknown + "ï¼å¯ç¨å½æ°ï¼" + String.join("ã", RuleEvaluator.functionNames())); } } catch (RuntimeException ignored) { // è¯æ³é误已ç»å¨ä¸ä¸æ¥æ¥è¿ï¼ä¸éå¤å·å± } } return errors; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/QualityResult.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,55 @@ package cn.iocoder.yudao.module.qcreport.engine; /** * å¤å®ç»æã * <p> * ä¸å端 {@code engine/context.ts} ç QUALITY_RESULT ä¸ä¸å¯¹åºï¼åå¼åæ¶æ¯ * {@code QcReportEnums.CheckResultEnum} çè½åºå¼ã */ public enum QualityResult { PASS("åæ ¼"), FAIL("ä¸åæ ¼"); /** * è§åæ¬èº«åºéæ¶çå 使æ¡ã * <p> * 没éªè¿ä¸è½è¯´åæ ¼ï¼æ²¡è¯æ®ä¹ä¸è½è¯´ä¸åæ ¼ï¼æä»¥ãæ ç»è®ºãå¿ é¡»æ¯ä¸ä¸ªå PASS/FAIL å¹¶åçæ¾å¼ç¶æã * è¿ç±»é¡¹ä»çå¨åæ ¼ç忝éï¼ä¸ä¸ªè·æçè§ååºè¯¥æåæ ¼ç»è®ºåä½ã */ public static final String PENDING_TEXT = "å¾ å¤å®"; /** * æ¢æ²¡æè§åã乿²¡æè§æ ¼ä¸ä¸éæ¶çå 使æ¡ã * <p> * å {@link #PENDING_TEXT} 廿åå¼ï¼æ¥åé常æãè¯æ ·è´¨é mããç§°éç¶ m0ãè¿ç±»åªä¾å ¬å¼åæ°çè¿ç¨åæ°ï¼ * å®ä»¬æ²¡æè§æ ¼ä¹å°±æ ä»å¤å®ï¼è¿ç±»é¡¹ä¸è®¡å ¥åæ ¼ç忝ï¼ä¹ä¸å䏿¥åç»è®ºã */ public static final String UNDECIDABLE_TEXT = "æ å¤å®è§å"; /** å¤å®ç»æå¯¹åºçå±ç¤ºæå */ private final String text; QualityResult(String text) { this.text = text; } public String text() { return text; } /** å±ç¤ºæå/è½åºå¼ â æä¸¾ï¼è¯å«ä¸åºè¿å null */ public static QualityResult of(String value) { if (value == null) { return null; } String normalized = value.trim().toUpperCase(); for (QualityResult result : values()) { if (result.name().equals(normalized)) { return result; } } return null; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/ResolvedPage.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,21 @@ package cn.iocoder.yudao.module.qcreport.engine; /** * è§£æåççº¸å¼ å°ºå¯¸ä¸é¡µè¾¹è·ï¼æ¯«ç±³ï¼ï¼æ¨ªå已翻转宽é«ã */ public record ResolvedPage(double widthMm, double heightMm, double marginTopMm, double marginRightMm, double marginBottomMm, double marginLeftMm) { /** {@code @page { size: ... } } çå¼ */ public String cssSize() { return Numbers.toString(widthMm) + "mm " + Numbers.toString(heightMm) + "mm"; } /** {@code @page { margin: ... } } çå¼ï¼é¡ºåºä¸º ä¸ å³ ä¸ å·¦ */ public String cssMargin() { return Numbers.toString(marginTopMm) + "mm " + Numbers.toString(marginRightMm) + "mm " + Numbers.toString(marginBottomMm) + "mm " + Numbers.toString(marginLeftMm) + "mm"; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/context/InspectionItem.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,113 @@ package cn.iocoder.yudao.module.qcreport.engine.context; import cn.iocoder.yudao.module.qcreport.engine.QualityResult; import lombok.Data; import lombok.experimental.Accessors; /** * å个æ£éªé¡¹ã * <p> * åæ®µåä¸å端 {@code engine/context.ts} ç ReportContextItem ä¿æä¸è´ï¼ * éå¤è¡çè¡å ç»å®åä½ {@code {{item.itemName}}}ã */ @Data @Accessors(chain = true) public class InspectionItem { private int index; private String itemCode = ""; private String itemName = ""; private String standardValue = ""; private String actualValue = ""; private String unit = ""; /** * ãæ£æµè¦æ±ãææ¬ï¼åç»ç¶é¡¹ = ææ ä¸çå ¬å¼ï¼å ¶ä½ = åæ®ä¸çæ åè¦æ±ã * <p> * ä¸ {@link #standardValue} å¹¶åèä¸åå¹¶ï¼ä¸¤è çæ¥æºä¸åï¼åæ®è¡ vs ææ ä¸»æ°æ®ï¼ï¼ * èåç»ç¶åé¡¹åæ ¹æ²¡æè§æ ¼ï¼åªæå ¬å¼ã */ private String requirement = ""; /** * æ£æµæ¹æ³ï¼æ¥èªåæ®è¡ï¼å¦ {@code GB 5009.3-2016} / {@code ANA.MTH-000036}ï¼ã * <p> * ä¸ {@link #requirement} å¹¶åèä¸åå¹¶ï¼æ£æµè¦æ±è¯´çæ¯ãè¿ä¸é¡¹è¦è¾¾å°ä»ä¹ãï¼ * æ£æµæ¹æ³è¯´çæ¯ãç¨ä»ä¹åæ³æµçãï¼åä»¶é常åå ä¸åã */ private String checkMethod = ""; /** * åç»ä¿¡æ¯ï¼åªææ£éªå带åç»ç»ææ¶ææå¼ã * <p> * null 表示ãè¿ä»½æ¥å没æåç»ãï¼{@link ReportContext#itemMap} å¨è¿ç§æ åµä¸ä¸æ¾è¿ä¸ªé®ï¼ * å å¾ç»æ²¡æåç»çæ¥åï¼ä»¥åå»ç»ç对æ fixtureï¼å¸¦ä¸ä¸ä¸ªæä¸ºç©ºçåæ®µã */ private Group group; /** è§æ ¼ä¸éï¼å¤å®å¨ååºé´å¤å®æ¶ä½¿ç¨ */ private Double upperLimit; /** è§æ ¼ä¸é */ private Double lowerLimit; /** PASS / FAIL */ private String result = ""; /** åæ ¼ / ä¸åæ ¼ / å¾ å¤å® / æ å¤å®è§å */ private String resultText = ""; private String remark = ""; /** * åç»ä¿¡æ¯ï¼æ¥åè¦æç»åè·¨ä½æ´ç»æéçä¸ä¸ªå¼ã * <p> * 渲ææç®ä¸åºè¿äºï¼ç»å®çè·¯å¾å¨æ°ç»ä¸ç»§ç»å屿§ä¼æå ç´ pluckï¼`group.children.length` * åä¸å°å¼ï¼ï¼æä»¥ãè¿ä¸ç»æå è¡ããè¿ä¸æ ¼è¦ä¸è¦è®©ä½ãåªè½ç±ä¸æ¸¸é项ä¸åã * åæ®µå¨**æ¯ä¸ä¸ª**æ£éªé¡¹ä¸é½è¦æå¼ï¼åªææ¯ç©ºä¸²ï¼ï¼ç»å®çåæ®µç¼ºå¸æ¶å±æ§ä¼è¢«è·³è¿ï¼ * æ¬è¯¥éèçåå¹¶æ ¼åèä¼é²åºæ¥ã */ @Data @Accessors(chain = true) public static class Group { /** ãå项ãåææ¬ï¼ç»å å项 = èªå·±çååï¼ç»å¤´è¡ä¸ç¬ç«é¡¹ = 空串 */ private String childName = ""; /** ç»åæ ¼ç纵ååå¹¶è¡æ°ï¼ç»ç第ä¸è¡ = æ´ç»çè¡æ°ï¼ç»å å ¶ä½è¡ä¸ç¬ç«é¡¹ = 1 */ private int span = 1; /** * ç»åæ ¼çéèå¼ï¼ç»å 第ä¸è¡ = 空串ï¼å¯è§ï¼å¸¦åå¹¶è¡æ°ï¼ï¼å ¶ä½è¡ = `hidden`ã * <p> * å符串è䏿¯å¸å°ï¼`hidden` 屿§é ãå¨ä¸å¨ãèµ·ä½ç¨ï¼`hidden="false"` ç §æ ·éèï¼ï¼ * åªæç©ºä¸²è½è¡¨è¾¾ãä¸è¾åºè¿ä¸ªå±æ§ = è¿ä¸æ ¼å¯è§ãã */ private String hidden = ""; public Group copy() { return new Group().setChildName(childName).setSpan(span).setHidden(hidden); } } public InspectionItem copy() { return new InspectionItem() .setIndex(index).setItemCode(itemCode).setItemName(itemName) .setStandardValue(standardValue).setActualValue(actualValue).setUnit(unit) .setRequirement(requirement) .setCheckMethod(checkMethod) .setGroup(group == null ? null : group.copy()) .setUpperLimit(upperLimit).setLowerLimit(lowerLimit) .setResult(result).setResultText(resultText).setRemark(remark); } /** æå¤å®ç»æåå result / resultTextï¼verdict 为 null æ¶ç½®ä¸ºå¾ å¤å® */ public InspectionItem applyResult(QualityResult verdict) { this.result = verdict == null ? "" : verdict.name(); this.resultText = verdict == null ? QualityResult.PENDING_TEXT : verdict.text(); return this; } /** * 没æå¤å®ä¾æ®ï¼ç½®ä¸ºãæ å¤å®è§åãã * <p> * æ¥åé常æãè¯æ ·è´¨é mããç§°éç¶ m0ãè¿ç±»åªä¾å ¬å¼åæ°çè¿ç¨åæ°ï¼å®ä»¬æ²¡æè§æ ¼ä¹å°±æ ä»å¤å®ï¼ * è¥ç §å¸¸ç®è¿åæ¯ï¼ä¸ä»½æ¬æ¥å ¨åæ ¼çæ¥åä¼å 为å 个è¿ç¨åæ°è¢«åæãå¾ å¤å®ãã */ public InspectionItem applyUndecidable() { this.result = ""; this.resultText = QualityResult.UNDECIDABLE_TEXT; return this; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/context/ReportContext.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,120 @@ package cn.iocoder.yudao.module.qcreport.engine.context; import lombok.Data; import lombok.experimental.Accessors; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * æ¥åä¸ä¸æã * <p> * 模æ¿éç {@code {{path}}} å ¨é¨ç¸å¯¹è¿ä¸ªç»æè§£æï¼report æ¯æ¥åçº§åæ®µï¼inspectionItems æ¯æ£éªé¡¹æ°ç»ã * ä¸å¡åæ® â ä¸ä¸æçæ å°ç±è°ç¨æ¹è´è´£ï¼æ¨¡æ¿æ¬èº«åªè®¤è¿ä¸ªæ åç»æï¼ * æ¢è¡ä¸ãæ¢åæ®æ¥æºé½ä¸éè¦æ¹æ¨¡æ¿ä¸ç»ä»¶ã * <p> * å弿¶å æå¹³æ Mapï¼{@link #toScope()}ï¼ï¼ä¸é åå°è¯» POJO 屿§ï¼ * åªæ´é²è¿éæ¾å¼æ¾è¿å»çåæ®µï¼æ¨¡æ¿è·¯å¾ä¸å¯è½é¡ºç getClass ä¹ç±»è¯»å°ä¸è¯¥ç»çä¸è¥¿ã */ @Data @Accessors(chain = true) public class ReportContext { private ReportFields report = new ReportFields(); private List<InspectionItem> inspectionItems = new ArrayList<>(); /** 空ç½ä¸ä¸æï¼å段é½å ¨ä½æ²¡æå¼ï¼ä¾¿äºé项填å */ public static ReportContext empty() { return new ReportContext(); } /** æ¥åçº§åæ®µ â Map */ public Map<String, Object> reportMap() { Map<String, Object> map = new LinkedHashMap<>(); map.put("reportNo", report.getReportNo()); map.put("reportName", report.getReportName()); map.put("sampleNo", report.getSampleNo()); map.put("productCode", report.getProductCode()); map.put("productName", report.getProductName()); map.put("spec", report.getSpec()); map.put("batchNo", report.getBatchNo()); map.put("workOrderNo", report.getWorkOrderNo()); map.put("inspectType", report.getInspectType()); map.put("inspector", report.getInspector()); map.put("inspectDate", report.getInspectDate()); map.put("department", report.getDepartment()); map.put("customerName", report.getCustomerName()); map.put("supplierName", report.getSupplierName()); map.put("result", report.getResult()); map.put("resultText", report.getResultText()); map.put("conclusion", report.getConclusion()); map.put("passRate", report.getPassRate()); map.put("qcResult", report.getQcResult()); map.put("qcResultText", report.getQcResultText()); map.put("total", report.getTotal()); map.put("passCount", report.getPassCount()); map.put("failCount", report.getFailCount()); return map; } /** æ£éªé¡¹å表 â Mapï¼æ¸²æä¸è§åæ±å¼é½ä»è¿éåå¼ */ public static List<Object> itemMaps(List<InspectionItem> items) { List<Object> maps = new ArrayList<>(items.size()); for (InspectionItem item : items) { maps.add(itemMap(item)); } return maps; } /** 渲æä½ç¨åçæ ¹ï¼{ report, inspectionItems } */ public Map<String, Object> toScope() { return scope(inspectionItems); } /** * æå®æ£éªé¡¹å表çä½ç¨åï¼{ report, inspectionItems }ã * <p> * é项å¤å®è¦ç¨åå§åè¡¨ãæ¥å级å¤å®è¦ç¨å¤å®åçå表ï¼ä¸¤è ä¸è½å ±ç¨ä¸ä»½ï¼ * æä»¥è¿éæãç¨åªä»½å表ãåææ¾å¼åæ°ï¼é¿å åéã */ public Map<String, Object> scope(List<InspectionItem> items) { Map<String, Object> scope = new LinkedHashMap<>(); scope.put("report", reportMap()); scope.put("inspectionItems", itemMaps(items)); return scope; } public static Map<String, Object> itemMap(InspectionItem item) { Map<String, Object> map = new LinkedHashMap<>(); map.put("index", item.getIndex()); map.put("itemCode", item.getItemCode()); map.put("itemName", item.getItemName()); map.put("standardValue", item.getStandardValue()); map.put("actualValue", item.getActualValue()); map.put("unit", item.getUnit()); map.put("requirement", item.getRequirement()); map.put("checkMethod", item.getCheckMethod()); // 没æåç»çæ¥å䏿¾è¿ä¸ªé®ï¼æ¨¡æ¿éä¸ä¸ª {{item.group.x}} 齿²¡æï¼æ¾ä¸ªç©ºçç©ºæ ¼ååªä¼ç½å ä½ç¨å if (item.getGroup() != null) { map.put("group", groupMap(item.getGroup())); } map.put("upperLimit", item.getUpperLimit()); map.put("lowerLimit", item.getLowerLimit()); map.put("result", item.getResult()); map.put("resultText", item.getResultText()); map.put("remark", item.getRemark()); return map; } /** åç»ä¿¡æ¯ â Mapï¼ä¸ä¸ªå段ä¸ä¸ªé½ä¸è½å°ï¼å¦å模æ¿é对åºçç»å®ä¼åä¸å°å¼ */ public static Map<String, Object> groupMap(InspectionItem.Group group) { Map<String, Object> map = new LinkedHashMap<>(); map.put("childName", group.getChildName()); map.put("span", group.getSpan()); map.put("hidden", group.getHidden()); return map; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/context/ReportContextCodec.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,137 @@ package cn.iocoder.yudao.module.qcreport.engine.context; import cn.iocoder.yudao.module.qcreport.engine.JsValues; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * æ¥åä¸ä¸æä¸ãæ°æ®å¿«ç §ãä¹é´çç¼è§£ç ã * <p> * æ¥åå®ä¾è½åºæ¶è¦å»ç»ä¸ä»½ {@code data_snapshot}ï¼ä¿è¯å岿¥åä¸å ä¸å¡æ°æ®ååèæ¹åã * å¿«ç §å°±æ¯ {@link ReportContext#toScope()} é£ä»½ç»æï¼{@code {report, inspectionItems}}ï¼ï¼ * è½ç´æ¥äº¤ç» JSON åçç±»åå¤çå¨ï¼åè¿æ¥è¦éæ°çææ¶ï¼å¾æå®è¯»å {@link ReportContext}ã * <p> * <b>为ä»ä¹æå fromMap èä¸ç¨ Jackson ååºååï¼</b>䏿¯ä¸å¼æå ¶ä½é¨å飿 ¼ä¸è´ * ï¼{@link ReportContext#reportMap()}ã{@link ReportContext#itemMap} 齿¯æ¾å¼åæ®µæ¸ åï¼ * å åæ®µæ¶æ¹ä¸å¤å°±è½çåºæ¥ï¼ï¼äºæ¯è¿ä¸ªæ¨¡åé Jackson 3ï¼JSON åç±»åå¤çå¨ï¼ä¸ Jackson 2 * ï¼Spring 请æ±ä½ï¼å¹¶åï¼é åå°æ å°çäºæãååªå¥é ç½®ãåæä¸ä»¶ç¢°è¿æ°çäºã * è¿éæ¯å岿¥åè½å¦å¤ç°çå¯ä¸è·¯å¾ï¼å¼å¾åæ»ã */ public final class ReportContextCodec { private static final String REPORT = "report"; private static final String INSPECTION_ITEMS = "inspectionItems"; private ReportContextCodec() { } /** * ä¸ä¸æ â å¿«ç §ã * <p> * ä¼ è¿æ¥çéå¸¸å·²ç»æ¯å¤å®åçä¸ä¸æï¼{@code RenderOutcome.context()}ï¼ï¼ * å»ç»çåºå½æ¯ã产åºè¿ä»½ HTML çé£ä»½æ°æ®ãï¼å å«ç®å¥½ç PASS/FAIL ä¸åæ ¼çã */ public static Map<String, Object> toMap(ReportContext context) { return context == null ? ReportContext.empty().toScope() : context.toScope(); } /** å¿«ç § â ä¸ä¸æãç¼ºåæ®µãç±»åä¸å¯¹é½æãè¿ä¸ªå段没æå¼ãå¤çï¼ä¸æå¼å¸¸ */ public static ReportContext fromMap(Map<String, Object> snapshot) { ReportContext context = ReportContext.empty(); if (snapshot == null) { return context; } if (snapshot.get(REPORT) instanceof Map<?, ?> report) { context.setReport(reportOf(report)); } if (snapshot.get(INSPECTION_ITEMS) instanceof List<?> items) { context.setInspectionItems(itemsOf(items)); } return context; } private static ReportFields reportOf(Map<?, ?> report) { return new ReportFields() .setReportNo(text(report.get("reportNo"))) .setReportName(text(report.get("reportName"))) .setSampleNo(text(report.get("sampleNo"))) .setProductCode(text(report.get("productCode"))) .setProductName(text(report.get("productName"))) .setSpec(text(report.get("spec"))) .setBatchNo(text(report.get("batchNo"))) .setWorkOrderNo(text(report.get("workOrderNo"))) .setInspectType(text(report.get("inspectType"))) .setInspector(text(report.get("inspector"))) .setInspectDate(text(report.get("inspectDate"))) .setDepartment(text(report.get("department"))) .setCustomerName(text(report.get("customerName"))) .setSupplierName(text(report.get("supplierName"))) .setResult(text(report.get("result"))) .setResultText(text(report.get("resultText"))) .setConclusion(text(report.get("conclusion"))) .setPassRate(text(report.get("passRate"))) .setQcResult(text(report.get("qcResult"))) .setQcResultText(text(report.get("qcResultText"))) .setTotal(integer(report.get("total"))) .setPassCount(integer(report.get("passCount"))) .setFailCount(integer(report.get("failCount"))); } private static List<InspectionItem> itemsOf(List<?> items) { List<InspectionItem> result = new ArrayList<>(items.size()); for (Object element : items) { if (element instanceof Map<?, ?> item) { result.add(itemOf(item)); } } return result; } private static InspectionItem itemOf(Map<?, ?> item) { return new InspectionItem() .setIndex(integer(item.get("index"))) .setItemCode(text(item.get("itemCode"))) .setItemName(text(item.get("itemName"))) .setStandardValue(text(item.get("standardValue"))) .setActualValue(text(item.get("actualValue"))) .setUnit(text(item.get("unit"))) .setRequirement(text(item.get("requirement"))) .setCheckMethod(text(item.get("checkMethod"))) .setGroup(item.get("group") instanceof Map<?, ?> group ? groupOf(group) : null) .setUpperLimit(decimal(item.get("upperLimit"))) .setLowerLimit(decimal(item.get("lowerLimit"))) .setResult(text(item.get("result"))) .setResultText(text(item.get("resultText"))) .setRemark(text(item.get("remark"))); } /** åç»ä¿¡æ¯å读ï¼ä¸¢äºå®ï¼å岿¥åéåºæ¶ç»å就跨ä¸ä½æ´ç»äº */ private static InspectionItem.Group groupOf(Map<?, ?> group) { return new InspectionItem.Group() .setChildName(text(group.get("childName"))) .setSpan(span(group.get("span"))) .setHidden(text(group.get("hidden"))); } /** ææ¬åæ®µï¼null å¾ç©ºä¸²ãä¸ {@link ReportFields} çé»è®¤å¼è¯ä¹ä¸è´ââ空串æ¯ã没å¼ãï¼ä¸æ¯ãç¼ºåæ®µã */ private static String text(Object value) { return JsValues.asText(value); } private static int integer(Object value) { return value instanceof Number number ? number.intValue() : 0; } /** åå¹¶è¡æ°åä¸å°å¼æ¶æ 1 ç®ï¼0 伿¸²ææ rowspan="0"ï¼ææ´è¡çå坹齿å */ private static int span(Object value) { return value instanceof Number number ? number.intValue() : 1; } /** ä¸ä¸éå¯ä»¥ä¸ºç©ºï¼æ²¡æè§æ ¼åºé´ï¼ï¼ç©ºå¼è¦åæ ·ä¿çæ nullï¼ä¸è½åæ 0 */ private static Double decimal(Object value) { return value instanceof Number number ? number.doubleValue() : null; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/context/ReportFields.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,85 @@ package cn.iocoder.yudao.module.qcreport.engine.context; import cn.iocoder.yudao.module.qcreport.engine.QualityResult; import lombok.Data; import lombok.experimental.Accessors; /** * æ¥åçº§åæ®µã * <p> * 模æ¿éç {@code {{report.xxx}}} å ¨é¨ç¸å¯¹å®åå¼ï¼å段åä¸å端 * {@code engine/context.ts} ç ReportContextReport ä¿æä¸è´ã * <p> * å ¶ä¸ç result / resultText / conclusion / passRate / total / passCount / failCount * ç±å¤å®å¨ç®åºåååï¼è°ç¨æ¹ä¸å¿ èªå·±å¡«ã * <p> * ææ¬åæ®µé»è®¤ç©ºä¸²è䏿¯ nullï¼æ¨¡æ¿é {@code {{report.xxx}}} åå°ç©ºä¸²æ¯ãè¿ä¸ªå段没æå¼ãï¼ * åå° null æ¯ãè·¯å¾ä¸åå¨ãï¼åè ä¼è¢«æ¸²æå¼æå½ææ°æ®ç¼ºå£æ¥åºæ¥ã * 没填çåæ®µåºè¯¥èµ°åè ï¼å¦åæ¯å¼ æ¥åé½ä¼å·ä¸å å¹¶ä¸åå¨ç缺å£ã */ @Data @Accessors(chain = true) public class ReportFields { private String reportNo = ""; private String reportName = ""; private String sampleNo = ""; private String productCode = ""; private String productName = ""; private String spec = ""; private String batchNo = ""; private String workOrderNo = ""; private String inspectType = ""; private String inspector = ""; private String inspectDate = ""; private String department = ""; private String customerName = ""; private String supplierName = ""; /** PASS / FAIL */ private String result = ""; /** åæ ¼ / ä¸åæ ¼ / å¾ å¤å® */ private String resultText = ""; /** æ¥åç»è®ºï¼ä¸ {@link #resultText} åæºï¼æ ç»è®ºæ¶ä¸ºãå¾ å¤å®ã */ private String conclusion = ""; /** åæ ¼çï¼å½¢å¦ 96.67% */ private String passRate = ""; /** * ä¸å¡åæ®èªèº«çæ£éªå¤å®ï¼MES çåå¼ï¼åæ ¼ / ç¹é / ä¸åæ ¼éè´§ / ä¸åæ ¼æ¥åºï¼ï¼éå¤å®å¼æäº§åºã * <p> * ä¸ä¸é¢ç result / resultText / conclusion æ¯ä¸¤åäºï¼å»æåå¼ï¼ * å¼æåªæãåæ ¼ / ä¸åæ ¼ / å¾ å¤å®ã䏿ï¼è£ ä¸ä¸ãç¹éããä¸åæ ¼éè´§ãè¿ç±»å¤ç½®å£å¾ï¼ * èè´¨æ£åä¸çå¤å®æ¯äººå·¥æçæ¿ï¼æ¥åè¦å¦å®ç §ç»ã两è é½å¯è½åºç°å¨åä¸å¼ æ¥åä¸ã */ private String qcResult = ""; private String qcResultText = ""; private int total; private int passCount; private int failCount; /** å¤å¶ä¸ä»½ï¼å¤å®å¨ä¸ä¿®æ¹è°ç¨æ¹ä¼ è¿æ¥çä¸ä¸æ */ public ReportFields copy() { return new ReportFields() .setReportNo(reportNo).setReportName(reportName).setSampleNo(sampleNo) .setProductCode(productCode).setProductName(productName).setSpec(spec) .setBatchNo(batchNo).setWorkOrderNo(workOrderNo).setInspectType(inspectType) .setInspector(inspector).setInspectDate(inspectDate).setDepartment(department) .setCustomerName(customerName).setSupplierName(supplierName) .setResult(result).setResultText(resultText).setConclusion(conclusion) .setPassRate(passRate) .setQcResult(qcResult).setQcResultText(qcResultText) .setTotal(total).setPassCount(passCount).setFailCount(failCount); } /** æå¤å®ç»æåå result / resultText / conclusionï¼result 为 null æ¶ç½®ä¸ºå¾ å¤å® */ public ReportFields applyResult(QualityResult verdict) { this.result = verdict == null ? "" : verdict.name(); String text = verdict == null ? QualityResult.PENDING_TEXT : verdict.text(); this.resultText = text; this.conclusion = text; return this; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/render/CanvasSafety.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,268 @@ package cn.iocoder.yudao.module.qcreport.engine.render; import cn.hutool.core.util.StrUtil; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; /** * ç»å¸å®å ¨æ§æ ¡éªï¼æ¨¡æ¿ä¿åæ¶æãæ¸²ææ¶ä¼åæ ·æ¼è¿ HTML çå 容ãå å¡ä¸éã * <p> * æ¥åäº§ç©æ¯æ¼å符串æ¼åºæ¥çï¼{@link HtmlRenderer}ï¼ï¼æ¨¡æ¿ç»å¸åæ¯ç¨æ·å¯ç¼è¾æ°æ®ï¼ * æä»¥ç»å¸é塿¯<b>æªç»è½¬ä¹å°±è¿å ¥äº§ç©</b>çå 容齿¯æ³¨å ¥é¢ãéæ¡åæå®ä»¬ï¼æ¹å¨æ¸²æææ¬ç±»æ¶ä¸¤è¾¹ä¸èµ·çï¼ * <ul> * <li>{@code resolveTag} ââ èç¹ç {@code tagName} ç´æ¥æ¼æ {@code <tag>}ï¼æ¬ç±»æ<b>ç½åå</b>å¡ã</li> * <li>{@code renderAttributes} ââ åªå¯¹å±æ§<b>å¼</b>å转ä¹ï¼å±æ§<b>å</b>æ¯ç´æ¥æ¼ä¸å»çï¼ * ä¸ä¸ªå«å¼å·ç屿§åå°±è½æåé¢çå å®¹é¡¶ææ°å±æ§ï¼æ¬ç±»æå符éå¡ã</li> * <li>{@code buildCss} ââ {@code styles} è¥æ¯å符串ä¼è¢«<b>åæ ·è¿å</b>ï¼æ´ä¸ª {@code <style>} çå 容ï¼ï¼ * è {@code <style>} åéåºç° {@code </style} å°±è½æåéåãæåé¢åæç HTMLï¼ * æ¬ç±»è¦æ± {@code styles} å¿ é¡»æ¯è§åæ°ç»ï¼å¹¶ç¦æ¢éæ©å¨/屿§å/屿§å¼/mediaText éåºç° {@code <}ã</li> * </ul> * <p> * <b>为ä»ä¹å¨ä¿åæ¶å¡è䏿¯æ¸²ææ¶å¡</b>ï¼æ¸²æé¾è·¯ä¸ç {@link HtmlRenderer} ä¸å端 * {@code engine/render.ts} å¿ é¡»éåä¸è´ï¼æ¹å®å°±è¦éç®å¯¹æäº§ç©ã并让å鿥åç {@code regenerate} * ä¸åéåå¤ç°ï¼èãå ¥ç«å 容æ¯å¦å¯ä¿¡ãæ¬æ¥å°±æ¯åå ¥ä¾§çé®é¢ï¼å¨æ°æ®è¿åºåæ¦æï¼æ¸²æä¾§å¯ä»¥ç»§ç» * ä¿æä¸å端å®å ¨å¯¹ç§°ã代价æ¯<b>å¯¹æ ¡éªä¸çº¿åå°±å·²å ¥åºçèæ°æ®æ²¡æå åº</b>ââåéçæ¬éè¦å¦è¡æ ¸æ¥ã * <p> * <b>为ä»ä¹èµ°çæ¯ç½ååè䏿¯è¿æ»¤å±é©æ ç¾</b>ï¼é»ååæ°¸è¿æ¼ï¼èæ¥åæçç¨å¾å°çæ ç¾æ¯æéçã * 坿䏾çãæ£å¸¸æ¨¡æ¿ä¸ä¼å ä¸ºè¿æ¡ç½åå被æï¼å·²å¯¹åºéå ¨é¨åéçæ¬å®æµè¿ï¼ã */ public final class CanvasSafety { /** * å 许åºç°å¨ç»å¸éçæ ç¾ã * <p> * åªæ¶ãæç䏿æ¬ãç±»æ ç¾ï¼ç»ä»¶æ³¨å表è½äº§åºçï¼{@code div/p/span/hr/img/h1~h6} ä¸è¡¨æ ¼ä¸æï¼ * å ä¸ GrapesJS åºç¡ç»ä»¶ä¸å¸¸è§è¡å è¯ä¹æ ç¾ã廿æé¤åç±»ââ * 坿§è¡ï¼{@code script}ï¼ã坿³¨å ¥æ ·å¼ï¼{@code style}ï¼ãå¯åµå ¥å¤é¨å 容ï¼{@code iframe/object/embed}ï¼ã * å¯æäº¤æ°æ®ï¼{@code form} åå ¶æ§ä»¶ï¼ï¼ä»¥å {@code svg/math} è¿ç±»èªå¸¦èæ¬è½åçå½åç©ºé´æ ç¾ã * <p> * GrapesJS ä¼å¨ç»ä»¶ä¸æ {@code docEl: {tagName: "html"}}ã{@code head: {type: "head"}} è¿ç±» * <b>ä¸ä¼æ¸²æ</b>çå æ°æ®ï¼æä»¥æ¬ç±»åªæ²¿ {@code components} èµ°ï¼ä¸ {@code RenderNode.children()} ä¸è´ï¼ï¼ * ç½ååéä¹å°±ä¸éè¦ {@code html}/{@code head}ã */ private static final Set<String> ALLOWED_TAGS = Set.of( // åºåä¸å®¹å¨ "div", "span", "section", "article", "header", "footer", "main", "aside", "nav", "figure", "figcaption", "blockquote", "pre", "address", "center", // ææ¬ä¸æ é¢ "p", "br", "hr", "h1", "h2", "h3", "h4", "h5", "h6", "b", "i", "u", "s", "strong", "em", "small", "big", "sub", "sup", "mark", "code", "kbd", "samp", "var", "tt", "strike", "font", "wbr", "bdi", "bdo", "ruby", "rt", "rp", "abbr", "cite", "q", "time", "del", "ins", // å表 "ul", "ol", "li", "dl", "dt", "dd", // è¡¨æ ¼ "table", "thead", "tbody", "tfoot", "tr", "td", "th", "caption", "col", "colgroup", // å¾çä¸é¾æ¥ "img", "a", "label"); /** åæ³ç屿§åï¼åæ¯/ä¸å线/åå·å¼å¤´ï¼å ¶å忝æ°åä¸ {@code - _ : .}ãå«å¼å·æç©ºæ ¼ç屿§åä¸å¾æç» */ private static final Pattern ATTRIBUTE_NAME = Pattern.compile("^[A-Za-z_:][-A-Za-z0-9_:.]*$"); /** æå¤æ¥å æ¡ï¼ä¸æ¡æ¥éä¿¡æ¯éå å åæ¡æ²¡äººçå¾ä¸å»ï¼æ¹å®åå䏿¬¡å°±è½çå°ä¸ä¸æ¹ */ private static final int MAX_PROBLEMS = 8; /** è·¯å¾å±ç¤ºçæå¤§å±æ°ï¼å深就æå æ â¦ï¼é¿å 䏿¡æç¤ºéåºç°ä¸é¿ä¸²ã第 N 个ç»ä»¶ã */ private static final int MAX_PATH_DEPTH = 6; private static final String UNSAFE_LT = "<"; private CanvasSafety() { } /** * æ ¡éªä¸ä»½ç»å¸ã * * @param grapes GradesJS é¡¹ç®æ°æ®ï¼æ¨¡æ¿ Schema ç {@code grapes} åæ®µï¼ï¼å¯ä¸º null * @return é®é¢æ¸ åï¼ç©ºå表代表éè¿ãææ¡å·²å¯ç´æ¥æ¼ç»ç¨æ·ç */ public static List<String> validate(Map<String, Object> grapes) { List<String> problems = new ArrayList<>(); if (grapes == null) { return problems; } validateComponentTree(grapes, problems); validateStyles(grapes.get("styles"), problems); return problems; } /* ------------------------------ ç»ä»¶æ ------------------------------ */ private static void validateComponentTree(Map<String, Object> grapes, List<String> problems) { for (Map<String, Object> frameComponent : frameComponents(grapes)) { walk(frameComponent, "æ ¹", 0, problems); } } /** åææé¡µææ frame çæ ¹ç»ä»¶ã渲æå¨åªè¯» {@code pages[0].frames[0]}ï¼è¿éå ¨æ¥ä¸éï¼å¤åºæ¥çé¨åä¹ä¸è½èèä¸è¥¿ */ private static List<Map<String, Object>> frameComponents(Map<String, Object> grapes) { List<Map<String, Object>> result = new ArrayList<>(); collectFrames(grapes.get("pages"), result); return result; } @SuppressWarnings("unchecked") private static void collectFrames(Object pages, List<Map<String, Object>> result) { if (!(pages instanceof List<?> pageList)) { return; } for (Object rawPage : pageList) { if (!(rawPage instanceof Map<?, ?> page)) { continue; } Object frames = ((Map<String, Object>) page).get("frames"); if (!(frames instanceof List<?> frameList)) { continue; } for (Object rawFrame : frameList) { if (rawFrame instanceof Map<?, ?> frame && ((Map<String, Object>) frame).get("component") instanceof Map<?, ?> component) { result.add((Map<String, Object>) component); } } } } @SuppressWarnings("unchecked") private static void walk(Map<String, Object> node, String path, int depth, List<String> problems) { if (problems.size() >= MAX_PROBLEMS) { return; } validateNodeTag(node, path, problems); validateNodeAttributes(node, path, problems); if (!(node.get("components") instanceof List<?> children)) { return; } int index = 0; for (Object raw : children) { if (!(raw instanceof Map<?, ?> child)) { continue; } index++; walk((Map<String, Object>) child, path + " > 第 " + index + " 个ç»ä»¶", depth + 1, problems); if (problems.size() >= MAX_PROBLEMS) { return; } } } private static void validateNodeTag(Map<String, Object> node, String path, List<String> problems) { Object raw = node.get("tagName"); String tagName = raw == null ? null : String.valueOf(raw).trim(); // textnode ä¸äº§çæ ç¾ï¼HtmlRenderer 对å®åç¬å¤çï¼ï¼å®èº«ä¸ç tagName æ¸²ææ¶è¢«å¿½ç¥ï¼ä¸å¿ æ¦ if (StrUtil.isBlank(tagName) || "textnode".equals(node.get("type"))) { return; } if (ALLOWED_TAGS.contains(tagName.toLowerCase(Locale.ROOT))) { return; } problems.add(StrUtil.format( "{} çæ ç¾æ¯ã{}ãï¼æ¥å䏿¯æè¯¥æ ç¾ãå¯ç¨çåªæå¸¸è§æçæ ç¾" + "ï¼div/p/span/h1~h6/img/hr ä¸è¡¨æ ¼ãå表ãè¡å ææ¬æ ç¾ï¼ï¼" + "èæ¬ãæ ·å¼ãå åµé¡µé¢ã表åç±»æ ç¾ä¸å¾ä¸å 许", displayPath(path), tagName)); } @SuppressWarnings("unchecked") private static void validateNodeAttributes(Map<String, Object> node, String path, List<String> problems) { if (!(node.get("attributes") instanceof Map<?, ?> attributes)) { return; } for (Object key : ((Map<String, Object>) attributes).keySet()) { String name = String.valueOf(key); if (!ATTRIBUTE_NAME.matcher(name).matches()) { problems.add(StrUtil.format( "{} ç屿§åã{}ã䏿¯åæ³ç屿§åï¼åªè½ç±åæ¯ãæ°åã- _ : . ç»æï¼ä¸ä¸è½ä»¥æ°åå¼å¤´ï¼ï¼" + "è¯¥å±æ§ä¼è®©æ¥å产ç©ç»æéä¹±", displayPath(path), name)); continue; } if (name.toLowerCase(Locale.ROOT).startsWith("on")) { problems.add(StrUtil.format( "{} ç屿§ã{}ãæ¯äºä»¶å±æ§ï¼æ¥å䏿¯æ", displayPath(path), name)); } } } /* ------------------------------ æ ·å¼ ------------------------------ */ @SuppressWarnings("unchecked") private static void validateStyles(Object styles, List<String> problems) { if (styles == null) { return; } if (styles instanceof String text) { if (StrUtil.isNotBlank(text)) { // buildCss 伿å®åæ ·å¡è¿ <style>ï¼çäºè®©æ¨¡æ¿èªå¸¦ä¸æ´æ®µä¸åæ§ç CSSï¼è¿è½æåéå <style>ï¼ problems.add("ç»å¸ç styles æ¯ä¸æ®µ CSS ææ¬ï¼æ¥å䏿¥åæ´æ®µ CSSï¼" + "请æ¹ç¨è®¾è®¡å¨çæ ·å¼é¢æ¿éæ¡è®¾ç½®æ ·å¼ï¼ä¿ååæ¯æ ·å¼è§åæ°ç»ï¼"); } return; } if (!(styles instanceof List<?> rules)) { problems.add("ç»å¸ç styles æ¢ä¸æ¯æ ·å¼è§åæ°ç»ä¹ä¸æ¯ CSS ææ¬ï¼æ æ³è¯å«ï¼è¯·éæ°ä¿å模æ¿"); return; } for (int i = 0; i < rules.size() && problems.size() < MAX_PROBLEMS; i++) { if (!(rules.get(i) instanceof Map<?, ?> rule)) { continue; } Map<String, Object> ruleMap = (Map<String, Object>) rule; validateStyleFragments(ruleMap.get("selectors"), "æ ·å¼è§å " + (i + 1) + " çéæ©å¨", problems); validateStyleFragments(ruleMap.get("mediaText"), "æ ·å¼è§å " + (i + 1) + " çåªä½æ¥è¯¢æ¡ä»¶", problems); Object style = ruleMap.get("style"); if (style instanceof Map<?, ?> styleMap) { for (Map.Entry<?, ?> entry : ((Map<String, Object>) styleMap).entrySet()) { String where = StrUtil.format("æ ·å¼è§å {} ç屿§ã{}ã", i + 1, String.valueOf(entry.getKey())); if (String.valueOf(entry.getKey()).contains(UNSAFE_LT)) { problems.add(where + "ååé嫿ã<ãï¼æ¥å䏿¯æ"); continue; } validateStyleFragments(entry.getValue(), where, problems); } } } } /** éæ©å¨/åªä½æ¥è¯¢/æ ·å¼å¼é½è¿ {@code <style>}ï¼åºç° {@code <} å°±å¯è½éå style å */ private static void validateStyleFragments(Object raw, String where, List<String> problems) { if (raw instanceof List<?> list) { for (Object item : list) { validateStyleFragments(item, where, problems); } return; } if (raw == null || problems.size() >= MAX_PROBLEMS) { return; } String text = String.valueOf(raw); if (text.contains(UNSAFE_LT)) { problems.add(StrUtil.format("{}ã{}ãé嫿ã<ãï¼æ¥å䏿¯æï¼è¯·å¨è®¾è®¡å¨éæ¹æ", where, StrUtil.maxLength(text, 60))); } } /* ------------------------------ å±ç¤º ------------------------------ */ /** è·¯å¾å¤ªæ·±å°±æå 䏿®µï¼æ¥éä¿¡æ¯éåºç°ä¸é¿ä¸²ã第 N 个ç»ä»¶ã对å®ä½æ²¡æå¸®å© */ private static String displayPath(String path) { String[] segments = path.split(" > "); if (segments.length <= MAX_PATH_DEPTH) { return "ç»å¸ã" + path + "ã"; } List<String> shown = new ArrayList<>(List.of(segments[0], segments[1], "â¦")); for (int i = segments.length - 3; i < segments.length; i++) { shown.add(segments[i]); } return "ç»å¸ã" + String.join(" > ", shown) + "ã"; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/render/HtmlRenderer.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,408 @@ package cn.iocoder.yudao.module.qcreport.engine.render; import cn.iocoder.yudao.module.qcreport.engine.Bindings; import cn.iocoder.yudao.module.qcreport.engine.PageSetting; import cn.iocoder.yudao.module.qcreport.engine.PageSizes; import cn.iocoder.yudao.module.qcreport.engine.Paths; import cn.iocoder.yudao.module.qcreport.engine.ResolvedPage; import cn.iocoder.yudao.module.qcreport.engine.context.ReportContext; import cn.iocoder.yudao.module.qcreport.engine.report.EvaluationOutcome; import cn.iocoder.yudao.module.qcreport.engine.report.ReportEvaluator; import cn.iocoder.yudao.module.qcreport.engine.rule.QualityRuleDefinition; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; /** * æ¥å渲æå¼æã * <p> * è¾å ¥ãæ¨¡æ¿ Schema + æ¥åä¸ä¸æãï¼è¾åºå¯ç´æ¥æå°ç HTML ææ¡£ã * 渲æé¡ºåºåºå®ä¸ºãå å¤å®ã忏²æãï¼PASS/FAIL ç± {@link ReportEvaluator} ç®å¥½åè¿ä¸ä¸æï¼ * 模æ¿éåªååå¼ï¼å æ¤åä¸ä»½æ°æ®å¨ä»»ä½æ¨¡æ¿ä¸å¤å®ä¸è´ï¼æ¨¡æ¿ä¹æ æ³å·¦å³å¤å®ç»æã * <p> * è¿éä¸è®¤è¯å ·ä½ç»ä»¶ç±»åï¼åªå GrapesJS 导åºçé¡¹ç®æ°æ®ï¼ç»ä»¶æ + æ ·å¼ï¼ï¼ * åªè®¤ä¸å¥åè®®ï¼{@code data-qc-repeat} éå¤è¡ã{@code {{è·¯å¾}}} ç»å®ã{@code schema.rules} å¤å®è§åã * ä¸å端 {@code engine/render.ts} ä¸ä¸å¯¹åºï¼ä¸¤è¾¹äº§åºç HTML å¿ é¡»ä¸è´ã */ public final class HtmlRenderer { /** è®¾è®¡å¨æ è®°ï¼ä¸è¿å ¥äº§ç© */ private static final String QUALITY_TYPE_ATTR = "data-quality-type"; private static final String QUALITY_PROP_PREFIX = "data-qc-"; private static final String QUALITY_REPEAT_ATTR = "data-qc-repeat"; private static final String QUALITY_REPEAT_ROW_ATTR = "data-qc-repeat-row"; /** GrapesJS çç»ä»¶ id æ¢æ data 屿§ï¼éå¤è¡é id ä¼éåï¼data 屿§ä¸ä¼ */ private static final String QUALITY_ID_ATTR = "data-qc-id"; /** èªéåæ ç¾ï¼ä¸è½çæç»ææ ç¾ */ private static final Set<String> VOID_TAGS = Set.of( "br", "col", "hr", "img", "input", "link", "meta", "source", "track", "wbr"); /** * GrapesJS ç»ä»¶ç±»å â HTML æ ç¾ã * <p> * é¡¹ç®æ°æ®åªå¨æ ç¾ä¸ç±»åé»è®¤å¼ä¸åæ¶æå tagNameï¼ä¾å¦ thãspanãh2ï¼ï¼ * è¡¨æ ¼ç»æï¼table/thead/tbody/row/cellï¼å¨æ°æ®éé½åªæ typeï¼ * æä»¥æ¼æè¿å¼ è¡¨å°±ä¼ææ´å¼ è¡¨æ ¼æ¸²ææä¸å divãæªæ¶å½çç±»ååéå° divï¼ * ä¸ GrapesJS åºç¡ç»ä»¶çé»è®¤æ ç¾ä¸è´ã */ private static final Map<String, String> TYPE_TAGS = Map.of( "cell", "td", "row", "tr", "table", "table", "tbody", "tbody", "tfoot", "tfoot", "thead", "thead", "text", "div", "wrapper", "div"); /** 渲æäº§ç©çåºç¡æçãæµè§å¨é»è®¤çè¡¨æ ¼ä¸é¡µè¾¹è·ä¼è®©æ¥åèµ°æ ·ï¼è¿éæ¶æææå°å好çåºçº¿ */ private static final String BASE_CSS = """ * { box-sizing: border-box; } body { margin: 0; color: #000; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; font-size: 12px; line-height: 1.5; } table { border-collapse: collapse; width: 100%; } img { max-width: 100%; } tr, td, th { page-break-inside: avoid; }"""; /** æ¿è½½å¤é¨å°åç屿§ï¼å¼éåºç°å¯æ§è¡åè®®ä¸å¾æ¦æ */ private static final Set<String> URL_ATTRIBUTES = Set.of( "href", "src", "xlink:href", "action", "formaction", "background", "poster"); /** 坿§è¡ / 坿³¨å ¥ç URL åè®® */ private static final Pattern DANGEROUS_SCHEME = Pattern.compile("^\\s*(?:javascript|vbscript|data)\\s*:", Pattern.CASE_INSENSITIVE); /** data: éåªæå¾çæ¯æ£å½ç¨éï¼å ¶ä½ï¼text/html çï¼ä¼åææ³¨å ¥é¢ */ private static final Pattern DATA_IMAGE = Pattern.compile("^\\s*data:image/", Pattern.CASE_INSENSITIVE); /** GrapesJS ç¨ #ç»ä»¶id éæ©å¨ï¼è¿éæ¹åæ data 屿§éæ©å¨ */ private static final Pattern ID_SELECTOR = Pattern.compile("#([\\w-]+)"); private HtmlRenderer() { } /** * æ¸²ææ¥åï¼å¤å® â çææ£æ â æ¼æ ·å¼ â ç»è£ ææ¡£ã * * @param grapes GrapesJS é¡¹ç®æ°æ®ï¼æ¨¡æ¿ Schema ç grapes åæ®µï¼ * @param page çº¸å¼ ä¸é¡µè¾¹è·é ç½® * @param context æ¥åä¸ä¸æ * @param rules å¤å®è§åï¼å¯ä¸ºç©º */ public static RenderOutcome render(Map<String, Object> grapes, PageSetting page, ReportContext context, List<QualityRuleDefinition> rules) { EvaluationOutcome evaluated = ReportEvaluator.evaluate(context, rules); RenderState state = new RenderState(evaluated.context().toScope(), "模æ¿", new ArrayList<>(evaluated.errors()), new LinkedHashSet<>()); RenderNode root = RenderNode.of(Paths.readPath(grapes, "pages[0].frames[0].component")); String body = root == null ? "" : renderNode(root, state); String css = buildCss(grapes == null ? null : grapes.get("styles")); String html = buildDocument(page, body, css, evaluated.context()); return new RenderOutcome(html, body, evaluated.context(), state.errors); } /* ------------------------------ èç¹å±å¼ ------------------------------ */ /** 渲æå个èç¹ï¼ææ¬èç¹ä¸éå¤å®¹å¨åç¬å¤çï¼å ¶ä½æãæ ç¾ + 屿§ + åèç¹ãå±å¼ */ private static String renderNode(RenderNode node, RenderState state) { // ææ¬èç¹åªè´¡ç®ææ¬ï¼èªèº«ä¸äº§çæ ç¾ if ("textnode".equals(node.type())) { return renderText(node.content() == null ? "" : node.content(), state); } String repeatPath = node.attribute(QUALITY_REPEAT_ATTR); if (repeatPath != null && !repeatPath.isEmpty()) { return renderRepeat(node, repeatPath, state); } String tag = resolveTag(node); String attributes = renderAttributes(node, state); String style = renderStyle(node); if (VOID_TAGS.contains(tag)) { return "<" + tag + attributes + style + ">"; } return "<" + tag + attributes + style + ">" + renderChildren(node, state) + "</" + tag + ">"; } /** æ ç¾ä¼å åæ°æ®éç tagNameï¼å ¶æ¬¡æç»ä»¶ç±»åæ¨æ */ private static String resolveTag(RenderNode node) { String tagName = node.tagName(); if (tagName != null && !tagName.isEmpty()) { return tagName; } String type = node.type(); String byType = type == null ? null : TYPE_TAGS.get(type); return byType == null ? "div" : byType; } /** * åèç¹æå£°æé¡ºåºå±å¼ã * <p> * æåç»ä»¶æ¶å¿½ç¥ contentââä¸ GrapesJS ä¸è´ï¼å¾ä¸ä¸ªå¸¦ææ¬çç»ä»¶éåæå ¥ç»ä»¶åï¼ * ææ¬å°±è®©ä½ç»åç»ä»¶ï¼è¿éå¿ é¡»åè§åï¼å¦å渲æäº§ç©ä¼å设计å¨çå°çä¸ä¸æ ·ã */ private static String renderChildren(RenderNode node, RenderState state) { List<RenderNode> children = node.children(); if (!children.isEmpty()) { StringBuilder builder = new StringBuilder(); for (RenderNode child : children) { builder.append(renderNode(child, state)); } return builder.toString(); } String content = node.content(); return content == null ? "" : renderText(content, state); } /** * éå¤å®¹å¨ï¼ææ°ç»è·¯å¾å±å¼è¡æ¨¡æ¿ï¼å ¶ä½åèç¹å¨åä½ç½®æ¸²æä¸æ¬¡ã * <p> * è¡æ¨¡æ¿ä¸ç {@code data-qc-repeat-row} 声æäºè¡å ç循ç¯åéåï¼å¦ itemï¼ï¼ * è¡å å®¹ç¨ {@code {{item.xxx}}} åå¼ï¼ä¸è®¾è®¡æå¨ç»ä»¶é约å®çç»å®è·¯å¾ä¸è´ã */ private static String renderRepeat(RenderNode node, String path, RenderState state) { List<Object> rows = toRowList(Paths.readPath(state.scope, path)); StringBuilder inner = new StringBuilder(); for (RenderNode child : node.children()) { String rowVar = child.attribute(QUALITY_REPEAT_ROW_ATTR); if (rowVar == null || rowVar.isEmpty()) { inner.append(renderNode(child, state)); continue; } for (int index = 0; index < rows.size(); index++) { Map<String, Object> rowScope = new LinkedHashMap<>(state.scope); rowScope.put(rowVar, rows.get(index)); // index ç±éå¤å®¹å¨æ³¨å ¥ï¼è¡å å¯å {{index}}ï¼ä» 1 å¼å§ rowScope.put("index", index + 1); inner.append(renderNode(child, state.withScope(rowScope, "第 " + (index + 1) + " è¡"))); } } String tag = resolveTag(node); return "<" + tag + renderAttributes(node, state) + renderStyle(node) + ">" + inner + "</" + tag + ">"; } /** éæ°ç»æåè¡å¤çï¼ç©ºå¼å¾å°ç©ºè¡¨ï¼æ¯æéæ´è´´åãæ°æ®æ²¡å¡«å ¨ãçæ¥ååºæ¯ */ private static List<Object> toRowList(Object value) { if (value instanceof List<?> list) { return new ArrayList<>(list); } if (value == null) { return List.of(); } return List.of(value); } /* ------------------------------ 屿§ä¸æ ·å¼ ------------------------------ */ /** * 渲æèç¹å±æ§ã * <p> * è®¾è®¡å¨æ è®°ï¼data-quality-type / data-qc-*ï¼ä¸è¿å ¥äº§ç©ï¼ * 屿§å¼éç {@code {{path}}} ä¸å¹¶è§£æï¼å¾çå°åã龿¥çä¹è½ç»å®æ°æ®ã * <p> * æ¯å端å¤ä¸éé¸ï¼äºä»¶å±æ§ä¸å¯æ§è¡åè®®ä¸è¿äº§ç©ãäº§ç©æ¯ç»äººæå°çææ¡£ï¼ * on* ä¹ç±»ç屿§æ²¡ææ£å½ç¨éï¼æ¾è¡çäºç»æ¨¡æ¿å¼äºä¸ªæ§è¡å£åã */ private static String renderAttributes(RenderNode node, RenderState state) { StringBuilder builder = new StringBuilder(); for (Map.Entry<String, String> entry : node.attributes().entrySet()) { String name = entry.getKey(); if (QUALITY_TYPE_ATTR.equals(name) || name.startsWith(QUALITY_PROP_PREFIX)) { continue; } String text = renderText(entry.getValue() == null ? "" : entry.getValue(), state); if (text.isEmpty()) { continue; } if (!isSafeAttribute(name, text, state)) { continue; } builder.append(' ').append("id".equals(name) ? QUALITY_ID_ATTR : name) .append("=\"").append(escapeHtml(text)).append('"'); } return builder.toString(); } /** 屿§å/å¼å®å ¨é¸ï¼æ¦ä¸çåè¿é®é¢æ¸ åï¼è®©æ¨¡æ¿ä½è çå¾è§è䏿¯ææå°ä¸ªå±æ§ */ private static boolean isSafeAttribute(String name, String value, RenderState state) { String lower = name.toLowerCase(Locale.ROOT); if (lower.length() > 2 && lower.startsWith("on")) { state.errors.add(state.where + "屿§ã" + name + "ãæ¯äºä»¶å±æ§ï¼æ¥å产ç©ä¸è¾åºï¼å·²å¿½ç¥"); return false; } if (URL_ATTRIBUTES.contains(lower) && DANGEROUS_SCHEME.matcher(value).find() && !DATA_IMAGE.matcher(value).find()) { state.errors.add(state.where + "屿§ã" + name + "ãçå°ååè®®ä¸å®å ¨ï¼æ¥å产ç©ä¸è¾åºï¼å·²å¿½ç¥"); return false; } return true; } /** èç¹èªèº«çè¡å æ ·å¼ */ private static String renderStyle(RenderNode node) { String style = cssStyle(node.style()); return style.isEmpty() ? "" : " style=\"" + escapeHtml(style) + "\""; } /** åææ¬éçç»å®å¼ï¼åä¸å°çè·¯å¾è®°è¿é®é¢æ¸ åï¼æªè§£æç {{}} ä¸ä¼çå¨äº§ç©éï¼ */ private static String renderText(String template, RenderState state) { return Bindings.resolveText(template, state.scope, path -> { if (state.reported.add(state.where + "|" + path)) { state.errors.add(state.where + "ç»å®ã{{" + path + "}}ãå¨å½åæ°æ®ä¸åä¸å°å¼ï¼å·²æ¸²æä¸ºç©º"); } }); } /** æ¼ä¸æ®µå èæ ·å¼ï¼è¿æ»¤æç©ºå¼ */ private static String cssStyle(Map<String, String> style) { List<String> parts = new ArrayList<>(style.size()); for (Map.Entry<String, String> entry : style.entrySet()) { String value = entry.getValue(); if (value == null || value.isEmpty()) { continue; } parts.add(entry.getKey() + ":" + value); } return String.join(";", parts); } /* ------------------------------ æ ·å¼ä¸ææ¡£ ------------------------------ */ /** æ ·å¼è§åæ°ç» â CSS ææ¬ï¼GrapesJS ç¨ #ç»ä»¶id éæ©å¨ï¼è¿éæ¹åæ data 屿§éæ©å¨ */ private static String buildCss(Object styles) { if (styles instanceof String text) { return text; } if (!(styles instanceof List<?> rules)) { return ""; } List<String> blocks = new ArrayList<>(); Map<String, List<String>> mediaBlocks = new LinkedHashMap<>(); for (Object raw : rules) { if (!(raw instanceof Map<?, ?> rule)) { continue; } String selector = buildSelector(rule.get("selectors")); String body = cssStyle(textMap(rule.get("style"))); if (selector.isEmpty() || body.isEmpty()) { continue; } String line = selector + " { " + body + " }"; String mediaText = textOf(rule.get("mediaText")).trim(); if (!mediaText.isEmpty()) { mediaBlocks.computeIfAbsent(mediaText, key -> new ArrayList<>()).add(line); continue; } blocks.add(line); } mediaBlocks.forEach((mediaText, lines) -> { String atRule = mediaText.startsWith("@") ? mediaText : "@media " + mediaText; blocks.add(atRule + " { " + String.join(" ", lines) + " }"); }); return String.join("\n", blocks); } private static String buildSelector(Object rawSelectors) { if (!(rawSelectors instanceof List<?> selectors)) { return ""; } List<String> parts = new ArrayList<>(selectors.size()); for (Object raw : selectors) { String selector = toSafeSelector(textOf(raw)); if (!selector.isEmpty()) { parts.add(selector); } } return String.join(", ", parts); } /** #ç»ä»¶id â [data-qc-id="ç»ä»¶id"]ï¼éå¤è¡å¤å¶åæ ·å¼ä»ç¶å½ä¸ */ private static String toSafeSelector(String selector) { return ID_SELECTOR.matcher(selector).replaceAll("[" + QUALITY_ID_ATTR + "=\"$1\"]"); } private static Map<String, String> textMap(Object raw) { Map<String, String> result = new LinkedHashMap<>(); if (raw instanceof Map<?, ?> map) { for (Map.Entry<?, ?> entry : map.entrySet()) { result.put(String.valueOf(entry.getKey()), entry.getValue() == null ? null : String.valueOf(entry.getValue())); } } return result; } private static String textOf(Object value) { return value == null ? "" : String.valueOf(value); } /** ç»è£ æç»ææ¡£ï¼çº¸å¼ ä¸é¡µè¾¹è·äº¤ç» @pageï¼æ£æéä¸åéå¤çç½ï¼æè½ä¿è¯æ¯é¡µé½æè¾¹è· */ private static String buildDocument(PageSetting page, String body, String css, ReportContext context) { ResolvedPage resolved = PageSizes.resolve(page); String title = context.getReport().getReportName(); if (title == null || title.isEmpty()) { title = context.getReport().getReportNo(); } if (title == null || title.isEmpty()) { title = "è´¨æ£æ¥å"; } return String.join("\n", "<!doctype html>", "<html lang=\"zh-CN\">", "<head>", "<meta charset=\"utf-8\" />", "<title>" + escapeHtml(title) + "</title>", "<style>", "@page { size: " + resolved.cssSize() + "; margin: " + resolved.cssMargin() + "; }", BASE_CSS, css, "</style>", "</head>", "<body>" + body + "</body>", "</html>"); } /** 任使¥èªç¨æ·ï¼è®¾è®¡å¨å±æ§é¢æ¿ï¼çææ¬é½å¿ é¡»å 转ä¹åæ¼è¿æ ç¾ï¼å¦å渲ææå°±æ¯æ³¨å ¥ç¹ */ private static String escapeHtml(String value) { return value.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); } /** 䏿¬¡æ¸²æçè¡èµ°ç¶æï¼ä½ç¨å + å½åä½ç½® + é®é¢æ¸ åï¼è¡å å¤å¶æ¶å ±äº«å两è */ private static final class RenderState { private final Map<String, Object> scope; /** å½å渲æä½ç½®çæè¿°ï¼ç¨äºæé®é¢å®ä½å°å ·ä½è¡ */ private final String where; private final List<String> errors; /** 已䏿¥ç缺å£ï¼åä¸å¤åªæç¤ºä¸æ¬¡ */ private final Set<String> reported; private RenderState(Map<String, Object> scope, String where, List<String> errors, Set<String> reported) { this.scope = scope; this.where = where; this.errors = errors; this.reported = reported; } private RenderState withScope(Map<String, Object> scope, String where) { return new RenderState(scope, where, errors, reported); } } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/render/RenderNode.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,88 @@ package cn.iocoder.yudao.module.qcreport.engine.render; import cn.iocoder.yudao.module.qcreport.engine.JsValues; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * GrapesJS é¡¹ç®æ°æ®èç¹ã * <p> * åªå£°ææ¸²æéè¦çåæ®µï¼ç´æ¥è¯»åå§ Mapï¼{@code components} æ¢å¯è½æ¯æ°ç»ä¹å¯è½æ¯åç¬¦ä¸²ï¼ * ç¨ POJO ååºååè¦é¢å¤å¤çèåç±»åï¼ä¸å¦æéå弿¥å¾ç´ç½ï¼ä¹ç䏿¬¡æ·è´ã */ public final class RenderNode { private final Map<String, Object> data; private RenderNode(Map<String, Object> data) { this.data = data; } /** 䏿¯å¯¹è±¡èç¹æ¶è¿å nullï¼å¦æ°ç»éæ··å ¥äºåç¬¦ä¸²ï¼ */ @SuppressWarnings("unchecked") public static RenderNode of(Object raw) { return raw instanceof Map<?, ?> ? new RenderNode((Map<String, Object>) raw) : null; } public String type() { return text(data.get("type")); } public String tagName() { return text(data.get("tagName")); } /** åªå¨ content æ¬èº«æ¯å符串æ¶è¿åï¼ä¸å端 {@code typeof node.content === 'string'} ä¸è´ */ public String content() { return data.get("content") instanceof String content ? content : null; } /** åä¸ä¸ªå±æ§å¼ï¼åä¸å°è¿å null */ public String attribute(String name) { return attributes().get(name); } /** 屿§è¡¨ï¼å¼ç»ä¸æææ¬ */ public Map<String, String> attributes() { Map<String, String> result = new LinkedHashMap<>(); if (data.get("attributes") instanceof Map<?, ?> attributes) { for (Map.Entry<?, ?> entry : attributes.entrySet()) { result.put(String.valueOf(entry.getKey()), JsValues.asText(entry.getValue())); } } return result; } /** è¡å æ ·å¼è¡¨ï¼å¼ç»ä¸æææ¬ */ public Map<String, String> style() { Map<String, String> result = new LinkedHashMap<>(); if (data.get("style") instanceof Map<?, ?> style) { for (Map.Entry<?, ?> entry : style.entrySet()) { result.put(String.valueOf(entry.getKey()), JsValues.asText(entry.getValue())); } } return result; } /** åç»ä»¶ï¼components æ¯åç¬¦ä¸²ï¼æ§æ°æ®/çº¯ææ¬ç»ä»¶ï¼æ¶è¿å空å表 */ public List<RenderNode> children() { List<RenderNode> result = new ArrayList<>(); if (data.get("components") instanceof List<?> components) { for (Object raw : components) { RenderNode child = of(raw); if (child != null) { result.add(child); } } } return result; } private static String text(Object value) { return value == null ? null : String.valueOf(value); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/render/RenderOutcome.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,16 @@ package cn.iocoder.yudao.module.qcreport.engine.render; import cn.iocoder.yudao.module.qcreport.engine.context.ReportContext; import java.util.List; /** * 渲æç»æã * * @param html 宿´ HTML ææ¡£ï¼æå°/PDF ç´æ¥ä½¿ç¨ * @param body æ£æçæ®µï¼ä¾¿äºåµå ¥é¡µé¢é¢è§ * @param context å¤å®åçä¸ä¸æï¼å« PASS/FAIL å¤å®ä¸åæ ¼çï¼ * @param errors è§åä¸ç»å®çé®é¢æ¸ åï¼è°ç¨æ¹å¿ é¡»æ¾å¼æ´é²ï¼ä¸è½ææåæ */ public record RenderOutcome(String html, String body, ReportContext context, List<String> errors) { } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/report/EvaluationOutcome.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,14 @@ package cn.iocoder.yudao.module.qcreport.engine.report; import cn.iocoder.yudao.module.qcreport.engine.context.ReportContext; import java.util.List; /** * å¤å®ç»æã * * @param context å¤å®åçä¸ä¸æï¼å«å项 PASS/FAIL 䏿¥åç»è®ºãåæ ¼çï¼ * @param errors è§åçé®é¢æ¸ åï¼æ¸²ææ¶å¿ 须卿¥å䏿¾å¼æ´é²ï¼ä¸è½ææåæ */ public record EvaluationOutcome(ReportContext context, List<String> errors) { } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/report/ReportEvaluator.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,190 @@ package cn.iocoder.yudao.module.qcreport.engine.report; import cn.iocoder.yudao.module.qcreport.engine.Paths; import cn.iocoder.yudao.module.qcreport.engine.QualityResult; import cn.iocoder.yudao.module.qcreport.engine.context.InspectionItem; import cn.iocoder.yudao.module.qcreport.engine.context.ReportContext; import cn.iocoder.yudao.module.qcreport.engine.context.ReportFields; import cn.iocoder.yudao.module.qcreport.engine.rule.QualityRuleDefinition; import cn.iocoder.yudao.module.qcreport.engine.rule.RuleEngine; import cn.iocoder.yudao.module.qcreport.engine.rule.RuleScope; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; /** * æ¥åå¤å®ã * <p> * 卿¸²æåææ£éªé¡¹ç®æ PASS/FAILï¼å¹¶æ±æ»åºæ¥åç»è®ºä¸åæ ¼çã * å¤å®åªåçå¨è¿éï¼ç»ä»¶ä¸æ¨¡æ¿é½ä¸å计ç®ï¼ä¿è¯åä¸ä»½æ°æ®å¨ä»»ä½æ¨¡æ¿ä¸å¤å®ä¸è´ã * <p> * ä¸å端 {@code engine/report-evaluator.ts} ä¸ä¸å¯¹åºï¼ä¸¤è¾¹å¿ é¡»ç®åºåä¸ç»æã */ public final class ReportEvaluator { private ReportEvaluator() { } /** * è®¡ç®æ£éªé¡¹å¤å®ä¸æ¥åæ±æ»ã * <p> * å个æ£éªé¡¹çå¤å®ä¼å 级ï¼item ä½ç¨åè§å â è§æ ¼ä¸ä¸é â æ°æ®èªå¸¦ç»æã * æ¥å级å¤å®é»è®¤åãå ¨é¨æ£éªé¡¹åæ ¼ãï¼æ report ä½ç¨åè§åæ¶ä»¥è§å为åã * <p> * ä¸ä¿®æ¹ä¼ å ¥çä¸ä¸æï¼å¤å®åçç»ææ¾å¨è¿åå¼éã */ public static EvaluationOutcome evaluate(ReportContext context, List<QualityRuleDefinition> rules) { List<String> errors = new ArrayList<>(); List<QualityRuleDefinition> itemRules = new ArrayList<>(); List<QualityRuleDefinition> reportRules = new ArrayList<>(); for (QualityRuleDefinition rule : rules == null ? List.<QualityRuleDefinition>of() : rules) { if (rule == null || !rule.active()) { continue; } if (rule.scope() == RuleScope.REPORT) { reportRules.add(rule); } else { itemRules.add(rule); } } List<InspectionItem> source = context.getInspectionItems() == null ? List.of() : context.getInspectionItems(); List<InspectionItem> items = new ArrayList<>(source.size()); for (InspectionItem item : source) { items.add(item.copy()); } // æ å¤å®ä¾æ®ç项æ°ï¼åªç¨æ¥æè¿ç±»é¡¹ä»åæ ¼ç忝䏿¥åç»è®ºéæåºå» int undecidableCount = 0; for (int index = 0; index < items.size(); index++) { InspectionItem item = items.get(index); if (!itemRules.isEmpty()) { // é项è§åå¨ãå项ä¸ä¸æãéæ±å¼ï¼item æåå½åè¡ï¼index ä» 1 å¼å§ä¸æ¥ååºå·ä¸è´ Map<String, Object> scoped = context.toScope(); scoped.put("item", ReportContext.itemMap(item)); scoped.put("index", index + 1); boolean passed = true; boolean failed = false; for (QualityRuleDefinition rule : itemRules) { try { if (!RuleEngine.test(rule.expression(), scoped)) { passed = false; } } catch (RuntimeException error) { failed = true; errors.add("第 " + (index + 1) + " 项ã" + orEmpty(item.getItemName()) + "ãçè§åã" + rule.rawLabel() + "ãæ æ³æ§è¡ï¼" + error.getMessage()); } } item.applyResult(failed ? null : (passed ? QualityResult.PASS : QualityResult.FAIL)); continue; } QualityResult byLimit = evaluateByLimit(item); if (byLimit != null) { item.applyResult(byLimit); continue; } QualityResult existing = QualityResult.of(item.getResult()); if (existing == null) { // æ¢æ²¡æè§åã乿²¡æå¤å®ä¾æ®ï¼å¦å®æ åºæ¥ï¼ä¸è¦é»è®¤åæ ¼ item.applyUndecidable(); undecidableCount++; errors.add("第 " + (index + 1) + " 项ã" + (isBlank(item.getItemName()) ? "æªå½å" : item.getItemName()) + "ãæ²¡æè§æ ¼ä¸ä¸é乿²¡æå¤å®è§åï¼æ æ³å¤å®"); continue; } item.applyResult(existing); } int passCount = 0; int failCount = 0; for (InspectionItem item : items) { if (QualityResult.PASS.name().equals(item.getResult())) { passCount++; } else if (QualityResult.FAIL.name().equals(item.getResult())) { failCount++; } } int total = items.size(); // åæ ¼çç忝åªç®ãè½å¤å®ãçé¡¹ï¼æ å¤å®è§åç项æ¢ä¸å åä¹ä¸åå int decidableCount = total - undecidableCount; QualityResult verdict = null; if (!reportRules.isEmpty()) { Map<String, Object> scoped = context.scope(items); scoped.put("index", 0); boolean passed = true; boolean failed = false; for (QualityRuleDefinition rule : reportRules) { try { if (!RuleEngine.test(rule.expression(), scoped)) { passed = false; } } catch (RuntimeException error) { failed = true; errors.add("æ¥å级è§åã" + rule.rawLabel() + "ãæ æ³æ§è¡ï¼" + error.getMessage()); } } verdict = failed ? null : (passed ? QualityResult.PASS : QualityResult.FAIL); } else if (decidableCount > 0) { // åªæãå ¨é¨å¯å¤å®é¡¹é½åæ ¼ãæç»åæ ¼ç»è®ºãæ å¤å®è§åç项ä¸åä¸ï¼å®ä»¬æ²¡æå¯¹éå¯è¨ã // åå¨å¾ å¤å®é¡¹ï¼è§åè·æï¼æ¶ä¸ç»ç»è®ºï¼æ²¡éªè¿ä¸è½è¯´åæ ¼ï¼æ²¡è¯æ®ä¹ä¸è½è¯´ä¸åæ ¼ï¼ // å¦åä¼åºç°ãç»è®ºåæ ¼ãåæ ¼ç 0%ãè¿ç§èªç¸çç¾çæ¥åã if (failCount > 0) { verdict = QualityResult.FAIL; } else if (passCount == decidableCount) { verdict = QualityResult.PASS; } } ReportFields report = context.getReport().copy() .setTotal(total) .setPassCount(passCount) .setFailCount(failCount) .setPassRate(formatPassRate(passCount, decidableCount)) .applyResult(verdict); return new EvaluationOutcome(new ReportContext().setReport(report).setInspectionItems(items), errors); } /** ç¨è§æ ¼ä¸ä¸éå¤å®å项ï¼å®æµå¼ä¸å¨åºé´å å³ä¸åæ ¼ */ private static QualityResult evaluateByLimit(InspectionItem item) { double actual = Paths.toNumber(item.getActualValue()); if (!Double.isFinite(actual)) { return null; } boolean hasUpper = item.getUpperLimit() != null && Double.isFinite(item.getUpperLimit()); boolean hasLower = item.getLowerLimit() != null && Double.isFinite(item.getLowerLimit()); if (!hasUpper && !hasLower) { return null; } if (hasUpper && actual > item.getUpperLimit()) { return QualityResult.FAIL; } if (hasLower && actual < item.getLowerLimit()) { return QualityResult.FAIL; } return QualityResult.PASS; } private static String formatPassRate(int passCount, int total) { if (total == 0) { return ""; } return String.format(Locale.ROOT, "%.2f%%", (double) passCount / total * 100); } private static String orEmpty(String value) { return value == null ? "" : value; } private static boolean isBlank(String value) { return value == null || value.isEmpty(); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/rule/QualityRuleDefinition.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,79 @@ package cn.iocoder.yudao.module.qcreport.engine.rule; import java.util.Map; /** * å¤å®è§åå®ä¹ã * <p> * ä¸å端 {@code engine/report-evaluator.ts} ç QualityRuleDefinition ä¸ä¸å¯¹åºï¼ * ä¹å°±æ¯æ¨¡æ¿ Schema ç {@code rules} æ°ç»éæ¯ä¸é¡¹çå¥çº¦ï¼ * {@code { id, name, scope: 'item' | 'report', expression, enabled }}ã * * @param id è§åæ è¯ï¼ç¨äºåºéæ¶å®ä½ * @param name è§ååç§° * @param scope ä½ç¨åï¼é项å¤å® / æ¥åçº§æ±æ» * @param expression å¤å®è¡¨è¾¾å¼ï¼æ±å¼ä¸ºçè§ä¸ºåæ ¼ * @param enabled æ¯å¦åä¸å¤å®ï¼null çå true */ public record QualityRuleDefinition(String id, String name, RuleScope scope, String expression, Boolean enabled) { /** æ¯å¦åä¸å¤å®ï¼ä¸å端 {@code rule.enabled !== false} åè¯ä¹ */ public boolean active() { return enabled == null || enabled; } /** åºéæç¤ºéç¨çåå§åï¼ä¸¤è é½ç¼ºæ¶ä¸ºç©ºä¸²ï¼ä¸å端 {@code rule.name ?? rule.id ?? ''} ä¸è´ï¼ */ public String rawLabel() { if (name != null) { return name; } return id == null ? "" : id; } /** åºéæç¤ºéç¨çè§åå */ public String label() { String trimmed = name == null ? "" : name.trim(); if (!trimmed.isEmpty()) { return trimmed; } return id == null || id.isEmpty() ? "æªå½åè§å" : id; } /** * ä» Schema ç rules 项解æï¼ç¼ºå段çæé»è®¤å¼å åºã * <p> * åªååå¼ä¸åæ ¡éªï¼Schema æ¯è®¾è®¡å¨åçï¼æ ¼å¼é®é¢ä¼å¨æ§è¡æ¶æ¥åºæ¥ï¼ * è¯»çæ¶ååæä¸æ¬¡å¼å¸¸åªä¼æãä¸å¤åæ°æ®ãæ¾å¤§æãæ´ä»½æ¨¡æ¿æä¸å¼ãã */ public static QualityRuleDefinition from(Map<?, ?> raw) { if (raw == null) { return null; } return new QualityRuleDefinition( text(raw.get("id")), text(raw.get("name")), RuleScope.of(text(raw.get("scope"))), text(raw.get("expression")), flag(raw.get("enabled"))); } private static String text(Object value) { return value == null ? null : String.valueOf(value); } /** åªææç¡®ç false / "false" / 0 å½ä½åç¨ï¼ç¼ºçè§ä¸ºå¯ç¨ */ private static Boolean flag(Object value) { if (value instanceof Boolean bool) { return bool; } if (value instanceof Number number) { return number.intValue() != 0; } if (value instanceof String text) { return !"false".equalsIgnoreCase(text.trim()); } return null; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/rule/RuleEngine.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,101 @@ package cn.iocoder.yudao.module.qcreport.engine.rule; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * å¤å®è§åå¼æå ¥å£ã * <p> * è§åææ¬æ¥èªç¨æ·ï¼å ¨ç¨åªç¨èªå»ºçè¯æ³/è¯æ³åæä¸ç½åå彿°æ±å¼ï¼ * ä¸ç¢° evalãä¸ç¢°èæ¬å¼æââè¿æ¯ç¡¬çº¦æï¼ä¸æ¯é£æ ¼å好ã * <p> * ä¸å端 {@code engine/rule-engine.ts} ä¸ä¸å¯¹åºï¼åä¸ä»½è§åä¸¤ç«¯å¿ é¡»ç®åºåä¸ç»æã */ public final class RuleEngine { /** 已解æçè¯æ³æ ç¼åï¼å䏿¡è§åè¦å¨å¤è¡æ£éªé¡¹ä¸é夿±å¼ï¼ä¸å¿ åå¤è§£æ */ private static final Map<String, RuleNode> CACHE = new ConcurrentHashMap<>(); /** * ç¼åæ¡æ°ä¸éã * <p> * è§åæ¥èªä¿å䏿¥ç模æ¿ï¼æ¡æ°å¤©ç¶æéï¼ä½æ ¡éªæ¥å£ä¼è¢«åå¤è°ç¨ï¼ * 没æä¸éçéæç¼åçäºæç¨æ·è¾å ¥æå¨å éï¼è¿éå°éå°±ä¸åå¾éæ¾ã */ private static final int MAX_CACHE_SIZE = 512; private RuleEngine() { } /** è§£æè§åææ¬ï¼è¯æ³é误æ {@link RuleSyntaxException} */ public static RuleNode parse(String expression) { return RuleParser.parse(expression); } /** è§£æå¹¶ç¼åï¼ä¾æ¸²ææ¶å夿±å¼ä½¿ç¨ */ public static RuleNode compiled(String expression) { if (expression == null) { throw new RuleSyntaxException("è§å表达å¼ä¸ºç©º", 0); } RuleNode cached = CACHE.get(expression); if (cached != null) { return cached; } RuleNode node = RuleParser.parse(expression); if (CACHE.size() < MAX_CACHE_SIZE) { CACHE.put(expression, node); } return node; } /** * æ ¡éªè§åææ¬çè¯æ³ï¼è¿åéè¯¯æ¸ åï¼ç©ºå表表示éè¿ã * <p> * è¿é<b>åªæ¥è¯æ³</b>ï¼å½æ°ç½ååäº¤ç» {@link RuleEvaluator#findUnknownFunction}ï¼ * ç± {@link cn.iocoder.yudao.module.qcreport.engine.QualityReportEngine#validateRules} * æä¸¤è æ¼æå®æ´çä¿ååæ ¡éªã * å端æè¿ä¸¤æ¥åå¨äº {@code validateRule} ä¸ä¸ªå½æ°éï¼ä½æ¥éææ¡ä¸¤ç«¯éåä¸è´ââ * å£å¾ä¸ä¸è´ä¼åæã设计å¨è½åãå端åä¸äºãè¿ç§äºç¸ææ¶çåºé¢ã */ public static List<String> validate(QualityRuleDefinition rule) { List<String> errors = new ArrayList<>(); String label = rule.label(); String expression = rule.expression(); if (expression == null || expression.trim().isEmpty()) { errors.add("è§åã" + label + "ãç表达å¼ä¸ºç©º"); return errors; } try { RuleParser.parse(expression); } catch (RuleSyntaxException error) { errors.add("è§åã" + label + "ãè¯æ³é误ï¼" + error.getMessage() + "ï¼ç¬¬ " + (error.position() + 1) + " 个å符ï¼"); } catch (RuntimeException error) { errors.add("è§åã" + label + "ãæ æ³è§£æï¼" + error.getMessage()); } return errors; } /** æ±å¼ï¼è¿ååå§ç»æï¼å¯è½æ¯æ°åãå符串ãå¸å°ï¼ */ public static Object evaluate(String expression, Object context) { return RuleEvaluator.evaluateNode(parse(expression), context); } /** æ±å¼å¹¶è½¬æå¸å°å¤å®ï¼ç¨äºãæ¯å¦åæ ¼ãè¿ç±»æ¡ä»¶ */ public static boolean test(String expression, Object context) { return RuleEvaluator.evaluateCondition(parse(expression), context); } /** éææ£æ¥è¡¨è¾¾å¼éè°ç¨ç彿°æ¯å¦å¨ç½ååå ï¼ä¸å¨åè¿å彿°å */ public static String findUnknownFunction(String expression) { return RuleEvaluator.findUnknownFunction(parse(expression)); } /** 表达å¼ä¾èµçä¸ä¸æè·¯å¾ï¼æ¸²æå坿®æ¤æ£æ¥æ°æ®æ¯å¦é½å¤ */ public static List<String> listPaths(String expression) { return RuleEvaluator.listPaths(parse(expression)); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/rule/RuleEvaluator.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,415 @@ package cn.iocoder.yudao.module.qcreport.engine.rule; import cn.iocoder.yudao.module.qcreport.engine.JsValues; import cn.iocoder.yudao.module.qcreport.engine.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; /** * è§åæ±å¼ã * <p> * åªè®¤ {@link RuleNode} ä¸ç½åå彿°ï¼è§åææ¬éçä»»ä½å 容é½ä¸ä¼åæå¯æ§è¡ä»£ç ã * ä¸å端 {@code engine/rule-engine.ts} çæ±å¼æ®µä¸ä¸å¯¹åºï¼ä¸¤è¾¹ç»æå¿ é¡»ä¸è´ã */ public final class RuleEvaluator { /** å¤å®ç»æç±»åå¼ç»ä¸æ PASS / FAILï¼å ¼å®¹ä¸æãå¸å°ä¸æ°å 1/0 */ private static final Set<String> PASS_TOKENS = Set.of("PASS", "TRUE", "åæ ¼", "OK", "1", "æ¯"); private static final Set<String> FAIL_TOKENS = Set.of("FAIL", "FALSE", "ä¸åæ ¼", "NG", "0", "å¦"); private RuleEvaluator() { } /* ------------------------------ åºç¡å¤å® ------------------------------ */ /** * JS {@code String(value)} ççä»·å®ç°ã * <p> * è§åéçææ¬æ¯è¾ä¸æ¼æ¥é½è¦èµ°è¿éï¼å¦å两端ä¼ç®åºä¸åç»æã */ public static String asText(Object value) { return JsValues.asText(value); } /** 空å¼ï¼null æç©ºä¸²ï¼ç©ºæ°ç»ä¸ç®ç©ºï¼ */ public static boolean isBlank(Object value) { return value == null || "".equals(value); } /** çå¼å¤å®ï¼ä¸ JS ç truthy åè¯ä¹ */ public static boolean truthy(Object value) { if (value instanceof List<?> list) { return !list.isEmpty(); } if (isBlank(value)) { return false; } if (value instanceof Boolean bool) { return bool; } double numeric = Paths.toNumber(value); if (Double.isFinite(numeric)) { return numeric != 0; } return true; } /** 两侧é½è½å½æ°åæ¶ææ°å¼æ¯è¾ï¼å¦åæå符串æ¯è¾ */ public static int compare(Object left, Object right) { double leftNumber = Paths.toNumber(left); double rightNumber = Paths.toNumber(right); if (Double.isFinite(leftNumber) && Double.isFinite(rightNumber)) { return Double.compare(leftNumber, rightNumber); } return asText(left).compareTo(asText(right)); } /** å¤å®ç»æç±»åå¼ç»ä¸æ PASS / FAILï¼è¯å«ä¸åºè¿å空串 */ public static String normalizeResultToken(Object value) { if (value instanceof Boolean bool) { return bool ? "PASS" : "FAIL"; } String text = asText(value).trim().toUpperCase(Locale.ROOT); if (PASS_TOKENS.contains(text)) { return "PASS"; } return FAIL_TOKENS.contains(text) ? "FAIL" : ""; } /* ------------------------------ ç½åå彿° ------------------------------ */ /** ç½åå彿°è¡¨ï¼è§åææ¬åªè½è°ç¨è¿éé¢ç彿° */ private static final Map<String, RuleFunction> FUNCTIONS = buildFunctions(); private static Map<String, RuleFunction> buildFunctions() { Map<String, RuleFunction> functions = new LinkedHashMap<>(); functions.put("ABS", args -> Math.abs(requireNumber("ABS", arg(args, 0)))); functions.put("AVG", args -> mean(requireNumbers("AVG", arg(args, 0)))); functions.put("COUNT", args -> arg(args, 0) instanceof List<?> list ? list.size() : Paths.toNumberArray(arg(args, 0)).size()); // è¿ç¨è½åææ° CP=(USL-LSL)/(6Ï) functions.put("CP", args -> { List<Double> values = requireNumbers("CP", arg(args, 0)); double deviation = standardDeviation(values); if (!Double.isFinite(deviation) || deviation == 0) { return Double.NaN; } return (requireNumber("CP", arg(args, 1)) - requireNumber("CP", arg(args, 2))) / (6 * deviation); }); // è¿ç¨è½åææ° CPK=min(USL-μ, μ-LSL)/(3Ï) functions.put("CPK", args -> { List<Double> values = requireNumbers("CPK", arg(args, 0)); double deviation = standardDeviation(values); if (!Double.isFinite(deviation) || deviation == 0) { return Double.NaN; } double upper = requireNumber("CPK", arg(args, 1)); double lower = requireNumber("CPK", arg(args, 2)); double average = mean(values); return Math.min(upper - average, average - lower) / (3 * deviation); }); // ä¸åæ ¼çï¼ç¾åæ¯ï¼ï¼å£å¾ä¸ PASS_RATE ä¸è´ functions.put("FAIL_RATE", args -> rate(arg(args, 0), "FAIL")); functions.put("MAX", args -> extreme(requireNumbers("MAX", arg(args, 0)), true)); functions.put("MIN", args -> extreme(requireNumbers("MIN", arg(args, 0)), false)); // åæ ¼çï¼ç¾åæ¯ï¼ä¿ç两ä½ï¼ functions.put("PASS_RATE", args -> rate(arg(args, 0), "PASS")); functions.put("ROUND", args -> { double digits = args.size() > 1 ? requireNumber("ROUND", arg(args, 1)) : 0; double factor = Math.pow(10, digits); return Math.round(requireNumber("ROUND", arg(args, 0)) * factor) / factor; }); functions.put("STDDEV", args -> standardDeviation(requireNumbers("STDDEV", arg(args, 0)))); functions.put("SUM", args -> { double sum = 0; for (Double value : requireNumbers("SUM", arg(args, 0))) { sum += value; } return sum; }); // ä¸è½æ¢æ Map.copyOfï¼å®ä¸ä¿è¯éå顺åºï¼æ¥ééååºçå¯ç¨å½æ°ä¼éæºæ return Collections.unmodifiableMap(functions); } /** * å第 index 个å®åï¼ç¼ºåè¿å nullã * <p> * å°äºè¿ä¸ªå åºï¼{@code SUM()} è¿ç§åéçè§åä¼ç´æ¥æ IndexOutOfBoundsException ç©¿éå°æ¥å£å±ï¼ * ç¨æ·çå°çæ¯ãç³»ç»å¼å¸¸ãè䏿¯ãè¿æ¡è§ååéäºãã */ private static Object arg(List<Object> args, int index) { return index < args.size() ? args.get(index) : null; } /** ç½åå彿°åï¼é¡ºåºç¨³å®ï¼ç¨äºæ¥éæ¶ååºå¯ç¨å½æ° */ public static List<String> functionNames() { return List.copyOf(FUNCTIONS.keySet()); } /** 䏿¬¡æ§æ±å¼å ¨é¨åæ ¼/ä¸åæ ¼å æ¯ï¼å£å¾ä¸å端ä¸è´ */ private static double rate(Object value, String expected) { List<Object> values = Paths.flatten(value); if (values.isEmpty()) { return Double.NaN; } int hit = 0; for (Object element : values) { if (expected.equals(normalizeResultToken(element))) { hit++; } } return toFixed2((double) hit / values.size() * 100); } /** JS toFixed(2) ååååæ°å¼ï¼ä¿ç两ä½çç¾åæ¯ */ private static double toFixed2(double value) { return Math.round(value * 100) / 100.0; } private static double extreme(List<Double> values, boolean max) { double result = values.get(0); for (Double value : values) { result = max ? Math.max(result, value) : Math.min(result, value); } return result; } private static double mean(List<Double> values) { double sum = 0; for (Double value : values) { sum += value; } return sum / values.size(); } /** æ ·æ¬æ åå·®ï¼n-1ï¼ï¼SPC è®¡ç® CP/CPK ç¨è¿ä¸ªå£å¾ */ private static double standardDeviation(List<Double> values) { if (values.size() < 2) { return Double.NaN; } double average = mean(values); double variance = 0; for (Double value : values) { variance += Math.pow(value - average, 2); } return Math.sqrt(variance / (values.size() - 1)); } private static List<Double> requireNumbers(String name, Object value) { List<Double> values = Paths.toNumberArray(value); if (values.isEmpty()) { throw new RuleRuntimeException("彿° " + name + " éè¦æ°å¼ååæ°ï¼å®é åå°ã" + asText(value) + "ã"); } return values; } private static double requireNumber(String name, Object value) { double numeric = Paths.toNumber(value); if (!Double.isFinite(numeric)) { throw new RuleRuntimeException("彿° " + name + " éè¦æ°ååæ°ï¼å®é åå°ã" + asText(value) + "ã"); } return numeric; } /* ------------------------------ æ±å¼ ------------------------------ */ /** æ±å¼ï¼è¿ååå§ç»æï¼å¯è½æ¯æ°åãå符串ãå¸å°ï¼ */ public static Object evaluateNode(RuleNode node, Object context) { return switch (node) { case RuleNode.Literal literal -> literal.value(); case RuleNode.Path path -> Paths.readPath(context, path.path()); case RuleNode.Call call -> evaluateCall(call, context); case RuleNode.Unary unary -> evaluateUnary(unary, context); case RuleNode.Between between -> evaluateBetween(between, context); case RuleNode.In inNode -> evaluateIn(inNode, context); case RuleNode.Binary binary -> evaluateBinary(binary, context); }; } /** æ±å¼å¹¶è½¬æå¸å°å¤å®ï¼ç¨äºãæ¯å¦åæ ¼ãè¿ç±»æ¡ä»¶ */ public static boolean evaluateCondition(RuleNode node, Object context) { return truthy(evaluateNode(node, context)); } private static Object evaluateCall(RuleNode.Call call, Object context) { RuleFunction handler = FUNCTIONS.get(call.name()); if (handler == null) { throw new RuleRuntimeException( "䏿¯æå½æ° " + call.name() + "ï¼å¯ç¨å½æ°ï¼" + String.join("ã", FUNCTIONS.keySet())); } List<Object> args = new ArrayList<>(call.args().size()); for (RuleNode arg : call.args()) { args.add(evaluateNode(arg, context)); } return handler.apply(args); } private static Object evaluateUnary(RuleNode.Unary unary, Object context) { Object value = evaluateNode(unary.operand(), context); return "!".equals(unary.operator()) ? !truthy(value) : -Paths.toNumber(value); } private static Object evaluateBetween(RuleNode.Between between, Object context) { Object value = evaluateNode(between.value(), context); boolean hit = compare(value, evaluateNode(between.lower(), context)) >= 0 && compare(value, evaluateNode(between.upper(), context)) <= 0; return between.negated() != hit; } private static Object evaluateIn(RuleNode.In inNode, Object context) { Object value = evaluateNode(inNode.value(), context); boolean hit = false; for (RuleNode item : inNode.items()) { if (compare(value, evaluateNode(item, context)) == 0) { hit = true; break; } } return inNode.negated() != hit; } private static Object evaluateBinary(RuleNode.Binary binary, Object context) { if ("AND".equals(binary.operator())) { return truthy(evaluateNode(binary.left(), context)) && truthy(evaluateNode(binary.right(), context)); } if ("OR".equals(binary.operator())) { return truthy(evaluateNode(binary.left(), context)) || truthy(evaluateNode(binary.right(), context)); } Object left = evaluateNode(binary.left(), context); Object right = evaluateNode(binary.right(), context); return switch (binary.operator()) { case "=" -> compare(left, right) == 0; case "!=" -> compare(left, right) != 0; case ">" -> compare(left, right) > 0; case ">=" -> compare(left, right) >= 0; case "<" -> compare(left, right) < 0; case "<=" -> compare(left, right) <= 0; case "+" -> add(left, right); case "-" -> Paths.toNumber(left) - Paths.toNumber(right); case "*" -> Paths.toNumber(left) * Paths.toNumber(right); case "/" -> divide(left, right); default -> throw new RuleRuntimeException("䏿¯æçè¿ç®ç¬¦ " + binary.operator()); }; } /** 䏤侧齿¯æ°å¼æåå æ³ï¼å¦åæææ¬æ¼æ¥ï¼ç¼å·ç±»å段常ç¨ï¼ */ private static Object add(Object left, Object right) { double leftNumber = Paths.toNumber(left); double rightNumber = Paths.toNumber(right); if (Double.isFinite(leftNumber) && Double.isFinite(rightNumber)) { return leftNumber + rightNumber; } return asText(left) + asText(right); } private static Object divide(Object left, Object right) { double divisor = Paths.toNumber(right); if (divisor == 0) { throw new RuleRuntimeException("è§åéåºç°äºé¤ä»¥ 0"); } return Paths.toNumber(left) / divisor; } /* ------------------------------ éæåæ ------------------------------ */ /** 表达å¼éè°ç¨ç彿°åï¼å«åµå¥ï¼ï¼ç¨äºä¿ååæ ¡éªç½åå */ public static List<String> listFunctionNames(RuleNode node) { List<String> names = new ArrayList<>(); collectFunctionNames(node, names); return names; } private static void collectFunctionNames(RuleNode node, List<String> names) { switch (node) { case RuleNode.Call call -> { names.add(call.name()); call.args().forEach(arg -> collectFunctionNames(arg, names)); } case RuleNode.Unary unary -> collectFunctionNames(unary.operand(), names); case RuleNode.Binary binary -> { collectFunctionNames(binary.left(), names); collectFunctionNames(binary.right(), names); } case RuleNode.Between between -> { collectFunctionNames(between.value(), names); collectFunctionNames(between.lower(), names); collectFunctionNames(between.upper(), names); } case RuleNode.In inNode -> { collectFunctionNames(inNode.value(), names); inNode.items().forEach(item -> collectFunctionNames(item, names)); } default -> { } } } /** æªç¥å½æ°åè¿å nullï¼ä¾è°ç¨æ¹ç»åºå¯è¯»æ¥é */ public static String findUnknownFunction(RuleNode node) { for (String name : listFunctionNames(node)) { if (!FUNCTIONS.containsKey(name)) { return name; } } return null; } /** * è§£æåçè·¯å¾ä¾èµï¼æ¸²æå坿®æ¤æ£æ¥æ°æ®æ¯å¦é½å¤ã * <p> * ç»è®¡å½æ°ä½ç¨å¨æ°ç»è·¯å¾ä¸ï¼è¿é廿æ«ç«¯å段åªä¿çæ°ç»æ¬èº«ã */ public static List<String> listPaths(RuleNode node) { Set<String> paths = new LinkedHashSet<>(); collectPaths(node, paths); Set<String> normalized = new LinkedHashSet<>(); for (String path : paths) { normalized.add(String.join(".", Paths.parsePath(path))); } return List.copyOf(normalized); } private static void collectPaths(RuleNode node, Set<String> paths) { switch (node) { case RuleNode.Path path -> { paths.add(path.path()); } case RuleNode.Call call -> call.args().forEach(arg -> collectPaths(arg, paths)); case RuleNode.Unary unary -> collectPaths(unary.operand(), paths); case RuleNode.Binary binary -> { collectPaths(binary.left(), paths); collectPaths(binary.right(), paths); } case RuleNode.Between between -> { collectPaths(between.value(), paths); collectPaths(between.lower(), paths); collectPaths(between.upper(), paths); } case RuleNode.In inNode -> { collectPaths(inNode.value(), paths); inNode.items().forEach(item -> collectPaths(item, paths)); } default -> { } } } /** ç½åå彿° */ @FunctionalInterface public interface RuleFunction { Object apply(List<Object> args); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/rule/RuleNode.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,42 @@ package cn.iocoder.yudao.module.qcreport.engine.rule; import java.util.List; /** * è§åè¯æ³æ èç¹ã * <p> * ä¸å端 {@code engine/rule-engine.ts} ç RuleNode èåç±»åä¸ä¸å¯¹åºï¼ * éå½ä¸éè§£ææè¿æ£µæ åèªå·±æ±å¼ï¼å ¨ç¨ä¸ç¢° eval / èæ¬å¼æââ * è§åææ¬æ¥èªç¨æ·ï¼ä¸æ¦è½è¢«å½ä½ä»£ç æ§è¡å°±æ¯æ³¨å ¥å£åã */ public sealed interface RuleNode { /** åé¢éï¼æ°åï¼Doubleï¼/ å符串 / å¸å° / null */ record Literal(Object value) implements RuleNode { } /** ä¸ä¸æè·¯å¾åå¼ï¼å¦ item.actualValue */ record Path(String path) implements RuleNode { } /** ç½åå彿°è°ç¨ */ record Call(String name, List<RuleNode> args) implements RuleNode { } /** ä¸å è¿ç®ï¼- æ ! */ record Unary(String operator, RuleNode operand) implements RuleNode { } /** äºå è¿ç®ï¼AND / OR / = / != / > / >= / < / <= / + / - / * / / */ record Binary(String operator, RuleNode left, RuleNode right) implements RuleNode { } /** value BETWEEN lower AND upper */ record Between(RuleNode value, RuleNode lower, RuleNode upper, boolean negated) implements RuleNode { } /** value IN (items) / value NOT IN (items) */ record In(RuleNode value, List<RuleNode> items, boolean negated) implements RuleNode { } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/rule/RuleParser.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,372 @@ package cn.iocoder.yudao.module.qcreport.engine.rule; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.regex.Pattern; /** * è§åææ¬ â è¯æ³æ ã * <p> * ä¸å端 {@code engine/rule-engine.ts} ç tokenize + Parser ä¸ä¸å¯¹åºï¼ * æ¯æçè¯æ³è§ docs/æºè½è´¨æ£æ¥åå¹³å°-æ¹æ¡è®¾è®¡.md §13ã */ public final class RuleParser { private static final Set<String> KEYWORDS = Set.of( "AND", "BETWEEN", "FALSE", "IN", "NOT", "NOT_IN", "NULL", "OR", "TRUE"); private static final Set<String> TWO_CHAR_OPERATORS = Set.of("<=", ">=", "!=", "<>", "=="); private static final String SINGLE_CHAR_OPERATORS = "><=+-*/!"; private static final Pattern NUMBER_TEXT = Pattern.compile("^\\d+(?:\\.\\d+)?$"); private static final Pattern DIGIT_TEXT = Pattern.compile("^\\d+$"); private RuleParser() { } /** è§£æè§åææ¬ï¼è¯æ³é误æ {@link RuleSyntaxException} */ public static RuleNode parse(String expression) { String trimmed = expression == null ? "" : expression.trim(); if (trimmed.isEmpty()) { throw new RuleSyntaxException("è§å表达å¼ä¸ºç©º", 0); } return new Parser(tokenize(trimmed)).parse(); } /* ------------------------------ è¯æ³åæ ------------------------------ */ private static List<RuleToken> tokenize(String source) { List<RuleToken> tokens = new ArrayList<>(); int length = source.length(); int index = 0; while (index < length) { char ch = source.charAt(index); if (Character.isWhitespace(ch)) { index++; continue; } if (ch == '\'' || ch == '"') { index = readString(source, index, ch, tokens); continue; } if (ch >= '0' && ch <= '9') { int cursor = index; while (cursor < length && isDigitOrDot(source.charAt(cursor))) { cursor++; } String text = source.substring(index, cursor); if (!NUMBER_TEXT.matcher(text).matches()) { throw new RuleSyntaxException("æ°åæ ¼å¼ä¸æ£ç¡®ï¼" + text, index); } tokens.add(new RuleToken(TokenType.NUMBER, text, index)); index = cursor; continue; } if (isIdentStart(ch)) { index = readWord(source, index, tokens); continue; } if (index + 2 <= length && TWO_CHAR_OPERATORS.contains(source.substring(index, index + 2))) { tokens.add(new RuleToken(TokenType.OPERATOR, source.substring(index, index + 2), index)); index += 2; continue; } if (ch == '(' || ch == ')') { tokens.add(new RuleToken(ch == '(' ? TokenType.LPAREN : TokenType.RPAREN, String.valueOf(ch), index)); index++; continue; } if (ch == ',') { tokens.add(new RuleToken(TokenType.COMMA, String.valueOf(ch), index)); index++; continue; } if (SINGLE_CHAR_OPERATORS.indexOf(ch) >= 0) { tokens.add(new RuleToken(TokenType.OPERATOR, String.valueOf(ch), index)); index++; continue; } throw new RuleSyntaxException("æ æ³è¯å«çå符ã" + ch + "ã", index); } tokens.add(new RuleToken(TokenType.EOF, "", length)); return tokens; } /** 读ä¸ä¸ªå符串åé¢éï¼è¿åç»æåç䏿 */ private static int readString(String source, int start, char quote, List<RuleToken> tokens) { int length = source.length(); StringBuilder value = new StringBuilder(); int cursor = start + 1; while (cursor < length && source.charAt(cursor) != quote) { if (source.charAt(cursor) == '\\' && cursor + 1 < length) { value.append(source.charAt(cursor + 1)); cursor += 2; continue; } value.append(source.charAt(cursor)); cursor++; } if (cursor >= length) { throw new RuleSyntaxException("å符串缺å°ç»å°¾å¼å·", start); } tokens.add(new RuleToken(TokenType.STRING, value.toString(), start)); return cursor + 1; } /** * 读ä¸ä¸ªæ è¯ç¬¦/è·¯å¾ï¼è¿åç»æåç䏿 ã * <p> * è·¯å¾å°¾å·´ï¼.屿§ ä¸ [䏿 ]ï¼å¨è¯æ³é¶æ®µå°±å¹¶è¿åä¸ä¸ªè¯ï¼å弿¶æ´æ¡è·¯å¾ä¸æ¬¡è§£æã */ private static int readWord(String source, int start, List<RuleToken> tokens) { int length = source.length(); int cursor = start; while (cursor < length && isIdentPart(source.charAt(cursor))) { cursor++; } StringBuilder value = new StringBuilder(source.substring(start, cursor)); while (cursor < length) { char next = source.charAt(cursor); if (next == '.' && cursor + 1 < length && isIdentStart(source.charAt(cursor + 1))) { int end = cursor + 1; while (end < length && isIdentPart(source.charAt(end))) { end++; } value.append(source, cursor, end); cursor = end; continue; } if (next == '[') { int close = source.indexOf(']', cursor); if (close == -1) { throw new RuleSyntaxException("æ°ç»ä¸æ ç¼ºå° ]", cursor); } String inner = source.substring(cursor + 1, close).trim(); if (!DIGIT_TEXT.matcher(inner).matches()) { throw new RuleSyntaxException("æ°ç»ä¸æ åªè½æ¯æ°åï¼" + inner, cursor); } value.append('[').append(inner).append(']'); cursor = close + 1; continue; } break; } tokens.add(new RuleToken(TokenType.WORD, value.toString(), start)); return cursor; } private static boolean isIdentStart(char ch) { return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch == '_' || ch == '$'; } private static boolean isIdentPart(char ch) { return isIdentStart(ch) || ch >= '0' && ch <= '9'; } private static boolean isDigitOrDot(char ch) { return ch >= '0' && ch <= '9' || ch == '.'; } /* ------------------------------ è¯æ³åæ ------------------------------ */ private static final class Parser { private final List<RuleToken> tokens; private int cursor; private Parser(List<RuleToken> tokens) { this.tokens = tokens; } private RuleNode parse() { RuleNode node = parseOr(); RuleToken token = peek(); if (token.type() != TokenType.EOF) { throw new RuleSyntaxException("å¤ä½çå 容ï¼" + token.value(), token.position()); } return node; } private RuleNode parseOr() { RuleNode left = parseAnd(); while (matchWord("OR")) { left = new RuleNode.Binary("OR", left, parseAnd()); } return left; } private RuleNode parseAnd() { RuleNode left = parseComparison(); while (matchWord("AND")) { left = new RuleNode.Binary("AND", left, parseComparison()); } return left; } private RuleNode parseComparison() { RuleNode left = parseAdditive(); if (matchWord("BETWEEN")) { RuleNode lower = parseAdditive(); if (!matchWord("AND")) { throw new RuleSyntaxException("BETWEEN ç¼ºå° AND ä¸ä¸ç", peek().position()); } RuleNode upper = parseAdditive(); return new RuleNode.Between(left, lower, upper, false); } if (matchWord("NOT")) { if (matchWord("IN") || matchWord("NOT_IN")) { return new RuleNode.In(left, parseList(), true); } throw new RuleSyntaxException("NOT åªè½ç¨äº NOT IN", peek().position()); } if (matchWord("IN")) { return new RuleNode.In(left, parseList(), false); } RuleToken token = peek(); if (token.type() == TokenType.OPERATOR && "< <= = == != <> > >=".contains(token.value())) { cursor++; return new RuleNode.Binary(normalizeOperator(token.value()), left, parseAdditive()); } return left; } /** == ç»ä¸æ =ã<> ç»ä¸æ !=ï¼æ±å¼é¶æ®µåªè®¤ä¸ç§åæ³ */ private static String normalizeOperator(String operator) { if ("==".equals(operator)) { return "="; } return "<>".equals(operator) ? "!=" : operator; } private List<RuleNode> parseList() { expect(TokenType.LPAREN); List<RuleNode> items = new ArrayList<>(); if (peek().type() != TokenType.RPAREN) { items.add(parseOr()); while (peek().type() == TokenType.COMMA) { cursor++; items.add(parseOr()); } } expect(TokenType.RPAREN); return items; } private RuleNode parseAdditive() { RuleNode left = parseMultiplicative(); for (; ; ) { RuleToken token = peek(); if (token.type() != TokenType.OPERATOR || !"+".equals(token.value()) && !"-".equals(token.value())) { return left; } cursor++; left = new RuleNode.Binary(token.value(), left, parseMultiplicative()); } } private RuleNode parseMultiplicative() { RuleNode left = parseUnary(); for (; ; ) { RuleToken token = peek(); if (token.type() != TokenType.OPERATOR || !"*".equals(token.value()) && !"/".equals(token.value())) { return left; } cursor++; left = new RuleNode.Binary(token.value(), left, parseUnary()); } } private RuleNode parseUnary() { RuleToken token = peek(); if (token.type() == TokenType.OPERATOR && ("-".equals(token.value()) || "!".equals(token.value()))) { cursor++; return new RuleNode.Unary(token.value(), parseUnary()); } return parsePrimary(); } private RuleNode parsePrimary() { RuleToken token = peek(); if (token.type() == TokenType.NUMBER) { cursor++; return new RuleNode.Literal(Double.valueOf(token.value())); } if (token.type() == TokenType.STRING) { cursor++; return new RuleNode.Literal(token.value()); } if (token.type() == TokenType.LPAREN) { cursor++; RuleNode node = parseOr(); expect(TokenType.RPAREN); return node; } if (token.type() == TokenType.WORD) { cursor++; String upper = token.value().toUpperCase(); if ("TRUE".equals(upper)) { return new RuleNode.Literal(Boolean.TRUE); } if ("FALSE".equals(upper)) { return new RuleNode.Literal(Boolean.FALSE); } if ("NULL".equals(upper)) { return new RuleNode.Literal(null); } if (peek().type() == TokenType.LPAREN) { return new RuleNode.Call(upper, parseList()); } if (KEYWORDS.contains(upper)) { throw new RuleSyntaxException("å ³é®å " + upper + " çä½ç½®ä¸æ£ç¡®", token.position()); } return new RuleNode.Path(token.value()); } throw new RuleSyntaxException( token.type() == TokenType.EOF ? "表达å¼ä¸å®æ´" : "æ æ³è§£æçå 容ï¼" + token.value(), token.position()); } private RuleToken peek() { return tokens.get(cursor); } private boolean matchWord(String word) { RuleToken token = peek(); if (token.type() == TokenType.WORD && token.value().toUpperCase().equals(word)) { cursor++; return true; } return false; } private void expect(TokenType type) { RuleToken token = peek(); if (token.type() != type) { throw new RuleSyntaxException( "åºè¯¥æ¯ " + type.label() + "ï¼å®é æ¯ã" + token.value() + "ã", token.position()); } cursor++; } } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/rule/RuleRuntimeException.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,12 @@ package cn.iocoder.yudao.module.qcreport.engine.rule; /** * è§åè¿è¡æé误ï¼å½æ°ä¸å¨ç½ååãåæ°ç±»åä¸å¯¹ãé¤ä»¥ 0 çã */ public class RuleRuntimeException extends RuntimeException { public RuleRuntimeException(String message) { super(message); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/rule/RuleScope.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,28 @@ package cn.iocoder.yudao.module.qcreport.engine.rule; /** * è§åä½ç¨åã */ public enum RuleScope { /** é项å¤å®ï¼å¯¹æ¯ä¸è¡æ£éªé¡¹å嫿±å¼ */ ITEM("item"), /** æ¥åçº§æ±æ»å¤å® */ REPORT("report"); private final String label; RuleScope(String label) { this.label = label; } public String label() { return label; } /** è½åº/å端åå¼ â æä¸¾ï¼è¯å«ä¸åºæé项å¤ç */ public static RuleScope of(String value) { return REPORT.label.equalsIgnoreCase(value == null ? "" : value.trim()) ? REPORT : ITEM; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/rule/RuleSyntaxException.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,20 @@ package cn.iocoder.yudao.module.qcreport.engine.rule; /** * è§åææ¬åå¨è¯æ³é误ã */ public class RuleSyntaxException extends RuntimeException { /** åºéä½ç½®å¨è§åææ¬ä¸ç䏿 ï¼ä» 0 å¼å§ï¼ */ private final transient int position; public RuleSyntaxException(String message, int position) { super(message); this.position = position; } public int position() { return position; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/rule/RuleToken.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,11 @@ package cn.iocoder.yudao.module.qcreport.engine.rule; /** * è¯æ³åå ã * * @param type ç±»å * @param value åå§ææ¬ï¼åç¬¦ä¸²å·²å»æå¼å·å¹¶è§£è½¬ä¹ï¼ * @param position å¨è§åææ¬ä¸çèµ·å§ä¸æ ï¼åºéæ¶ç¨äºå®ä½ */ public record RuleToken(TokenType type, String value, int position) { } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/engine/rule/TokenType.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,30 @@ package cn.iocoder.yudao.module.qcreport.engine.rule; /** * è¯æ³åå ç±»åã * <p> * label ä¿çå端 {@code engine/rule-engine.ts} çå°ååæ³ï¼ * è¯æ³é误æç¤ºè¦å¨ä¸¤ç«¯éåä¸è´ï¼ç¨æ·æä¸ä¼å¯¹åä¸ä»½è§åçå°ä¸¤ç§è¯´æ³ã */ public enum TokenType { COMMA("comma"), EOF("eof"), LPAREN("lparen"), NUMBER("number"), OPERATOR("operator"), RPAREN("rparen"), STRING("string"), WORD("word"); private final String label; TokenType(String label) { this.label = label; } public String label() { return label; } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/enums/ErrorCodeConstants.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,117 @@ package cn.iocoder.yudao.module.qcreport.enums; import cn.iocoder.yudao.framework.common.exception.ErrorCode; /** * qcreport æºè½è´¨æ£æ¥åè®¾è®¡å¹³å° éè¯¯ç æä¸¾ç±» * <p> * qcreport ç³»ç»ï¼ä½¿ç¨ 1-070-000-000 段 */ public interface ErrorCodeConstants { // ========== æ¥å模æ¿ï¼1-070-100-000ï¼ ========== ErrorCode TEMPLATE_NOT_EXISTS = new ErrorCode(1_070_100_000, "æ¥å模æ¿ä¸åå¨"); ErrorCode TEMPLATE_CODE_DUPLICATE = new ErrorCode(1_070_100_001, "æ¥å模æ¿ç¼ç å·²åå¨"); ErrorCode TEMPLATE_STATUS_DISABLED = new ErrorCode(1_070_100_002, "æ¥å模æ¿å·²åç¨ï¼æ æ³æ§è¡è¯¥æä½"); // ========== 模æ¿çæ¬ï¼1-070-101-000ï¼ ========== ErrorCode TEMPLATE_VERSION_NOT_EXISTS = new ErrorCode(1_070_101_000, "模æ¿çæ¬ä¸åå¨"); ErrorCode TEMPLATE_VERSION_DUPLICATE = new ErrorCode(1_070_101_001, "è¯¥çæ¬å·å·²åå¨ï¼è¯·ä½¿ç¨æ°ççæ¬å·"); ErrorCode TEMPLATE_VERSION_PUBLISHED_CANNOT_MODIFY = new ErrorCode(1_070_101_002, "模æ¿çæ¬å·²åå¸ï¼ä¸å 许修æ¹ï¼è¯·å建æ°çæ¬"); ErrorCode TEMPLATE_VERSION_NOT_PUBLISHED = new ErrorCode(1_070_101_003, "模æ¿çæ¬æªåå¸ï¼æ æ³çææ¥å"); ErrorCode TEMPLATE_VERSION_SCHEMA_INVALID = new ErrorCode(1_070_101_004, "æ¨¡æ¿ Schema æ ¼å¼ä¸æ£ç¡®ï¼è¯·æ£æ¥è®¾è®¡å¨ä¿åçæ¨¡æ¿å 容"); ErrorCode TEMPLATE_VERSION_SCHEMA_EMPTY = new ErrorCode(1_070_101_005, "æ¨¡æ¿ Schema 为空ï¼è¯·å å¨è®¾è®¡å¨ä¸è®¾è®¡æ¨¡æ¿å 容并ä¿åï¼ååå¸çæ¬"); ErrorCode TEMPLATE_VERSION_RULE_INVALID = new ErrorCode(1_070_101_006, "模æ¿éçå¤å®è§åä¸åæ³ï¼è¯·ä¿®æ¹åéè¯"); ErrorCode TEMPLATE_NO_PUBLISHED_VERSION = new ErrorCode(1_070_101_007, "该模æ¿è¿æ²¡æå·²åå¸ççæ¬ï¼æ æ³çææ¥åï¼è¯·å å¨è®¾è®¡å¨ä¸åå¸ä¸ä¸ªçæ¬"); ErrorCode TEMPLATE_VERSION_CANVAS_EMPTY = new ErrorCode(1_070_101_008, "该模æ¿çæ¬æ²¡æç»å¸å å®¹ï¼æ æ³çææ¥åï¼è¯·å å¨è®¾è®¡å¨ä¸è®¾è®¡å¹¶ä¿å模æ¿å 容"); ErrorCode TEMPLATE_VERSION_CANVAS_UNSAFE = new ErrorCode(1_070_101_009, "模æ¿ç»å¸éæä¸å 许åºç°çå 容ï¼å·²æç»ä¿åï¼{}ã请å¨è®¾è®¡å¨ä¸å æè¿äºå 容åéæ°ä¿å"); ErrorCode TEMPLATE_VERSION_CURRENT_CANNOT_DISABLE = new ErrorCode(1_070_101_010, "çæ¬ã{}ãæ¯æ¨¡æ¿ã{}ãå½åæ£å¨ä½¿ç¨ççæ¬ï¼ä¸è½åç¨ã" + "请å å°è¯¥æ¨¡æ¿ççæ¬å表é对å¦ä¸ä¸ªå·²åå¸çæ¬æ§è¡ãåæ»ãï¼æå½åçæ¬åèµ°ï¼ååç¨æ¬çæ¬ï¼" + "è¥è¦è®©æ´ä¸ªæ¨¡æ¿åæ¢ä½¿ç¨ï¼è¯·æ¹ä¸ºåç¨æ¨¡æ¿æ¬èº«"); // ========== æ¥åå®ä¾ï¼1-070-102-000ï¼ ========== ErrorCode REPORT_INSTANCE_NOT_EXISTS = new ErrorCode(1_070_102_000, "æ¥åå®ä¾ä¸åå¨"); ErrorCode REPORT_INSTANCE_NO_DUPLICATE = new ErrorCode(1_070_102_001, "æ¥åç¼å·å·²åå¨"); ErrorCode REPORT_INSTANCE_DATA_SNAPSHOT_MISSING = new ErrorCode(1_070_102_002, "æ¥åå®ä¾ç¼ºå°æ°æ®å¿«ç §ï¼æ æ³éæ°æ¸²æ"); ErrorCode REPORT_INSTANCE_HTML_MISSING = new ErrorCode(1_070_102_003, "æ¥åå®ä¾æ²¡ææ¸²æäº§ç© HTMLï¼æ æ³çæ PDFï¼è¯·å 对该æ¥åæ§è¡ãéæ°çæã"); // ========== 渲æä¸è§åï¼1-070-103-000ï¼ ========== ErrorCode RENDER_BUSINESS_DATA_MISSING = new ErrorCode(1_070_103_000, "ä¸å¡æ°æ®ä¸ºç©ºï¼æ æ³æ¸²ææ¥å"); ErrorCode RENDER_DATA_FIELD_NOT_EXISTS = new ErrorCode(1_070_103_001, "æ°æ®ç»å®å段ä¸åå¨"); ErrorCode RENDER_RULE_PARSE_FAILED = new ErrorCode(1_070_103_002, "å¤å®è§åè§£æå¤±è´¥"); ErrorCode RENDER_HTML_FAILED = new ErrorCode(1_070_103_003, "æ¥å HTML 渲æå¤±è´¥"); ErrorCode RENDER_PDF_BROWSER_LAUNCH_FAILED = new ErrorCode(1_070_103_004, "PDF æ¸²ææµè§å¨å¯å¨å¤±è´¥ï¼è¯·æ£æ¥ Chromium æ¯å¦å·²å®è£ "); ErrorCode RENDER_PDF_FAILED = new ErrorCode(1_070_103_005, "PDF çæå¤±è´¥"); ErrorCode RENDER_PDF_ARCHIVE_FAILED = new ErrorCode(1_070_103_006, "PDF å·²çæï¼ä½å½æ¡£å°éä»¶åºå¤±è´¥"); ErrorCode RENDER_PDF_BUSY = new ErrorCode(1_070_103_007, "å½å PDF 渲æä»»å¡å·²è¾¾ä¸éï¼è¯·ç¨åéè¯"); ErrorCode RENDER_PDF_DISABLED = new ErrorCode(1_070_103_008, "PDF åºä»¶åè½æªå¯ç¨ï¼è¯·è系管çå"); // ========== AI å¯¼å ¥æ¨¡æ¿è稿ï¼1-070-104-000ï¼ ========== ErrorCode AI_IMPORT_DISABLED = new ErrorCode(1_070_104_000, "AI å¯¼å ¥åè½æªå¯ç¨ï¼yudao.qcreport.ai-import.enabled å½å为 falseï¼ï¼è¯·è系管çåå¼å¯ååè¯"); ErrorCode AI_IMPORT_BLOB_NOT_FOUND = new ErrorCode(1_070_104_001, "ä¸ä¼ çæä»¶ï¼blobId={}ï¼ä¸åå¨æå·²è¢«æ¸ çï¼è¯·éæ°ä¸ä¼ ååè¯"); ErrorCode AI_IMPORT_FILE_UNSUPPORTED = new ErrorCode(1_070_104_002, "䏿¯æçæä»¶ã{}ãï¼æ©å±åï¼{}ï¼ãä» æ¯æ PDFãWord(.docx)ãExcel(.xlsx)ã" + "å¾ç(png/jpg/jpeg/bmp/gif/webp) 䏿æ¬(txt/csv)ï¼è¯·è½¬æ¢æ ¼å¼åéæ°å¯¼å ¥"); ErrorCode AI_IMPORT_FILE_LEGACY_OFFICE = new ErrorCode(1_070_104_003, "䏿¯æçæä»¶ã{}ãï¼æ§ç Office æ ¼å¼.{}ï¼ãè¯·ç¨ Office æå¼åãå¦å为ã.docx æ .xlsx åéæ°å¯¼å ¥"); ErrorCode AI_IMPORT_FILE_TOO_LARGE = new ErrorCode(1_070_104_004, "æä»¶ã{}ãå¤§å° {}MBï¼è¶ è¿åæä»¶ä¸é {}MBï¼è¯·å缩ææååéæ°å¯¼å ¥"); ErrorCode AI_IMPORT_FILE_EMPTY = new ErrorCode(1_070_104_005, "æä»¶ã{}ãæªè§£æåºä»»ä½å¯ç¨å 容ï¼å ± {} 页ï¼ãè¥æ¯æ«æä»¶ï¼è¯·ç¡®è®¤å辨çä¸ä½äº 200 DPIã" + "æåæ 严é徿æé®æ¡ï¼æ¨èæ¹ç¨çµåç PDF æ Word/Excel"); ErrorCode AI_IMPORT_TOO_MANY_FILES = new ErrorCode(1_070_104_006, "䏿¬¡æå¤å¯¼å ¥ {} 个æä»¶ï¼æ¬æ¬¡æäº¤äº {} 个ï¼è¯·åæ¹å¯¼å ¥"); ErrorCode AI_IMPORT_TOO_MANY_PAGES = new ErrorCode(1_070_104_007, "æä»¶ã{}ãå ± {} 页ï¼è¶ è¿å次è¯å«ä¸é {} 页ï¼è¯·æåååæ¹å¯¼å ¥"); ErrorCode AI_IMPORT_CATALOG_INVALID = new ErrorCode(1_070_104_008, "ç»ä»¶æ¸ ååæ°ä¸åæ³ï¼{}ï¼ï¼è¯·å·æ°é¡µé¢åéè¯"); ErrorCode AI_IMPORT_SCHEMA_VERSION_UNSUPPORTED = new ErrorCode(1_070_104_009, "ç»ä»¶æ¸ åç Schema çæ¬ä¸ºã{}ãï¼æå¡ç«¯åªæ¥åã{}ãï¼è¯·å·æ°é¡µé¢åéè¯"); ErrorCode AI_IMPORT_AI_UNAVAILABLE = new ErrorCode(1_070_104_010, "AI è¯å«è°ç¨å¤±è´¥ï¼å·²èæ¶ {}msï¼ï¼{}ã请å°ãAI 大模åãä¸ç¡®è®¤å¯¹è¯æ¨¡åå·²å¯ç¨ä¸å¯é¥ææï¼æç¨åéè¯"); ErrorCode AI_IMPORT_AI_TIMEOUT = new ErrorCode(1_070_104_011, "AI è¯å«è¶ æ¶ï¼æ¬æ¬¡å·²ç {} ç§ï¼ãæä»¶é¡µæ°ææ°éè¾å¤æ¶èæ¶æ´é¿ï¼" + "请åå°æä»¶æ°éï¼ææ¹ç¨çµåç PDF / Word / Excel"); ErrorCode AI_IMPORT_RESPONSE_UNPARSEABLE = new ErrorCode(1_070_104_012, "AI è¿åçå å®¹æ æ³è§£æä¸ºæ¨¡æ¿è稿ï¼å·²æ¶å° {} 个å符ï¼ãè¥æä»¶å 容å¾é¿ï¼è¯·æåååæ¹å¯¼å ¥ï¼" + "è¥æç»å¤±è´¥ï¼è¯·æ¹ç¨æå·¥è®¾è®¡"); ErrorCode AI_IMPORT_DRAFT_EMPTY = new ErrorCode(1_070_104_013, "AI æªè½ä»æä»¶ã{}ãä¸è¯å«åºä»»ä½å¯ç¨ç»ä»¶ã请确认该æä»¶æ¯æ£éªæ¥åï¼" + "è¥ç¡®å®æ¯ï¼è¯·æ¹ç¨æå·¥è®¾è®¡æ¨¡æ¿"); ErrorCode AI_IMPORT_TOO_MANY_PAGES_TOTAL = new ErrorCode(1_070_104_014, "æ¬æ¬¡æäº¤çæä»¶å计 {} 页ï¼è¶ è¿å次è¯å«ä¸é {} 页ï¼åæä»¶ä¸é {} 页ï¼ï¼" + "请åå°æä»¶æ°éæåæ¹å¯¼å ¥"); ErrorCode AI_IMPORT_OFFICE_CORRUPT = new ErrorCode(1_070_104_015, "æä»¶ã{}ãæ æ³ä½ä¸º .{} æå¼ï¼å 容已æåæåå¯ç ä¿æ¤ã" + "请å ç¨ Office æå¼è¯¥æä»¶ï¼ç¡®è®¤è½æ£å¸¸æ¾ç¤ºåãå¦å为ã.docx / .xlsx åéæ°å¯¼å ¥ï¼" + "è¥æä»¶ææå¼å¯ç ï¼è¯·å è§£é¤å¯ç ä¿æ¤"); ErrorCode AI_IMPORT_AI_BUDGET_EXCEEDED = new ErrorCode(1_070_104_016, "æ¬æ¬¡è¯å«å·²è¾¾å°åæ¬¡å¯¼å ¥èæ¶ä¸éï¼{} ç§ï¼å·²ç¨æ¶ {} ç§ï¼å¹¶æå忢ï¼" + "卿件ã{}ã䏿ªè¯å«åºä»»ä½å¯ç¨ç»ä»¶ã请åå°æä»¶æ°éæé¡µæ°ååæ¹å¯¼å ¥ï¼" + "è¥å个æä»¶ä¹ä¸å¤§ï¼è¯·å°ãAI 大模åã䏿£æ¥å¯¹è¯æ¨¡åæ¯å¦ååºç¼æ ¢"); // ========== ç±è´¨æ£åçææ¥åï¼1-070-105-000ï¼ ========== ErrorCode QC_GENERATE_TYPE_INVALID = new ErrorCode(1_070_105_000, "è´¨æ£ç±»åã{}ãä¸åæ³ï¼åªæ¯æ IQCï¼æ¥ææ£éªï¼ãIPQCï¼è¿ç¨æ£éªï¼ãOQCï¼åºè´§æ£éªï¼ãRQCï¼éè´§æ£éªï¼"); ErrorCode QC_GENERATE_ORDER_NOT_EXISTS = new ErrorCode(1_070_105_001, "{}çè´¨æ£åï¼ID={}ï¼ä¸åå¨æå·²è¢«å é¤ï¼è¯·å·æ°å表åéæ°éæ©"); ErrorCode QC_GENERATE_ORDER_NOT_FINISHED = new ErrorCode(1_070_105_002, "{}çè´¨æ£åã{}ãå½åç¶æä¸ºã{}ãï¼å°æªå®ææ£éªï¼ä¸è½çææ¥åã" + "è¯·å æè¯¥åæ®æäº¤å¹¶å®ææ£éªå¤å®ååçæ"); ErrorCode QC_GENERATE_ORDER_NOT_JUDGED = new ErrorCode(1_070_105_003, "{}çè´¨æ£åã{}ãå°æªå¡«åæ£éªå¤å®ï¼å¤å®ä¸ºç©ºï¼ï¼ä¸è½çææ¥åã" + "请å å¨è¯¥åæ®ä¸å¡«åå¤å®ç»è®ºï¼åæ ¼ / ç¹é / ä¸åæ ¼éè´§ / ä¸åæ ¼æ¥åºï¼ååçæ"); // 105_004 / 105_007 两æ¡è·¯å¾å¤ç¨æ¢æç TEMPLATE_NOT_EXISTS / TEMPLATE_STATUS_DISABLED / // TEMPLATE_NO_PUBLISHED_VERSIONï¼é£å æ¡ææ¡æ¬æ¥å°±æ¯ç²¾ç¡®çï¼åé ä¸éåªä¼å¤ä¸¤æ¡ä¼æ¼ç§»ç坿¬ ErrorCode QC_GENERATE_TEMPLATE_TYPE_MISMATCH = new ErrorCode(1_070_105_005, "æ¥å模æ¿çæ¥åç±»åæ¯ã{}ãï¼ä¸æ¬æ¬¡è´¨æ£åçç±»åã{}ãä¸ä¸è´ï¼æ æ³çææ¥åã" + "è¯·éæ°éæ©ä¸å¼ {}çæ¥åæ¨¡æ¿"); ErrorCode QC_GENERATE_NO_ITEM = new ErrorCode(1_070_105_006, "{}çè´¨æ£åã{}ãæ²¡æå¯ç¨äºåºæ¥åçæ£éªé¡¹ï¼{}ï¼ï¼æ æ³çææ¥åã" + "请å å¨è¯¥åæ®ä¸å½å ¥æ£éªææ ä¸å®æµå¼ååçæ"); } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/enums/QcReportEnums.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,156 @@ package cn.iocoder.yudao.module.qcreport.enums; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; /** * æºè½è´¨æ£æ¥åè®¾è®¡å¹³å° æä¸¾éå */ public interface QcReportEnums { /** * 模æ¿ç¶æ */ @Getter @AllArgsConstructor enum TemplateStatusEnum implements ArrayValuable<Integer> { ENABLE(0, "å¯ç¨"), DISABLE(1, "åç¨"); private final Integer status; private final String name; public static final Integer[] ARRAYS = Arrays.stream(values()) .map(TemplateStatusEnum::getStatus).toArray(Integer[]::new); @Override public Integer[] array() { return ARRAYS; } } /** * 模æ¿çæ¬ç¶æ */ @Getter @AllArgsConstructor enum VersionStatusEnum implements ArrayValuable<Integer> { DRAFT(0, "è稿"), PUBLISHED(1, "å·²åå¸"), DISABLED(2, "å·²åç¨"); private final Integer status; private final String name; public static final Integer[] ARRAYS = Arrays.stream(values()) .map(VersionStatusEnum::getStatus).toArray(Integer[]::new); @Override public Integer[] array() { return ARRAYS; } } /** * æ¥åå®ä¾ç¶æ */ @Getter @AllArgsConstructor enum InstanceStatusEnum implements ArrayValuable<Integer> { GENERATING(0, "çæä¸"), SUCCESS(1, "çææå"), FAILED(2, "çæå¤±è´¥"); private final Integer status; private final String name; public static final Integer[] ARRAYS = Arrays.stream(values()) .map(InstanceStatusEnum::getStatus).toArray(Integer[]::new); @Override public Integer[] array() { return ARRAYS; } } /** * çº¸å¼ å°ºå¯¸ */ @Getter @AllArgsConstructor enum PageSizeEnum implements ArrayValuable<String> { A3("A3", "A3"), A4("A4", "A4"), A5("A5", "A5"), LETTER("Letter", "Letter"); private final String size; private final String name; public static final String[] ARRAYS = Arrays.stream(values()) .map(PageSizeEnum::getSize).toArray(String[]::new); @Override public String[] array() { return ARRAYS; } } /** * çº¸å¼ æ¹å */ @Getter @AllArgsConstructor enum OrientationEnum implements ArrayValuable<String> { PORTRAIT("portrait", "纵å"), LANDSCAPE("landscape", "横å"); private final String orientation; private final String name; public static final String[] ARRAYS = Arrays.stream(values()) .map(OrientationEnum::getOrientation).toArray(String[]::new); @Override public String[] array() { return ARRAYS; } } /** * å¤å®ç»æ */ @Getter @AllArgsConstructor enum CheckResultEnum implements ArrayValuable<String> { PASS("PASS", "åæ ¼"), FAIL("FAIL", "ä¸åæ ¼"); private final String result; private final String name; public static final String[] ARRAYS = Arrays.stream(values()) .map(CheckResultEnum::getResult).toArray(String[]::new); @Override public String[] array() { return ARRAYS; } } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/enums/QcReportSourceTypeEnum.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,54 @@ package cn.iocoder.yudao.module.qcreport.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Objects; /** * æ¥åæ¥æºåæ®ç±»åï¼MES çå类质æ£åï¼ã * <p> * è¿éåæ¶éæ»ä¸ä»¶äºç¸ç»å®çäºï¼é¿å æ£è½æä¸å¤å符串åé¢éï¼ * è´¨æ£åçç±»åå¼ï¼ä¼ ç» MES æ¥å£ç¨ï¼ãæ¨¡æ¿ {@code report_type} çåå¼ï¼ä¸åç«¯ä¸æåæºï¼æ¯æ°åå符串ï¼ã * 以忥åå®ä¾è½åºæ¶ç {@code business_type}ã * <p> * {@code businessType} ç¨ç {@code mes_qc_*} æ¯äºå®ä¸çå¥çº¦ ââ * ãæåæ®åæ¥æ¥åãé å®è¿æ»¤ï¼æ¹ä¸ä¸ªå符就æ¥ä¸å°å岿¥åã */ @Getter @AllArgsConstructor public enum QcReportSourceTypeEnum { IQC(1, "æ¥ææ£éª", "mes_qc_iqc"), IPQC(2, "è¿ç¨æ£éª", "mes_qc_ipqc"), OQC(3, "åºè´§æ£éª", "mes_qc_oqc"), RQC(4, "éè´§æ£éª", "mes_qc_rqc"); /** è´¨æ£ç±»åå¼ï¼è§ MES ç MesQcTypeEnum */ private final Integer type; /** ç±»ååï¼ä¸ MES å表ä¸ç嫿³ä¸è´ */ private final String name; /** æ¥åå®ä¾ç business_type */ private final String businessType; /** è¯å«ä¸åºè¿å nullï¼ç±è°ç¨æ¹å³å®æä¹æ¥é */ public static QcReportSourceTypeEnum of(Integer type) { for (QcReportSourceTypeEnum value : values()) { if (Objects.equals(value.type, type)) { return value; } } return null; } /** * 模æ¿ä¸çæ¥åç±»åæ¯å¦æåæ¬ç±»åã * <p> * 模æ¿ç {@code report_type} åçæ¯æ°åå符串ï¼åç«¯ä¸æå mes_qc_type åå ¸ç string å¼ï¼ï¼ * æä»¥æ¯çæ¯ {@code "1"} è䏿¯ {@code "IQC"}ã */ public boolean matchesTemplateReportType(String reportType) { return reportType != null && reportType.trim().equals(String.valueOf(type)); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/aiimport/QcReportAiImportService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,29 @@ package cn.iocoder.yudao.module.qcreport.service.aiimport; import cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo.QcReportAiDraftReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo.QcReportAiDraftRespVO; /** * AI å¯¼å ¥ Service æ¥å£ï¼è¯»æä»¶ â åºæ¨¡æ¿è稿ã * * <h3>为ä»ä¹æ¯åæ¥æ¥å£</h3> * 䏿¬¡å¯¼å ¥è¦ç 10ï½60 ç§ãåæå¼æ¥éè¦ä»»å¡è¡¨ + 轮询 + çº¿ç¨æ± ï¼è宿¢æ¥çåªæ¯ãç¨æ·å¯ä»¥å å»å¹²å«çãââ * ä½è稿åªä½åèï¼ç¨æ·çç»æåå»å¹²å«çååæ¥ï¼æ¿å°çè¡ä¸ºå®å ¨ä¸æ ·ã为ä¸ä¸ªä¸æ¹åç»æçä½éª * å¼å ¥ä¸å¼ 表åä¸ä¸ªçº¿ç¨æ± ï¼ä¸åç®ã * * <h3>为ä»ä¹ä¸è½åº</h3> * è稿åªç»äººå¨è®¾è®¡å¨é确认ï¼ä¸éè¦ä¸ä¸ªè½æ ID 忥çå®ä½ãä¸è½åºæå³ç失败ãéè¯ãæ¾å¼ * é½ä¸çä¸ä»»ä½éè¦æ¸ ççè¡ï¼èãä¿åãä»ç¶æ¯æ¢æç {@code createVersion} / {@code updateVersion}ï¼ * Schema æ ¡éªï¼å«å¤å®è§åï¼é£éé¸é¨èªå¨çæï¼ä¸åå¨ç¬¬äºæ¡è½åºè·¯å¾ã */ public interface QcReportAiImportService { /** * è¯å«æä»¶ï¼è¿å模æ¿è稿ã * * @param reqVO 请æ±åæ°ï¼æä»¶ blobIdãç»ä»¶æ¸ åãè¡¥å 说æï¼ * @return 模æ¿è稿ï¼å«è½¯æç¤º */ QcReportAiDraftRespVO generateDraft(QcReportAiDraftReqVO reqVO); } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/aiimport/QcReportAiImportServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,323 @@ package cn.iocoder.yudao.module.qcreport.service.aiimport; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.module.ai.api.chat.AiChatApi; import cn.iocoder.yudao.module.qcreport.config.QcReportAiImportProperties; import cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo.QcReportAiDraftReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo.QcReportAiDraftRespVO; import cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo.QcReportComponentSpecVO; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.ReportTemplateSchema; import cn.iocoder.yudao.module.qcreport.service.aiimport.document.QcReportDocumentExtract; import cn.iocoder.yudao.module.qcreport.service.aiimport.document.QcReportDocumentExtractService; import cn.iocoder.yudao.module.qcreport.service.aiimport.llm.QcReportAiDraftNormalizer; import cn.iocoder.yudao.module.qcreport.service.aiimport.llm.QcReportAiDraftParser; import cn.iocoder.yudao.module.qcreport.service.aiimport.llm.QcReportLlmCall; import cn.iocoder.yudao.module.qcreport.service.aiimport.llm.QcReportLlmCallPlanner; import cn.iocoder.yudao.module.qcreport.service.aiimport.llm.QcReportTemplatePromptBuilder; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO; import cn.iocoder.yudao.module.system.service.storage.SystemStorageBlobService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.io.File; import java.io.IOException; import java.io.InterruptedIOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_AI_BUDGET_EXCEEDED; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_AI_TIMEOUT; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_AI_UNAVAILABLE; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_BLOB_NOT_FOUND; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_CATALOG_INVALID; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_DISABLED; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_DRAFT_EMPTY; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_FILE_TOO_LARGE; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_SCHEMA_VERSION_UNSUPPORTED; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_TOO_MANY_FILES; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_TOO_MANY_PAGES_TOTAL; /** * AI å¯¼å ¥ Service å®ç°ç±»ã * <p> * æµç¨åºå®ä¸ºï¼æ ¡éª â é个æä»¶æ½å â æè°ç¨è®¡å â æ§è¡ â 鿬¡å½ä¸ â åå¹¶å»é â ç»è£ ã * æ¯ä¸æ¥ç失败é½å¯¹åºä¸ä¸ªç¡®å®çé误ç ï¼å端æ¿å°çæ°¸è¿æ¯ãåªä¸æ¥ã为ä»ä¹ãæä¹åãï¼ * è䏿¯ä¸ä¸ª 500 å ä¸å¥ãAI è¯å«å¤±è´¥ãã * <p> * <b>ä¸ ERP / CRM é£ä¸å¤ AI è°ç¨çä¸å¤å»æå离</b>ï¼å®ä»¬æé误å¡è¿ååºä½ç {@code rawText} ä¸è¿å HTTP 200ï¼ * å端é ãrawText ææ²¡æå¼ãå æ¢å¤±è´¥ãæ¬å®ç°æ¹ç¨çæ£ç ServiceException + åé误ç ââ * é åæ®µææ²¡æå¼æ¥å¤ææè´¥ï¼ä¸æ¦æå¤©æ£å¸¸è¿åä¹å¸¦ä¸äº rawTextï¼å端ä¼ç«å»è¯¯å¤ã */ @Slf4j @Service @Validated public class QcReportAiImportServiceImpl implements QcReportAiImportService { /** * ç»ä»¶æ¸ åç项æ°ä¸éã * <p> * å端注åè¡¨ç°æ 12 个ç»ä»¶ï¼çåºä¸åä½éãçæ£çé²çº¿æ¯ç§¯æ¨æ¸ åçåºååé¿åº¦ä¸é * ï¼å¨ {@link QcReportTemplatePromptBuilder} éï¼ï¼è¿æ¡åªæ¯æåæ¦ä½ææ¾å¼å¸¸çå ¥åã */ private static final int MAX_CATALOG_ITEMS = 40; @Resource private QcReportAiImportProperties properties; @Resource private QcReportDocumentExtractService extractService; @Resource private AiChatApi aiChatApi; @Resource private SystemStorageBlobService storageBlobService; @Override public QcReportAiDraftRespVO generateDraft(QcReportAiDraftReqVO reqVO) { long startedAt = System.currentTimeMillis(); validateEnabled(); validateCatalog(reqVO); validateSchemaVersion(reqVO.getSchemaVersion()); validateFileCount(reqVO.getBlobIds().size()); String systemPrompt = QcReportTemplatePromptBuilder.buildSystemPrompt(reqVO.getCatalog()); List<QcReportAiDraftNormalizer.Result> perCall = new ArrayList<>(); List<String> warnings = new ArrayList<>(); List<String> fileNames = new ArrayList<>(); ReportTemplateSchema.Page page = null; String summary = null; int totalPages = 0; int callCount = 0; boolean budgetExceeded = false; long budgetMs = properties.getMaxDurationSeconds() * 1000L; for (Long blobId : reqVO.getBlobIds()) { // è¿æä»¶åå çé¢ç®ï¼å·²ç»è¶ äºå°±ä¸è¦åæä¸ä¸ä»½æä»¶è¯»è¿æ¥ãæ´ä¸è¦æ¸²æå®çé¡µï¼ // é£äºé½æ¯ç½è±ç CPU åå å if (System.currentTimeMillis() - startedAt >= budgetMs) { budgetExceeded = true; break; } LoadedBlob blob = loadBlob(blobId); QcReportDocumentExtract extract = extractService.extract(blob.content(), blob.originalFilename(), blob.contentType()); fileNames.add(blob.originalFilename()); totalPages += extract.pageCount(); validateTotalPages(totalPages); warnings.addAll(extract.notes()); for (QcReportLlmCall call : QcReportLlmCallPlanner.plan(extract, blob.originalFilename())) { // 两次è°ç¨ä¹é´å ³é¨ã忬¡è°ç¨æ æ³ææï¼AiChatApi 䏿¶è¶ æ¶åæ°ï¼ï¼ // æä»¥è¿éæ¦ä¸ä½ãæå䏿¬¡è°ç¨åèæ»¡ä¸ä¸ª yudao.ai.timeoutãââ // åç«¯è¶ æ¶å æ¤å¿ é¡»æ¯ãæ¬é¢ç® + 忬¡è¶ æ¶ãæ´å®½ï¼å¦åç¨æ·çå°çæ¯æµè§å¨æå¼èéè¿æ¡æç¤º if (System.currentTimeMillis() - startedAt >= budgetMs) { budgetExceeded = true; break; } QcReportAiDraftRespVO raw = execute(systemPrompt, reqVO.getHint(), call, startedAt); callCount++; if (page == null) { page = raw.getPage(); } if (summary == null && StrUtil.isNotBlank(raw.getSummary())) { summary = raw.getSummary(); } perCall.add(QcReportAiDraftNormalizer.normalize( raw.getComponents(), reqVO.getCatalog(), call.sourceLabel())); } if (budgetExceeded) { break; } } long elapsedSeconds = (System.currentTimeMillis() - startedAt) / 1000; if (budgetExceeded) { // å·²ç»è¯å«åºæ¥çé¨åç §å¸¸è¿åï¼å份èç¨¿å¯¹ç¨æ·ä»ç¶æç¨ï¼ // ä¸è¿éæãåç»æ²¡è¯å«ãæååºæ¥ï¼ä¸æ¯é黿ªæ warnings.add(StrUtil.format( "æ¬æ¬¡è¯å«å·²è¾¾å°åæ¬¡å¯¼å ¥èæ¶ä¸éï¼{} ç§ï¼å·²ç¨æ¶ {} ç§ï¼ï¼åç»å 容æªåè¯å«ï¼" + "以䏿¯å·²è¯å«å°çé¨åã请åå°æä»¶æ°éæé¡µæ°ååæ¹å¯¼å ¥", properties.getMaxDurationSeconds(), elapsedSeconds)); } QcReportAiDraftNormalizer.Result merged = QcReportAiDraftNormalizer.mergeAndDedup(perCall, properties.getMaxComponents()); warnings.addAll(merged.warnings()); if (merged.components().isEmpty()) { // ä¸ä¸ªç»ä»¶é½æ²¡è¯å«åºæ¥æ¶ï¼æ¥éææ¡è¦è¯´æ¸ å°åºæ¯å 为èå°½äºé¢ç®ï¼ // è¿æ¯æ¨¡åç¡®å®è®¤ä¸åºââ两è ç¨æ·è¦éåçå¨ä½å®å ¨ä¸å if (budgetExceeded) { throw exception(AI_IMPORT_AI_BUDGET_EXCEEDED, properties.getMaxDurationSeconds(), elapsedSeconds, String.join("ã", fileNames)); } throw exception(AI_IMPORT_DRAFT_EMPTY, String.join("ã", fileNames)); } QcReportAiDraftRespVO resp = new QcReportAiDraftRespVO(); resp.setComponents(merged.components()); resp.setPage(page); resp.setSummary(summary); resp.setWarnings(warnings); resp.setDurationMs(System.currentTimeMillis() - startedAt); log.info("AI å¯¼å ¥è¯å«å®æï¼files={}, pages={}, calls={}, components={}, budgetExceeded={}, durationMs={}", fileNames, totalPages, callCount, merged.components().size(), budgetExceeded, resp.getDurationMs()); return resp; } /* ------------------------------ è°ç¨æ§è¡ ------------------------------ */ /** * å䏿¬¡æ¨¡åè°ç¨å¹¶è§£æåå¤ã * <p> * ææå¼å¸¸é½å¨è¿éæ¶å£æä¸ä¸ªç¡®å®çé误ç ï¼è¶ æ¶ä¸å ¶å®å¤±è´¥åå¼ï¼å ä¸ºç¨æ·çä¸ä¸æ¥å¨ä½å®å ¨ä¸åââ * åè 该åæä»¶ï¼åè è¯¥å»æ¥ AI 模åé ç½®ãæå®ä»¬æ··æãAI è¯å«å¤±è´¥ãï¼ç¨æ·åªè½ä¸¤ä»¶äºé½è¯ä¸éã */ private QcReportAiDraftRespVO execute(String systemPrompt, String hint, QcReportLlmCall call, long startedAt) { String userMessage = QcReportTemplatePromptBuilder.buildUserMessage(hint, call.sourceLabel(), call.text()); String reply; try { reply = call.kind() == QcReportLlmCall.Kind.IMAGE ? aiChatApi.chatWithImage(systemPrompt, userMessage, call.imageBase64(), call.mimeType()) : aiChatApi.chat(systemPrompt, userMessage); } catch (Exception e) { long elapsed = System.currentTimeMillis() - startedAt; log.warn("AI å¯¼å ¥è°ç¨å¤±è´¥ï¼source={}, kind={}, elapsedMs={}", call.sourceLabel(), call.kind(), elapsed, e); if (isTimeout(e)) { throw exception(AI_IMPORT_AI_TIMEOUT, elapsed / 1000); } throw exception(AI_IMPORT_AI_UNAVAILABLE, elapsed, StrUtil.maxLength(String.valueOf(e.getMessage()), 200)); } try { return QcReportAiDraftParser.parse(reply); } catch (ServiceException e) { // 模åè¿åäºä»ä¹ï¼æ¯ææ¥è¿æ¡è·¯ä¸å¯ä¸ç线索ãåè¿æ¥å¿è䏿¯å¡è¿ååºï¼ // ä¸å¨æ¨¡ååæç»ç¨æ·ç没ææä¹ï¼èé误ç å·²ç»è®²æ¸ äºãæä¹åã log.warn("AI å¯¼å ¥è§£æåå¤å¤±è´¥ï¼source={}, reply={}", call.sourceLabel(), QcReportAiDraftParser.truncate(reply), e); throw e; } } /** * 夿å¼å¸¸é¾éææ²¡æè¶ æ¶ã * <p> * {@code AiChatApi} 䏿´é²åºå± HTTP 客æ·ç«¯ï¼è¶ æ¶æç»ä¼ä»¥ {@link InterruptedIOException} * ï¼OkHttp ç读åè¶ æ¶é½æ¯å®çåç±»ï¼æå¸¦æ "timeout" åæ ·çå¼å¸¸åºç°å¨åå é¾ä¸ã * æ²¿é¾æ¾è䏿¯åªçæå¤å±ï¼ä¸é´éäºå å±å è£ æ¯å¸¸æã */ private static boolean isTimeout(Throwable e) { for (Throwable cause = e; cause != null; cause = cause.getCause()) { if (cause instanceof InterruptedIOException) { return true; } if (cause.getMessage() != null && cause.getMessage().toLowerCase().contains("timeout")) { return true; } if (cause.getCause() == cause) { break; } } return false; } /* ------------------------------ 读åæä»¶ ------------------------------ */ /** * 读ä¸ä¸ª blob çåèã * <p> * <b>廿ä¸å é¤åå°çæä»¶ã</b> {@code getPublicFile} è¿åçæ¯ç£çä¸çå®åå¨çé£ä»½ blobï¼ * 䏿¯ä¸´æ¶å¯æ¬ââå äºå°±æ¯å ç¨æ·ççå®ä¸ä¼ æ°æ®ã * ï¼ERP éè´æ¥ç¥¨ AI ä¸ CRM æ¥ä»· AI æ¾å¨ {@code finally} éç¯è¿ä¸ªéï¼å·²ä¿®ï¼æ¬å®ç°ç»ä¸å¤å¶ãï¼ */ private LoadedBlob loadBlob(Long blobId) { SystemStorageBlobDO blob = storageBlobService.getStorageBlob(blobId); if (blob == null) { throw exception(AI_IMPORT_BLOB_NOT_FOUND, blobId); } String originalFilename = StrUtil.blankToDefault(blob.getOriginalFilename(), "file"); try { File file = storageBlobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey()); byte[] content = Files.readAllBytes(file.toPath()); long maxBytes = properties.getMaxFileSizeMb() * 1024L * 1024L; if (content.length > maxBytes) { throw exception(AI_IMPORT_FILE_TOO_LARGE, originalFilename, content.length / 1024L / 1024L, properties.getMaxFileSizeMb()); } return new LoadedBlob(content, originalFilename, blob.getContentType()); } catch (IOException e) { log.warn("读åä¸ä¼ æä»¶å¤±è´¥ï¼blobId={}", blobId, e); throw exception(AI_IMPORT_BLOB_NOT_FOUND, blobId); } } private record LoadedBlob(byte[] content, String originalFilename, String contentType) { } /* ------------------------------ æ ¡éª ------------------------------ */ private void validateEnabled() { if (!Boolean.TRUE.equals(properties.getEnabled())) { throw exception(AI_IMPORT_DISABLED); } } private void validateCatalog(QcReportAiDraftReqVO reqVO) { if (reqVO.getCatalog() == null || reqVO.getCatalog().isEmpty()) { throw exception(AI_IMPORT_CATALOG_INVALID, "ç»ä»¶æ¸ å为空"); } if (reqVO.getCatalog().size() > MAX_CATALOG_ITEMS) { throw exception(AI_IMPORT_CATALOG_INVALID, StrUtil.format("ç»ä»¶æ¸ åå ± {} 项ï¼è¶ è¿ä¸é {} 项", reqVO.getCatalog().size(), MAX_CATALOG_ITEMS)); } for (QcReportComponentSpecVO spec : reqVO.getCatalog()) { if (spec == null || StrUtil.isBlank(spec.getType())) { throw exception(AI_IMPORT_CATALOG_INVALID, "å卿²¡æ type çç»ä»¶é¡¹"); } } } /** * æ ¡éª Schema çæ¬ã * <p> * è¿æ¡æ£æ¥æãç§¯æ¨æ¸ åæ¥èª 1.1 è¯ä¹å±ãè¿ä¸ªéå¼è¦ååææ¾å¼å¤±è´¥ï¼çæ¬ä¸ä¸è´è¯´æåç«¯æ¯æ§å ï¼ * å®çæåºæ¥çæ¸ åå¯è½ä¸æå¡ç«¯çè§£çè¯ä¹å±å¯¹ä¸ä¸ï¼æ¤æ¶æ¥éè®©ç¨æ·å·æ°ï¼æ¯è®©æ¨¡ååºäºè¿ææ¸ åç¼ç»ä»¶å®å ¨ã */ private void validateSchemaVersion(String schemaVersion) { if (!ReportTemplateSchema.SCHEMA_VERSION.equals(schemaVersion)) { throw exception(AI_IMPORT_SCHEMA_VERSION_UNSUPPORTED, schemaVersion, ReportTemplateSchema.SCHEMA_VERSION); } } /** * æ ¡éªæä»¶ä¸ªæ°ã**è¿æ¯æä»¶æ°ä¸éçå¯ä¸é¸é¨**ï¼å ¥å VO ä¸å»æä¸åæ {@code @Size}ã * <p> * è°ç¨ä½ç½®å¨ {@link #generateDraft} çæå段ãä»»ä½ blob 读åä¹åï¼æä»¥æ¾å¤§æä»¶æ°ä¸ä¼å¸¦æ¥ * é¢å¤çç£ç/å åå¼éï¼æ³¨è§£é£å±æ¦æªå¹¶æ å¿ è¦ï¼è两个æ°åå¹¶åçç»æåªä¼æ¯æ³¨è§£æ´ä¸¥ãé 置失å»å¯è°æ§ã */ private void validateFileCount(int fileCount) { if (fileCount > properties.getMaxFiles()) { throw exception(AI_IMPORT_TOO_MANY_FILES, properties.getMaxFiles(), fileCount); } } /** * æ ¡éªç´¯è®¡é¡µæ°ã * <p> * åæä»¶é¡µæ°å¨éé å¨éå·²ç»å¡è¿äºï¼è¿éå¡çæ¯ãæ¯ä¸ªæä»¶é½æ²¡è¶ ãå èµ·æ¥å´å¾å¤ãââ * ä¸ä¸ªå 5 é¡µçæ«æä»¶ä¼åæ 15 æ¬¡å¤æ¨¡æè°ç¨ï¼ä»£ä»·ä¸èæ¶é½è¿è¶ 颿ã */ private void validateTotalPages(int totalPages) { if (totalPages > properties.getMaxPagesPerRequest()) { throw exception(AI_IMPORT_TOO_MANY_PAGES_TOTAL, totalPages, properties.getMaxPagesPerRequest(), properties.getMaxPagesPerFile()); } } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/aiimport/document/ImageImportAdapter.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,62 @@ package cn.iocoder.yudao.module.qcreport.service.aiimport.document; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.util.List; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_FILE_EMPTY; /** * å¾çï¼ç §ç / æªå¾ï¼æ½åå¨ã * <p> * ä¸ç§éé å¨éå¯ä¸ãä»ä¹é½ä¸ç¨åãçä¸ä¸ªï¼åèåæ ·äº¤ç»å¤æ¨¡ææ¨¡åï¼ä¸è§£ç ãä¸ç¼©æ¾ãä¸è½¬ç ã * 廿ä¸å¨è¿éç¨ {@code ImageIO} 读ä¸éââé£ä¸ä» å¤ä¸æ¬¡å åæ·è´ï¼è¿ä¼å¨éå° webp è¿ç±» * JDK åç䏿¯æçæ ¼å¼æ¶ç½ç½å¤±è´¥ï¼è模åé£è¾¹å ¶å®å®å ¨è½è®¤ã * <p> * æå¨ç¬¬ä¸ä½ï¼å®åªè®¤å¾çæ©å±åï¼ä¸å¦å¤ä¸¤ä¸ªçå¹é éä¸ç¸äº¤ï¼æ¾åªé½ä¸ä¼å¼èµ·æ§ä¹ï¼ * 使¾å¨æåé¢è½è®©ãä¸å¼ ç §çãè¿æ¡æå¸¸è§çè·¯å¾ä»¥æçç路走å°å¤´ã */ @Component @Order(1) public class ImageImportAdapter implements QcReportImportAdapter { private static final Set<String> EXTENSIONS = Set.of("png", "jpg", "jpeg", "bmp", "gif", "webp"); @Override public boolean supports(String extension, String contentType) { return EXTENSIONS.contains(extension); } @Override public QcReportDocumentExtract extract(byte[] content, String originalFilename) { if (content == null || content.length == 0) { throw exception(AI_IMPORT_FILE_EMPTY, originalFilename, 1); } String mimeType = mimeTypeOf(extensionOf(originalFilename)); return QcReportDocumentExtract.ofImages( List.of(new QcReportDocumentExtract.ImagePart(content, mimeType, "第 1 页")), 1, "raw-image", List.of()); } /** * PDF éé å¨éè¦åä¸å¥æ å°æ¥æ 注渲æåºæ¥ç PNGï¼æä»¥å ¬å¼åºæ¥å¤ç¨ã */ static String mimeTypeOf(String extension) { return switch (extension) { case "jpg", "jpeg" -> "image/jpeg"; case "gif" -> "image/gif"; case "bmp" -> "image/bmp"; case "webp" -> "image/webp"; default -> "image/png"; }; } private static String extensionOf(String filename) { int dot = filename == null ? -1 : filename.lastIndexOf('.'); return dot < 0 ? "" : filename.substring(dot + 1).toLowerCase(); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/aiimport/document/OfficeImportAdapter.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,372 @@ package cn.iocoder.yudao.module.qcreport.service.aiimport.document; import cn.iocoder.yudao.module.qcreport.config.QcReportAiImportProperties; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xwpf.usermodel.IBodyElement; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFHeaderFooter; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.apache.poi.xwpf.usermodel.XWPFTableRow; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_FILE_EMPTY; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_FILE_LEGACY_OFFICE; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_OFFICE_CORRUPT; /** * Word / Excel / çº¯ææ¬æ½åå¨ã * <p> * <b>ç¨ POI è䏿¯ Tika</b>ï¼ä¸¤ä¸ªåå ï¼ç¼ºä¸ä¸å¯ï¼ * æ£éªæ¥åçæ ¸å¿ä¿¡æ¯å¨è¡¨æ ¼éï¼è¡åç»æä¸æ¦è¢« Tika æå¹³æä¸æ®µæåå°±å乿¾ä¸åæ¥ï¼ * è {@code tika-parsers-standard-package} 伿è¿å ç¾ MB çè§£æå¨ä¾èµï¼ * ä¸ä¸ªåªå¤çåç§æ ¼å¼çæ¨¡åæ²¡å¿ è¦èè¿ä¸ªå 袱ã * * <h3>页ç页è</h3> * .docx ç page header / footer å卿£æä¹å¤ï¼{@code getBodyElements()} çä¸å°å®ä»¬ã * åªçæ£æçæä»¶ï¼ãå ¬å¸æ¬å¤´å¨é¡µçãååçµè¯å¨é¡µèãè¿ç±»æ¥å伿䏤头é½ä¸¢æââ * 模åè¿æå齿²¡è§è¿ï¼èªç¶ä¹ä¸ä¼æ³å°ç¨ {@code ReportHeader} / {@code ReportFooter}ã * æä»¥é¡µç页èä¸å¹¶è¯»å ¥ï¼ç¨ {@code [页ç]} / {@code [页è]} æ è®°ä¸æ£æåºåã * PDF ä¸å¾çä¸éè¦è¿å¥æ è®°ï¼é£ä¸¤ç§ééé页çé¡µèæ¬æ¥å°±æ¯é¡µé¢å 容çä¸é¨åã * * <h3>æ§æ ¼å¼ .doc / .xls</h3> * å¨ {@link #supports} é<b>认é¢</b>ãå¨ {@link #extract} é<b>æç»</b>ã * 让å®èµ°å°ãæ ¼å¼ä¸æ¯æã飿¡éç¨éè¯¯å»æ¯ä¸è¡çï¼é£è¾¹åªä¼åè¯ç¨æ·ãä» æ¯æ PDF / Word / Excelãï¼ * èç¨æ·æä¸çæä»¶æ©å±åçèµ·æ¥å®å ¨åè§ï¼.doc 乿¯ Wordï¼ï¼æç¤ºçäºæ²¡è¯´ã * 认é¢ä¹åå°±è½ç»åºçæ£çåºè·¯ââå¦å为 .docxã */ @Slf4j @Component @Order(3) public class OfficeImportAdapter implements QcReportImportAdapter { private static final Set<String> EXTENSIONS = Set.of("docx", "xlsx", "txt", "csv", "doc", "xls"); /** åªå«ç©ºç½å符ç UTF-8 BOMï¼è§£ç åè¦å 奿ï¼å¦å第ä¸è¡ç第ä¸ä¸ªåå æ ¼ä¼å¸¦ä¸ä¸ªä¸å¯è§å符 */ private static final byte[] BOM_UTF_8 = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; /** 页ç/é¡µèæ è®°ï¼ä¸æ è®°çè¯ï¼æ¨¡åæ ä»å¤æè¿æ®µæ¯æ¯é¡µéå¤çæ¬å¤´è¿æ¯æ£æéçæ®éææ¬ */ private static final String HEADER_MARK = "[页ç]"; private static final String FOOTER_MARK = "[页è]"; /** 页ç/页èåªæ¯è½¯æç¤ºï¼è¿ warningsï¼ï¼è¦ä¸è¦åæç»ä»¶ç±æ¨¡åæç»ä»¶ hint 夿ï¼ç¨æ·å确认 */ private static final String HEADER_FOOTER_NOTE = "å件带æé¡µç/页èï¼å·²ç¨ [页ç] / [页è] æ è®°å¹¶å ¥è¯å«å 容"; @Resource private QcReportAiImportProperties properties; @Override public boolean supports(String extension, String contentType) { return EXTENSIONS.contains(extension); } @Override public QcReportDocumentExtract extract(byte[] content, String originalFilename) { String extension = extensionOf(originalFilename); if ("doc".equals(extension) || "xls".equals(extension)) { throw exception(AI_IMPORT_FILE_LEGACY_OFFICE, originalFilename, extension); } if (content == null || content.length == 0) { throw exception(AI_IMPORT_FILE_EMPTY, originalFilename, 1); } List<String> notes = List.of(); String text; if ("docx".equals(extension)) { DocxRead read = readDocx(content, originalFilename); text = read.text(); notes = read.notes(); } else { text = switch (extension) { case "xlsx" -> readXlsx(content, originalFilename); case "txt", "csv" -> readPlainText(content); default -> ""; }; } if (text.isBlank()) { throw exception(AI_IMPORT_FILE_EMPTY, originalFilename, 1); } return QcReportDocumentExtract.ofText(text, 1, extractorNameOf(extension), notes); } /** * 读 .docxï¼æ£æä¸é¡µç/页èä¸èµ·è¯»ã * <p> * æ£ææ {@code getBodyElements()} è¿ä»£è䏿¯ãå æææ®µè½ãåææè¡¨æ ¼ãï¼æ¥åéè¡¨æ ¼å¤¹å¨æ®µè½ä¹é´ï¼ * å弿¶é伿çé¢é¡ºåºå½»åºæä¹±ï¼æ¨¡åçå°çæ¯ä¸å æ é¢å å¨å¼å¤´ãè¡¨æ ¼å å¨ç»å°¾ã * <p> * è¡¨æ ¼ç¨ Tab åéåå æ ¼ãæ¢è¡åéè¡ï¼å¹¶å¨ååå æ è®°ââæ è®°çä½ç¨æ¯è®©æ¨¡åç¥é * ãè¿å è¡å±äºåä¸ä¸ªè¡¨æ ¼ãï¼æ²¡æå®ï¼ä¸æ®µç¨ Tab æ¼èµ·æ¥çææ¬åæ®éæ®µè½æ ä»åºåã * è¡¨æ ¼å é¨çåå¹¶åå æ ¼ç± {@link #appendTable} å½ä¸åæå¯¹é½çç½æ ¼ï¼åå¹¶ä¿¡æ¯å æ¤ä¸ä¼ä¸¢å¤±ã * <p> * <b>页ç/页èå¿ é¡»è¯»</b>ï¼æ¥åçæ¬å¤´ï¼å ¬å¸åãæ¥ååï¼åè½æ¬¾ï¼ååãçµè¯ï¼é常就ä½å¨è¿éï¼ * è {@code getBodyElements()} åªçæ£æï¼ä¸è¯»çè¯æ¨¡ååæ ¹æ²¡è§è¿è¿äºæåï¼ * ä¹å°±ä¸å¯è½æå®ä»¬è®¤æ {@code ReportHeader} / {@code ReportFooter}ã读å°çå å®¹ç¨ * {@code [页ç]} / {@code [页è]} å ä½ï¼é¡ºåºä¸ææ¡£è§è§é¡ºåºä¸è´ï¼é¡µçå¨åã页èå¨åï¼ã */ private DocxRead readDocx(byte[] content, String originalFilename) { try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(content))) { StringBuilder sb = new StringBuilder(); String header = collectHeaderFooter(document.getHeaderList()); String footer = collectHeaderFooter(document.getFooterList()); if (!header.isBlank()) { sb.append(HEADER_MARK).append('\n').append(header); } appendBodyElements(sb, document.getBodyElements()); if (!footer.isBlank()) { sb.append(FOOTER_MARK).append('\n').append(footer); } List<String> notes = header.isBlank() && footer.isBlank() ? List.of() : List.of(HEADER_FOOTER_NOTE); return new DocxRead(sb.toString(), notes); } catch (IOException | RuntimeException e) { log.warn("docx è§£æå¤±è´¥ï¼file={}", originalFilename, e); throw exception(AI_IMPORT_OFFICE_CORRUPT, originalFilename, "docx"); } } /** * æ¶ä¸ä¸ªææ¡£ç页çæé¡µèã * <p> * 两件äºå¿ é¡»åï¼å°ä¸ä»¶é½ä¼æåªå£°å½ä¿¡å·ï¼ * <ul> * <li><b>空ç½è¿æ»¤</b>ï¼ææ¡£æ ¹æ¬æ²¡æé¡µçæ¶ï¼POI çé»è®¤çç¥ä»ä¼è¿åä¸ä¸ªç©º headerï¼ * ä¸è¿æ»¤å°±ä¼å空å¤åºä¸ä¸ª {@code [页ç]} æ è®°ï¼è®©æ¨¡å以为åä»¶ææ¬å¤´ã</li> * <li><b>å»é</b>ï¼Word å 许é¦é¡µ/奿°é¡µ/å¶æ°é¡µåé ä¸å¥ï¼å®é æä»¶éè¿å å¥å¸¸å¸¸éåç¸åï¼ * ä¸å»é伿å䏿®µæ¬å¤´éå¤å éåç»æ¨¡åï¼æ¢æµªè´¹ä¸ä¸æå忝å¨å¼ºè°å®ã</li> * </ul> */ private static String collectHeaderFooter(List<? extends XWPFHeaderFooter> parts) { Set<String> distinct = new LinkedHashSet<>(); for (XWPFHeaderFooter part : parts) { StringBuilder sb = new StringBuilder(); appendBodyElements(sb, part.getBodyElements()); String text = sb.toString().trim(); if (!text.isEmpty()) { distinct.add(text); } } return String.join("\n", distinct); } /** * æé¡ºåºææ®µè½ä¸è¡¨æ ¼åè¿ç¼å²åºãæ£æã页çã页èå ±ç¨ä¸ä»½ï¼é¿å 两å¥è¡¨æ ¼é»è¾åèªæ¼ç§»ã */ private static void appendBodyElements(StringBuilder sb, List<IBodyElement> elements) { for (IBodyElement element : elements) { if (element instanceof XWPFParagraph paragraph) { appendLine(sb, paragraph.getText()); } else if (element instanceof XWPFTable table) { appendTable(sb, table); } } } /** * æä¸å¼ 表å½ä¸åæç¨³å®ç½æ ¼ååè¾åºã * <p> * ç´æ¥æ {@code row.getTableCells()} æ¼ Tab æ¯ä¸å¤çï¼åå¹¶åå æ ¼ä¼è®©æ¯è¡çåæ°åä¸ç¸åï¼ * æ¨¡åæ¶å°çæ¯ä¸å¼ è¡åéä½ãåå·®ä¸é½çã表ãã * <ul> * <li>{@code w:gridSpan} 横ååå¹¶çæ ¼åï¼POI åªè¿åä¸ä¸ª {@code XWPFTableCell}ï¼ * 被å®çä½çåå åå空æ¶å¤± â åä¸è¡çåä¸è¡¨å¤´å¯¹ä¸ä¸ï¼</li> * <li>{@code w:vMerge} 纵ååå¹¶çç»æ ¼ï¼POI åå°çæåæ¯ç©ºä¸² â åç»ååªå¨ç¬¬ä¸è¡åºç°ï¼ * åé¢å è¡çèµ·æ¥æ¯ã没æåç»çç¬ç«æ£éªé¡¹ãï¼æ¨¡åèªç¶æ³ä¸å°è¿æ¯ä¸¤å±ç»æã</li> * </ul> * åæä¸åªæ¯æçé¾çï¼æ¨¡åçä¸å°ä¸¤å±ç»æï¼å°±ä¸ä¼å»æãåç»æ£éªé¡¹è¡¨ãç»ä»¶ï¼ * åªè½éèæ±å ¶æ¬¡æå¹³éºçæ£éªè¡¨ï¼æåç»ä¿¡æ¯æ´ä¸ªä¸¢æã * <p> * æä»¥è¿éæåå¹¶ä¿¡æ¯ç¿»è¯ææ¨¡åçå¾è§çåç¬¦ï¼ * <ul> * <li>{@code gridSpan=N} çæåè½å¨è¯¥ç»ç¬¬ 1 æ ¼ï¼å ¶åè¡¥ N-1 个空å ä½ï¼åä½ä¸è¡¨å¤´å¯¹é½ï¼</li> * <li>{@code vMerge} ç»æ ¼è¾åº {@link QcReportDocumentExtract#VERTICAL_MERGE_MARK}ï¼æè¯´ãåä¸ä¸è¡ãï¼</li> * <li>æ¯è¡å°¾é¨è¡¥é½å°æ´è¡¨ç½æ ¼å®½åº¦ï¼çè¡ä¸åè¢«å½æãåæ°å°±è¿ä¹å¤ãã</li> * </ul> * ç»æ ¼èªèº«å¸¦æåæ¶ä»¥æå为åï¼ç»æ ¼å¨ Word 鿬ä¸è¯¥æå 容ï¼çæå°±è¯´æå¶è¡¨ä¸è§èï¼ * æ¤æ¶ä¸¢æåæ¯ä¸¢ç»ææ´å¯æãæ´è¡¨ç½æ ¼å®½åº¦åææè¡çæå¤§å¼ï¼è䏿¯ç¬¬ä¸è¡çåæ°ââ * 表头常常带横ååå¹¶ï¼æè¡¨å¤´å®½åº¦å伿ä¸é¢å è¡æªæã */ private static void appendTable(StringBuilder sb, XWPFTable table) { List<List<String>> gridRows = new ArrayList<>(); int width = 0; for (XWPFTableRow row : table.getRows()) { List<String> grid = new ArrayList<>(); for (XWPFTableCell cell : row.getTableCells()) { grid.add(cellText(cell)); for (int i = 1; i < gridSpanOf(cell); i++) { grid.add(""); } } width = Math.max(width, grid.size()); gridRows.add(grid); } sb.append("[è¡¨æ ¼å¼å§]\n"); for (List<String> grid : gridRows) { while (grid.size() < width) { grid.add(""); } appendLine(sb, String.join("\t", grid)); } sb.append("[è¡¨æ ¼ç»æ]\n"); } /** åå æ ¼æåï¼ç©ºç½ä¸æ¯çºµååå¹¶ç»æ ¼æ¶ç»åº {@code âåä¸} è䏿¯ç©ºä¸² */ private static String cellText(XWPFTableCell cell) { String text = cell.getText().replaceAll("[\\r\\n]+", " ").trim(); if (text.isEmpty() && isVerticalMergeContinuation(cell)) { return QcReportDocumentExtract.VERTICAL_MERGE_MARK; } return text; } /** * æ¯å¦æ¯çºµååå¹¶çãç»æ ¼ãã * <p> * {@code <w:vMerge/>} ä¸å¸¦ val ä¸ {@code w:val="continue"} é½è¡¨ç¤ºç»æ ¼ï¼åè æ¯ Word ç常è§åæ³ï¼ï¼ * {@code w:val="restart"} æ¯åå¹¶çèµ·å§æ ¼ï¼å®å¸¦ççæ£çæåï¼ä¸ç®ç»æ ¼ã */ private static boolean isVerticalMergeContinuation(XWPFTableCell cell) { CTTcPr tcPr = cellPropertiesOf(cell); if (tcPr == null || !tcPr.isSetVMerge()) { return false; } CTVMerge vMerge = tcPr.getVMerge(); return !vMerge.isSetVal() || vMerge.getVal() == STMerge.CONTINUE; } /** 横ååå¹¶çåæ°ï¼éåå¹¶æ ¼ä¸º 1ï¼å¼å¼å¸¸æ¶ä¸å¾æ 1 å¤çï¼å®å¯å°è¡¥å ä½ä¹ä¸è¦å空å¤åºå */ private static int gridSpanOf(XWPFTableCell cell) { CTTcPr tcPr = cellPropertiesOf(cell); if (tcPr == null || !tcPr.isSetGridSpan()) { return 1; } BigInteger span = tcPr.getGridSpan().getVal(); return span == null || span.intValue() < 1 ? 1 : span.intValue(); } /** åæ ¼å±æ§ï¼POI 对没æ {@code <w:tcPr>} çåå æ ¼è¿å nullï¼ä¸è½ç´æ¥é¾å¼è°ç¨ */ private static CTTcPr cellPropertiesOf(XWPFTableCell cell) { CTTc ctTc = cell.getCTTc(); return ctTc == null ? null : ctTc.getTcPr(); } /** docx æ½åç»æï¼æ£æææ¬ + è¦è¿ warnings ç软æç¤º */ private record DocxRead(String text, List<String> notes) { } /** * 读 .xlsxã * <p> * ç¨ {@link DataFormatter} å<b>æ¾ç¤ºå¼</b>è䏿¯åå§å¼ï¼åå§å¼é {@code 1} ä¼åæ {@code 1.0}ã * æ¥æä¼åæä¸ä¸²åºåå·ï¼æ¨¡åçå°è¿äºåºæ¬åªè½ççãç¨æ·ç¼éçå°çæ¯ä»ä¹ï¼å°±è¯¥ç»æ¨¡åä»ä¹ã * <p> * è¡æ°æ {@code maxXlsxRows} å°é¡¶ï¼ç©ºç½è¡æ´è¡ä¸¢å¼ââè¶ å¤§è¡¨é大é空ç½è¡ä¼ææç¤ºè¯ç满åªå£°ã */ private String readXlsx(byte[] content, String originalFilename) { try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) { StringBuilder sb = new StringBuilder(); DataFormatter formatter = new DataFormatter(); for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) { Sheet sheet = workbook.getSheetAt(sheetIndex); sb.append("[å·¥ä½è¡¨: ").append(sheet.getSheetName()).append("]\n"); int lastRow = sheet.getLastRowNum(); int written = 0; for (int rowIndex = 0; rowIndex <= lastRow && written < properties.getMaxXlsxRows(); rowIndex++) { Row row = sheet.getRow(rowIndex); if (row == null) { continue; } List<String> cells = new ArrayList<>(); boolean hasValue = false; for (int cellIndex = 0; cellIndex < row.getLastCellNum(); cellIndex++) { Cell cell = row.getCell(cellIndex); String value = cell == null ? "" : formatter.formatCellValue(cell).trim(); if (!value.isEmpty()) { hasValue = true; } cells.add(value); } if (!hasValue) { continue; } appendLine(sb, String.join("\t", cells)); written++; } if (written >= properties.getMaxXlsxRows() && lastRow + 1 > written) { sb.append("[æ¬å·¥ä½è¡¨è¶ è¿ ").append(properties.getMaxXlsxRows()) .append(" è¡ï¼åç»å 容æªè¯»å]\n"); } } return sb.toString(); } catch (IOException | RuntimeException e) { log.warn("xlsx è§£æå¤±è´¥ï¼file={}", originalFilename, e); throw exception(AI_IMPORT_OFFICE_CORRUPT, originalFilename, "xlsx"); } } private String readPlainText(byte[] content) { if (startsWith(content, BOM_UTF_8)) { return new String(content, BOM_UTF_8.length, content.length - BOM_UTF_8.length, StandardCharsets.UTF_8); } if (content.length >= 2 && (content[0] & 0xFF) == 0xFF && (content[1] & 0xFF) == 0xFE) { return new String(content, 2, content.length - 2, StandardCharsets.UTF_16LE); } if (content.length >= 2 && (content[0] & 0xFF) == 0xFE && (content[1] & 0xFF) == 0xFF) { return new String(content, 2, content.length - 2, StandardCharsets.UTF_16BE); } // 没æ BOM æ¶ä¸å¾æ UTF-8 读ãGBK å¨è¿éä¼è¢«è¯»æä¹±ç ï¼ä½é£æ¯ã读åºä¸å é®å·ãè䏿¯å¤±è´¥ï¼ // ç¨æ·è½ä»é¢è§éçåºæ¥ï¼æ¯å¼ºè¡çç¼ç ç鿴坿§ return new String(content, StandardCharsets.UTF_8); } private static void appendLine(StringBuilder sb, String line) { if (line != null && !line.isBlank()) { sb.append(line).append('\n'); } } private static String extractorNameOf(String extension) { return "txt".equals(extension) || "csv".equals(extension) ? "plain-text" : "poi-" + extension; } private static boolean startsWith(byte[] content, byte[] prefix) { if (content.length < prefix.length) { return false; } for (int i = 0; i < prefix.length; i++) { if (content[i] != prefix[i]) { return false; } } return true; } private static String extensionOf(String filename) { int dot = filename == null ? -1 : filename.lastIndexOf('.'); return dot < 0 ? "" : filename.substring(dot + 1).toLowerCase(); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/aiimport/document/PdfImportAdapter.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,146 @@ package cn.iocoder.yudao.module.qcreport.service.aiimport.document; import cn.iocoder.yudao.module.qcreport.config.QcReportAiImportProperties; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.text.PDFTextStripper; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_FILE_EMPTY; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_TOO_MANY_PAGES; /** * PDF æ½åå¨ï¼åééã * <p> * <b>ééå¤å®ä¾æ®æ¯ãè¿ä»½ PDF ææ²¡æææ¬å±ãï¼ä¸æ¯ã宿¯ä¸æ¯ PDFãã</b> * è¿ä¸ªåºå«å¾å®é ï¼çµåç PDF æ½åºææ¬åªè¦å å KBã读å¾ä¹åï¼æ«æç PDF æ½åºæ¥æ¯ç©ºçï¼ * åªè½æé¡µé¢æ¸²ææå¾äº¤ç»å¤æ¨¡ææ¨¡åââææ¬é«ä¸ä¸ªæ°é级ï¼ä½è¿æ¯å¯ä¸è½è¯»åºåçåæ³ã * 夿®æ¯å¹³åæ¯é¡µå符æ°è¶ è¿éå¼ã * <p> * 廿<b>ä¸</b>åæ··åå PDF çé页éè·¯ï¼æçé¡µæææ¬å±ãæç没æï¼ï¼é£éè¦æé¡µå³å®ééï¼ * è°ç¨æ¬¡æ°ç¿»åèæ¶çåªå¨å°æ°æä»¶ä¸ãæ´ä»½æä»¶æä¸ä¸ªééèµ°ï¼è¡ä¸ºå¯é¢æã */ @Slf4j @Component @Order(2) public class PdfImportAdapter implements QcReportImportAdapter { @Resource private QcReportAiImportProperties properties; @Override public boolean supports(String extension, String contentType) { return "pdf".equals(extension) || "application/pdf".equalsIgnoreCase(contentType); } @Override public QcReportDocumentExtract extract(byte[] content, String originalFilename) { if (content == null || content.length == 0) { throw exception(AI_IMPORT_FILE_EMPTY, originalFilename, 0); } try (PDDocument document = Loader.loadPDF(content)) { int pageCount = document.getNumberOfPages(); if (pageCount <= 0) { throw exception(AI_IMPORT_FILE_EMPTY, originalFilename, 0); } // è¶ éç´æ¥æ¥éè䏿¯æªæï¼é黿ªæä¼è®©ç¨æ·ä»¥ä¸ºæ´ä»½æä»¶é½è¯å«è¿äºï¼ // æ¿å°ç模æ¿å°äºå 页崿²¡æä»»ä½æç¤ºï¼æ¯æ¥éå±é©å¾å¤ if (pageCount > properties.getMaxPagesPerFile()) { throw exception(AI_IMPORT_TOO_MANY_PAGES, originalFilename, pageCount, properties.getMaxPagesPerFile()); } List<String> pageTexts = extractPageTexts(document, pageCount); int totalChars = pageTexts.stream().mapToInt(t -> t.replaceAll("\\s", "").length()).sum(); int threshold = properties.getPdfTextPageThreshold() * pageCount; if (totalChars >= threshold) { return QcReportDocumentExtract.ofText(joinPages(pageTexts), pageCount, "pdfbox-text", List.of()); } // èµ°å¾çééï¼æã为ä»ä¹æ ¢/为ä»ä¹è´µãè®²æ¸ æ¥ï¼ç¨æ·æç¥éæ¢çµåç PDF è½å¿«å¾å¤ return renderPages(document, pageCount, originalFilename, List.of("æä»¶ã" + originalFilename + "ãæ²¡æææ¬å±ï¼å ± " + pageCount + " 页ï¼ï¼å·²ææ«æä»¶é页è¯å«ï¼ä¼æ¯è¾æ ¢")); } catch (IOException e) { log.warn("PDF è§£æå¤±è´¥ï¼file={}", originalFilename, e); throw exception(AI_IMPORT_FILE_EMPTY, originalFilename, 0); } } /** * éé¡µæ½ææ¬ãç¨ {@code setSortByPosition(true)} 让é è¯»é¡ºåºæçé¢åæ æï¼ * å¦åè¡¨æ ¼éçæå伿 PDF å é¨ç»å¶é¡ºåºä¹±åºæ½åºï¼æ¨¡åçå°çåå°±éä½äºã */ private List<String> extractPageTexts(PDDocument document, int pageCount) throws IOException { List<String> pageTexts = new ArrayList<>(pageCount); for (int i = 0; i < pageCount; i++) { PDFTextStripper stripper = new PDFTextStripper(); stripper.setSortByPosition(true); stripper.setStartPage(i + 1); stripper.setEndPage(i + 1); pageTexts.add(stripper.getText(document).trim()); } return pageTexts; } private String joinPages(List<String> pageTexts) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < pageTexts.size(); i++) { if (i > 0) { sb.append("\n--- 第 ").append(i + 1).append(" 页 ---\n"); } sb.append(pageTexts.get(i)); } return sb.toString(); } /** * éé¡µæ¸²ææ PNGã * <p> * 渲æå¤±è´¥ä¸åï¼åè½å»è¯ææ¬ééï¼ä¸¤æ¡è·¯é½ä¸éææ¥ãæªè§£æåºä»»ä½å¯ç¨å 容ãã * 乿以è¦åè½ï¼æ¯å 为ãå符æ°ä½äºéå¼ãå¯è½åªæ¯æä»¶ç¨çå使²¡å¸¦ ToUnicode 表 * ï¼æ½åºæ¥æ¯ä¹±ç æç©ºï¼ï¼ä½æ¸²ææ¬èº«è¿æ¯å¥½çââä¸è¿é£ç§æ åµä¸åè½åå°çææ¬ä¹æ¯ç©ºçï¼ * æä»¥çæ£ä¼è½å° {@link #extractPageTexts} çåºæ¯æ¯æ¸²æå¨å¯¹è¿ä¸ªæä»¶ä¸å·¥ä½ã */ private QcReportDocumentExtract renderPages(PDDocument document, int pageCount, String originalFilename, List<String> notes) { List<QcReportDocumentExtract.ImagePart> images = new ArrayList<>(pageCount); try { PDFRenderer renderer = new PDFRenderer(document); for (int i = 0; i < pageCount; i++) { BufferedImage image = renderer.renderImageWithDPI(i, properties.getRenderDpi(), ImageType.RGB); ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(image, "png", out); images.add(new QcReportDocumentExtract.ImagePart( out.toByteArray(), "image/png", "第 " + (i + 1) + " 页")); } return QcReportDocumentExtract.ofImages(images, pageCount, "pdfbox-render", notes); } catch (IOException | RuntimeException e) { log.warn("PDF æ¸²ææ«æé¡µå¤±è´¥ï¼å°è¯åè½ææ¬ééï¼file={}", originalFilename, e); try { List<String> pageTexts = extractPageTexts(document, pageCount); if (pageTexts.stream().anyMatch(t -> !t.isBlank())) { List<String> fallbackNotes = new ArrayList<>(notes); fallbackNotes.add("æä»¶ã" + originalFilename + "ãç页颿 æ³æ¸²ææå¾çï¼å·²æ¹ä¸ºè¯»åå ¶ææ¬å±"); return QcReportDocumentExtract.ofText(joinPages(pageTexts), pageCount, "pdfbox-text-fallback", fallbackNotes); } } catch (IOException ignored) { // åè½ä¹è¯»ä¸åºæ¥ï¼èµ°ä¸é¢çç»ä¸æ¥é } throw exception(AI_IMPORT_FILE_EMPTY, originalFilename, pageCount); } } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/aiimport/document/QcReportDocumentExtract.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,83 @@ package cn.iocoder.yudao.module.qcreport.service.aiimport.document; import java.util.List; /** * ä¸ä¸ªæä»¶è¢«éé 卿½ååçä¸é´ç»æã * <p> * <b>廿䏿¯æ ãä¹ä¸æ¯ HTML</b>ï¼å®åªæ¯ãè¿ä»½æä»¶è½äº¤ç»æ¨¡åçä¸è¥¿æ¯ä»ä¹ãè¿ä¸ªæ´å°çé®é¢ççæ¡ã * æ è¦è¡¨è¾¾çå¼ï¼HTML è¦è¡¨è¾¾æ ·å¼ï¼ä¸¤è é½ä¼æã模åè½çè§ä»ä¹ãæ¾å¤§å°ä¸å¯æ§ï¼è¿éåªç两个ééï¼ * è¿«ä½¿ä¸æ¸¸ï¼æç¤ºè¯æé ãè°ç¨è®¡åï¼ä¸å¿ åèè第ä¸ç§å½¢æã * * <h3>为ä»ä¹æ¯åééè䏿¯ä¸ç§</h3> * çµåç PDF / Word / Excel æç宿æ¬å±ï¼æ½åæå符串æç tokenï¼æ¨¡åä¹è¯»å¾æåï¼ * æ«æä»¶ä¸ç §çæ²¡æææ¬å±ï¼åªè½æé¡µé¢æ¸²ææå¾äº¤ç»å¤æ¨¡ææ¨¡åã * è¿ä¸¤æ¡è·¯çè°ç¨æ¹å¼ï¼{@code chat} ä¸ {@code chatWithImage}ï¼ã计费å£å¾ã失败模å¼é½ä¸åï¼ * æä»¥å¿ é¡»å¨ç±»åä¸åå¼ï¼è䏿¯å¡è¿ä¸ä¸ªå段éé çã * * @param channel ééç±»åï¼è§ {@link Channel} * @param text TEXT ééçæ£æï¼å¤é¡µæ¶é¡µé´ä»¥ {@code --- 第 N 页 ---} åéï¼IMAGES ééæä¸ºç©ºä¸² * @param images IMAGES ééçå¾çï¼æ¯é¡µä¸å¼ ï¼TEXT ééæä¸ºç©ºå表 * @param pageCount 页æ°ï¼ç¨äºè¶ éæ ¡éªä¸æç¤ºææ¡ * @param extractor å®é çæçæ½å卿 è¯ï¼å¦ {@code pdfbox-text}ï¼ï¼ä¼åè¿æ¥å¿ä¾¿äºææ¥ * @param notes æ½åé¶æ®µç软æç¤ºï¼ç´æ¥è¿ååºç warnings å±ç¤ºç»ç¨æ· */ public record QcReportDocumentExtract( Channel channel, String text, List<ImagePart> images, int pageCount, String extractor, List<String> notes) { /** * 纵ååå¹¶ç»æ ¼çå 使 è®°ã * <p> * å®å±äºãæ½ååºæ¥çææ¬é¿ä»ä¹æ ·ãè¿æ¡å¥çº¦ï¼æä»¥å {@code text} æ¾å¨ä¸å¤ï¼ * ç±ç产è ï¼{@link OfficeImportAdapter}ï¼ä¸è§£éè ï¼{@code QcReportTemplatePromptBuilder}ï¼ * å ±ç¨ä¸ä»½ï¼é¿å 两边ååä¸ä¸ªåé¢éååèªæ¼ç§»ã * <p> * 为ä»ä¹ä¸è½ç空ï¼{@code w:vMerge} çç»æ ¼å¨ Word é表示ãè¿ä¸æ ¼ä¸ä¸ä¸è¡æ¯åä¸ä¸ªå¼ãï¼ * ä½ POI ååºæ¥å°±æ¯ç©ºä¸²ãç空çè¯ï¼æ¨¡ååºåä¸åºãåå¹¶ç»æ ¼ãä¸ãåä»¶æ¬æ¥å°±æ²¡å¡«ãï¼ * äºæ¯æ´å¼ 表çèµ·æ¥æ¯ä¸å±å¹³éºçæ£éªé¡¹ï¼çä¸åºåç»ç»æã */ public static final String VERTICAL_MERGE_MARK = "âåä¸"; /** * æ½åééã */ public enum Channel { /** æææ¬å±ï¼æ´ä»½æä»¶æ¼æä¸æ®µææ¬ï¼ä¸æ¬¡æ¨¡åè°ç¨ */ TEXT, /** æ ææ¬å±ï¼æ«æä»¶/ç §çï¼ï¼é页转å¾ï¼æ¯é¡µä¸æ¬¡æ¨¡åè°ç¨ */ IMAGES } /** * IMAGES éééçä¸é¡µå¾ã * * @param bytes å¾çåèï¼æ§è¡å¨è´è´£è½¬æè£¸ base64 * @param mimeType å½¢å¦ {@code image/png} ç裸类åï¼ä¸å¸¦ {@code data:} åç¼ * @param label 页æ ç¾ï¼ç¨äºæ¼éè¯¯ä¸æç¤ºææ¡ï¼ä¾å¦ãæ«æä»¶.pdf 第 2 页ã */ public record ImagePart(byte[] bytes, String mimeType, String label) { } /** * ææ¬ééçå·¥åã{@code notes} å 许为空表示没æè½¯æç¤ºã */ public static QcReportDocumentExtract ofText(String text, int pageCount, String extractor, List<String> notes) { return new QcReportDocumentExtract(Channel.TEXT, text, List.of(), pageCount, extractor, notes); } /** * å¾çééçå·¥åã */ public static QcReportDocumentExtract ofImages(List<ImagePart> images, int pageCount, String extractor, List<String> notes) { return new QcReportDocumentExtract(Channel.IMAGES, "", List.copyOf(images), pageCount, extractor, notes); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/aiimport/document/QcReportDocumentExtractService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,52 @@ package cn.iocoder.yudao.module.qcreport.service.aiimport.document; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_FILE_UNSUPPORTED; /** * æ½åé¨é¢ï¼æåºå®é¡ºåºæ¾ç¬¬ä¸ä¸ªè½å¤ç该æä»¶çéé å¨ã * <p> * 顺åºç±åéé å¨ç {@code @Order} å³å®ï¼å¾ç â PDF â Officeï¼ï¼ä¸æ¯ãè°æ´åéãçæ©ä¼ï¼ * èæ¯æ¶é¤æ§ä¹çå®åºãçå®çæ§ä¹æ¯ PDFï¼å®æ¢å¯è½è¢« PDF éé å¨è®¤é¢ï¼ * ä¹å¯è½å 为 {@code contentType} 䏿¥æ {@code application/pdf} ä¹å¤çæªå¼èè½å° Office çå åºå¤æä¸ï¼ * å®åºä¹åè°å è°åæ¯ç¡®å®çï¼ä¸ä¼é Bean 注åé¡ºåºæ¼ç§»ã * <p> * åªåååï¼ä¸åã读ä¸åºæ¥å°±æ¢ä¸ªéé å¨åè¯ãçå åºââæ¯ä¸ªéé å¨å é¨å·²ç»æè¯¥è¯çè·¯å¾è¯è¿äº * ï¼PDF çæ¸²æå¤±è´¥ä¼åè½ææ¬ééï¼ï¼å¨è¿éåå ä¸å±åªä¼è®©å¤±è´¥åå åå¾è¯´ä¸æ¸ ã */ @Service public class QcReportDocumentExtractService { @Resource private List<QcReportImportAdapter> adapters; /** * æ½åæä»¶å 容ã * * @param content æä»¶åè * @param originalFilename åå§æä»¶åï¼å«æ©å±åï¼ * @param contentType ä¸ä¼ æ¶è®°å½ç MIME ç±»åï¼å¯è½ä¸º null * @throws cn.iocoder.yudao.framework.common.exception.ServiceException 没æéé å¨è®¤é¢è¯¥æ ¼å¼æ¶ */ public QcReportDocumentExtract extract(byte[] content, String originalFilename, String contentType) { String extension = extensionOf(originalFilename); for (QcReportImportAdapter adapter : adapters) { if (adapter.supports(extension, contentType)) { return adapter.extract(content, originalFilename); } } throw exception(AI_IMPORT_FILE_UNSUPPORTED, originalFilename, extension.isBlank() ? "æ " : "." + extension); } private static String extensionOf(String filename) { int dot = filename == null ? -1 : filename.lastIndexOf('.'); return dot < 0 ? "" : filename.substring(dot + 1).toLowerCase(); } } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/aiimport/document/QcReportImportAdapter.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,36 @@ package cn.iocoder.yudao.module.qcreport.service.aiimport.document; /** * ä¸ç§è¾å ¥æ ¼å¼çæ½åå¨ã * <p> * 乿以åéé å¨è䏿¯å¨ä¸ä¸ªå¤§ç±»é {@code switch (extension)}ï¼ä¸ç±»è¾å ¥çæ½æ³æ¯«æ å ±åç¹ * ï¼å¾çä»ä¹é½ä¸ç¨åãPDF è¦å å¤æææ²¡æææ¬å±ãOffice è¦ææ ¼å¼è§£æè¡¨æ ¼ï¼ï¼ * å¡è¿ä¸ä¸ªç±»ä¼å¾å°ä¸ä¸ªæ¢è¯»ä¸äºä¹æ¹ä¸å¨ç忝å¡ã * <p> * å®ç°æ {@code @Order} åºå®é¡ºåºæåï¼é¨é¢å<b>第ä¸ä¸ª</b> {@link #supports} 为ççââ * 顺åºåºå®æ¯ä¸ºäºæ¶é¤æ§ä¹ï¼PDF å¯è½åæ¶è¢« PDF éé å¨å Office éé å¨ã认é¢ãï¼ï¼ * è䏿¯ä¸ºäºæ©ä¼ã */ public interface QcReportImportAdapter { /** * æ¯å¦è½å¤çè¿ä¸ªæä»¶ã * * @param extension å°åãå»ç¹çæ©å±åï¼å¦ {@code pdf}ï¼æ²¡ææ©å±åæ¶ä¸ºç©ºä¸² * @param contentType æµè§å¨ä¸æ¥ç MIME ç±»åï¼å¯è½ä¸º nullï¼æ¬å°æä»¶å¸¸è§ï¼ */ boolean supports(String extension, String contentType); /** * æ½åã * <p> * ä¸åãè¿å null 表示失败ãè¿ç§çº¦å®ï¼è¯»ä¸åºå å®¹ãæ ¼å¼ä¸æ¯æã页æ°è¶ é齿¯**ç¡®å®ç失败åå **ï¼ * åèªå¯¹åºä¸ä¸ªé误ç ï¼è®©ç¨æ·ç¥éä¸ä¸æ¥è¯¥åä»ä¹ï¼æ¢ä¸ªæ ¼å¼ / æé¡µ / éæ°æ«æï¼ã * * @param content æä»¶åè * @param originalFilename åå§æä»¶åï¼ç¨äºéè¯¯ä¸æç¤ºææ¡ * @return æ½åç»æ */ QcReportDocumentExtract extract(byte[] content, String originalFilename); } yudao-module-qcreport/src/main/java/cn/iocoder/yudao/module/qcreport/service/aiimport/llm/QcReportAiDraftNormalizer.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,227 @@ package cn.iocoder.yudao.module.qcreport.service.aiimport.llm; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo.QcReportAiDraftRespVO; import cn.iocoder.yudao.module.qcreport.controller.admin.aiimport.vo.QcReportComponentSpecVO; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; /** * ææ¨¡åè§£æåºæ¥çè稿å½ä¸æãåªå«åæ³ç»ä»¶ã忳屿§ãçæ¸ åã * <p> * ä¸çº§é²çº¿ç第ä¸çº§ï¼ä¹æ¯æ¢æèå¼ï¼ERP / CRM / MES ç AI è°ç¨ï¼<b>没æ</b>ç䏿®µã * é£ä¸å¤æ¯æå¹³åå±ç»æãåæ®µåæ»å¨ä»£ç éï¼éäºå°±æ¯éäºï¼è¿æ¬¡è¦åç»ä¸å¯ä¿¡ç模åå»äº§åºåµå¥ç»æï¼ * å°è¿ä¸å±å°±ä¼åºç°ã模åç¼äºä¸ªç»ä»¶ç±»åï¼åç«¯è£ é å¨è·³è¿å®ï¼ç¨æ·çå°çç»å¸åé¢è§å¯¹ä¸ä¸ãã * * <h3>ååï¼å个å ç´ åæä¸è½è®©æ´ä¸ªè¯·æ±å¤±è´¥</h3> * åå ç´ è¿ warningsãå ¶ä½çç §å¸¸è¿åãæ´ä½å¤±è´¥åªå¨æå¡å±å¤æï¼æç»ä¸é¡¹é½ä¸å© â DRAFT_EMPTYï¼ï¼ * å 为ã大é¨åè¯å«å¯¹äºãåãå ¨é½æ²¡è¯å«åºæ¥ãå¯¹ç¨æ·çå«ä¹å®å ¨ä¸åã * * <h3>为ä»ä¹å端ä¹è¦è¿æ»¤ä¸éï¼åç«¯è£ é å¨å·²ç»è¿æ»¤äºï¼</h3> * åç«¯è¿æ»¤åçå¨è£ é é£ä¸å»ï¼ç¨æ·çå°çæ¯ãæä¸ä¼ äº 5 个ç»ä»¶ï¼ç»å¸ä¸åªåºæ¥ 3 个ãââæ²¡æè§£éã * å端å¨é¢è§é¶æ®µå°±æãåªä¸é¡¹è¢«ä¸¢äºã为ä»ä¹ãè®²åºæ¥ï¼ç¨æ·æç¥é该补ä»ä¹ã * è¿ä¸æ¯éå¤é²çº¿ï¼æ¯æéé»å¤±è´¥åæå¯è§å¤±è´¥ã */ public class QcReportAiDraftNormalizer { private QcReportAiDraftNormalizer() { } /** * å½ä¸ç»æã * * @param components è¿æ»¤åçç»ä»¶ï¼å¯è½ä¸ºç©ºï¼ç©ºçæç»ç±æå¡å±æ¥ DRAFT_EMPTYï¼ * @param warnings éè¦å±ç¤ºç»ç¨æ·ç软æç¤º */ public record Result(List<QcReportAiDraftRespVO.DraftComponent> components, List<String> warnings) { } /** * å½ä¸ä¸æ¬¡æ¨¡ååå¤å¾å°çç»ä»¶ã * * @param raw è§£æåºæ¥çåå§ç»ä»¶ï¼å è®¸å« null å ç´ * @param catalog å¯ç¨ç»ä»¶æ¸ åï¼å³å®åªäº type ä¸ prop key åæ³ * @param sourceLabel æ¥æºæ ç¾ï¼æä»¶å / æä»¶å+页ç ï¼ï¼ä¼æ¼è¿æç¤ºææ¡ */ public static Result normalize(List<QcReportAiDraftRespVO.DraftComponent> raw, List<QcReportComponentSpecVO> catalog, String sourceLabel) { Map<String, QcReportComponentSpecVO> specByType = new TreeMap<>(); for (QcReportComponentSpecVO spec : catalog) { if (spec != null && StrUtil.isNotBlank(spec.getType())) { specByType.put(spec.getType(), spec); } } List<QcReportAiDraftRespVO.DraftComponent> kept = new ArrayList<>(); List<String> warnings = new ArrayList<>(); String prefix = StrUtil.isBlank(sourceLabel) ? "" : sourceLabel + "ï¼"; if (raw == null) { return new Result(kept, warnings); } for (int i = 0; i < raw.size(); i++) { QcReportAiDraftRespVO.DraftComponent item = raw.get(i); int no = i + 1; if (item == null) { warnings.add(prefix + "第 " + no + " 项䏿¯ä¸ä¸ªææçç»ä»¶å¯¹è±¡ï¼å·²å¿½ç¥"); continue; } String type = item.getType(); if (StrUtil.isBlank(type)) { warnings.add(prefix + "第 " + no + " 项缺å°ç»ä»¶ç±»åï¼å·²å¿½ç¥"); continue; } QcReportComponentSpecVO spec = specByType.get(type); if (spec == null) { warnings.add(prefix + "AI è¾åºäºç»ä»¶æ¸ åå¤çç»ä»¶ç±»åã" + type + "ãï¼å·²å¿½ç¥"); continue; } Map<String, Object> props = item.getProps(); if (props == null || props.isEmpty()) { warnings.add(prefix + "ç»ä»¶ã" + displayName(spec) + "ãæªè¯å«åºä»»ä½å±æ§ï¼å·²å¿½ç¥"); continue; } Map<String, Object> filtered = filterProps(props, spec, prefix, warnings); if (filtered.isEmpty()) { warnings.add(prefix + "ç»ä»¶ã" + displayName(spec) + "ãç屿§é½ä¸å¨æ¸ å声æèå´å ï¼å·²å¿½ç¥"); continue; } warnMissingRequired(filtered, spec, prefix, warnings); QcReportAiDraftRespVO.DraftComponent draft = new QcReportAiDraftRespVO.DraftComponent(); draft.setType(type); draft.setProps(filtered); kept.add(draft); } return new Result(kept, warnings); } /** * å并夿¬¡è°ç¨çç»æå¹¶å»éï¼æåæä¸éæªæã * <p> * å»éæ¯å¤é¡µæ«æä»¶çå¿ éåï¼é¡µçãæ¥åæ é¢ãè¡¨å¤´å¨æ¯ä¸é¡µé½ä¼åºç°ï¼é页è¯å«å°±ä¼å¾å° N ä»½ï¼ * ä¸å»éçè¯ç»å¸ä¸ä¼æ 5 ä¸ªä¸æ ·çæ¬å¤´ãå¤å®å䏿¯ * {@code (type, props çç¨³å® JSON)}ââprops å æ key æåºï¼å 为ä¸å页è¿åç key é¡ºåº * ä¸ä¿è¯ä¸è´ï¼ç´æ¥æ¯åå§é¡ºåºä¼æåä¸ä¸ªç»ä»¶å½æä¸¤ä¸ªã * * @param perCallResults 忬¡è°ç¨å½ä¸åçç»æï¼ææ§è¡é¡ºåº * @param maxComponents ç»ä»¶æ»æ°ä¸éï¼è¶ åºæªæ */ public static Result mergeAndDedup(List<Result> perCallResults, int maxComponents) { List<QcReportAiDraftRespVO.DraftComponent> merged = new ArrayList<>(); Set<String> seen = new LinkedHashSet<>(); for (Result result : perCallResults) { for (QcReportAiDraftRespVO.DraftComponent component : result.components()) { if (seen.add(dedupKey(component))) { merged.add(component); } } } int total = merged.size(); List<String> warnings = new ArrayList<>(); for (Result result : perCallResults) { warnings.addAll(result.warnings()); } if (perCallResults.size() > 1 && total < countRaw(perCallResults)) { warnings.add(StrUtil.format( "å¤é¡µè¯å«å ±å¾å° {} 项ç»ä»¶ï¼å ¶ä¸æ {} 项å¨å ¶å®é¡µå·²åºç°ï¼é¡µçãæ é¢ã表头çéå¤å 容ï¼ï¼å·²åå¹¶", countRaw(perCallResults), countRaw(perCallResults) - total)); } if (maxComponents > 0 && total > maxComponents) { warnings.add(StrUtil.format("è¯å«åºçç»ä»¶å ± {} 项ï¼è¶ è¿å次ä¸é {} 项ï¼å·²ä¿çå {} 项ï¼å ¶ä½è¯·æå·¥è¡¥å ", total, maxComponents, maxComponents)); merged = new ArrayList<>(merged.subList(0, maxComponents)); } return new Result(merged, warnings); } private static int countRaw(List<Result> results) { return results.stream().mapToInt(r -> r.components().size()).sum(); } /** * ä¸¢ææ¸ åæªå£°æç prop keyã * <p> * {@code fields} 为 <b>null</b>ï¼æ¸ ååæ ¹æ²¡å£°æï¼ä¸ä¸º <b>空å表</b>ï¼æç¡®å£°æãè¿ä¸ªç»ä»¶ä¸åä»»ä½å±æ§ãï¼ * æ¯ä¸¤åäºï¼åè æ ä»å¤æï¼åæ ·æ¾è¡ï¼åè ææ¯ççè¦ä¸ä¸ªä¸ªä¸¢æã * 混为ä¸è°ä¼å¨å端æ¼ä¼ fields æ¶æç¨æ·è¯å«åºæ¥ç屿§é黿¹å¹³ã */ private static Map<String, Object> filterProps(Map<String, Object> props, QcReportComponentSpecVO spec, String prefix, List<String> warnings) { if (spec.getFields() == null) { return props; } Set<String> allowed = new LinkedHashSet<>(); for (QcReportComponentSpecVO.Field field : spec.getFields()) { if (field != null && StrUtil.isNotBlank(field.getKey())) { allowed.add(field.getKey()); } } Map<String, Object> filtered = new TreeMap<>(); List<String> dropped = new ArrayList<>(); for (Map.Entry<String, Object> entry : props.entrySet()) { if (allowed.contains(entry.getKey())) { filtered.put(entry.getKey(), entry.getValue()); } else { dropped.add(entry.getKey()); } } if (!dropped.isEmpty()) { warnings.add(prefix + "ç»ä»¶ã" + displayName(spec) + "ãä¸ä¸å±äºè¯¥ç»ä»¶ç屿§ " + String.join("ã", dropped) + " 已丢å¼"); } return filtered; } private static void warnMissingRequired(Map<String, Object> props, QcReportComponentSpecVO spec, String prefix, List<String> warnings) { if (spec.getFields() == null) { return; } for (QcReportComponentSpecVO.Field field : spec.getFields()) { if (field == null || !Boolean.TRUE.equals(field.getRequired())) { continue; } if (isBlankValue(props.get(field.getKey()))) { warnings.add(prefix + "ç»ä»¶ã" + displayName(spec) + "ã缺å°å¿ 填屿§ã" + fieldLabel(field) + "ãï¼è¯·å¨è®¾è®¡å¨å±æ§é¢æ¿ä¸è¡¥ä¸ååä¿å"); } } } private static boolean isBlankValue(Object value) { if (value == null) { return true; } if (value instanceof CharSequence cs) { return StrUtil.isBlank(cs.toString()); } return value instanceof Collection<?> collection && collection.isEmpty(); } /** * å»éé®ï¼type + props ç稳å®åºååã * <p> * props å æ key æåºåç¨ {@link TreeMap}ï¼ä¿è¯ãåæ ·çç»ä»¶ãä¸åç key 顺åºãä¹è½å¤ä¸ºåä¸ä¸ªã */ private static String dedupKey(QcReportAiDraftRespVO.DraftComponent component) { Map<String, Object> props = component.getProps(); return component.getType() + "