Compare commits
53 Commits
4416c20b77
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 65898478ea | |||
| 7e3a90a2b8 | |||
| 00f718fbda | |||
| 6e6199bd26 | |||
| cacff6f63a | |||
| 963c5e41c8 | |||
| c9344a2715 | |||
| 6d5cfaf29a | |||
| 0839ba92da | |||
| 0adae3e06d | |||
| 573ce012e7 | |||
| 520c8b19d0 | |||
| f25a93b539 | |||
| 2d5183d2bd | |||
| 4e14334030 | |||
| e362f530ea | |||
| bd5bfbac2d | |||
| bb6cc6e241 | |||
| 9de75d2f25 | |||
| 0af774ae9d | |||
| 23cdfa8c2b | |||
| 1210b926c3 | |||
| 83e801a0b8 | |||
| c2cae5665e | |||
| c8924c625c | |||
| 9a4f51d378 | |||
| 40adf50702 | |||
| 751df5c4a9 | |||
| 93ad5e8876 | |||
| 1952d75f13 | |||
| b444303055 | |||
| 1e5ce9725b | |||
| 1144a86d02 | |||
| 4dfc418fc5 | |||
| 339d415322 | |||
| d600cbf285 | |||
| a364e1de81 | |||
| 60e2f520ba | |||
| 83c7da7517 | |||
| aa1d8a6c52 | |||
| 960312b088 | |||
| 7c1aa7d934 | |||
| 74f3f03d2c | |||
| 2befd44430 | |||
| 43a0542a11 | |||
| 9bb011e429 | |||
| 87ead4fa6a | |||
| da79640259 | |||
| c9f003e1b7 | |||
| 067880bf2e | |||
| 6467fd4ae5 | |||
| 70614dff5e | |||
| b280c2b453 |
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git submodule *)",
|
||||
"Bash(python -c \"import py_compile; py_compile.compile\\('scripts/init_kb.py', doraise=True\\); print\\('init_kb.py OK'\\)\")",
|
||||
"Bash(python -c \"import py_compile; py_compile.compile\\('agent/nodes.py', doraise=True\\); print\\('nodes.py OK'\\)\")",
|
||||
"Bash(python -c \"import py_compile; py_compile.compile\\('backend/embeddings.py', doraise=True\\); print\\('embeddings.py OK'\\)\")",
|
||||
"Bash(python *)",
|
||||
"Bash(PYTHONIOENCODING=utf-8 python batch_chunker.py jrxml_source)",
|
||||
"Bash(taskkill /F /IM python.exe)",
|
||||
"Bash(pkill -f embed_chunks)",
|
||||
"Bash(pip show *)",
|
||||
"Bash(streamlit run *)",
|
||||
"Bash(curl -s http://localhost:8001/validate -X POST -H \"Content-Type: application/json\" -d '{\"jrxml\":\"\"}')",
|
||||
"Bash(STREAMLIT_SERVER_HEADLESS=true streamlit run app.py --server.port 8501)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit -m ' *)",
|
||||
"Bash(pip install *)",
|
||||
"Bash(git push *)",
|
||||
"Bash(claude mcp *)",
|
||||
"mcp__zai-mcp-server__extract_text_from_screenshot",
|
||||
"mcp__MiniMax__understand_image",
|
||||
"Bash(curl -s http://localhost:8001/health)",
|
||||
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8501)",
|
||||
"Bash(curl -s -X POST http://localhost:8001/validate -H \"Content-Type: application/json\" -d \"{\\\\\"jrxml\\\\\": \\\\\"\\\\\"}\")",
|
||||
"Bash(curl -s -X POST http://localhost:8001/validate -H \"Content-Type: application/json\" -d \"{\\\\\"jrxml\\\\\": \\\\\"<?xml version=\\\\\\\\\\\\\"1.0\\\\\\\\\\\\\"?><jasperReport name=\\\\\\\\\\\\\"test\\\\\\\\\\\\\" pageWidth=\\\\\\\\\\\\\"595\\\\\\\\\\\\\" pageHeight=\\\\\\\\\\\\\"842\\\\\\\\\\\\\"><queryString><![CDATA[SELECT 1]]></queryString></jasperReport>\\\\\"}\")",
|
||||
"Bash(curl -s -X POST http://localhost:8001/validate -H \"Content-Type: application/json\" -d \"{\\\\\"jrxml\\\\\": \\\\\"<?xml version=\\\\\\\\\\\\\"1.0\\\\\\\\\\\\\"?><jasperReport name=\\\\\\\\\\\\\"test\\\\\\\\\\\\\" pageWidth=\\\\\\\\\\\\\"595\\\\\\\\\\\\\" pageHeight=\\\\\\\\\\\\\"842\\\\\\\\\\\\\"><queryString><![CDATA[SELECT 1]]></queryString><detail><band height=\\\\\\\\\\\\\"50\\\\\\\\\\\\\"/></detail></jasperReport>\\\\\"}\")",
|
||||
"Bash(curl -s -X POST http://localhost:8001/validate -H 'Content-Type: application/json' -d '{\"jrxml\": \"<?xml version=\\\\\"1.0\\\\\"?><jasperReport name=\\\\\"test\\\\\" pageWidth=\\\\\"595\\\\\" pageHeight=\\\\\"842\\\\\"><queryString><![CDATA[SELECT name FROM users]]></queryString><field name=\\\\\"name\\\\\" class=\\\\\"java.lang.String\\\\\"/><detail><band height=\\\\\"50\\\\\"><textField><reportElement x=\\\\\"0\\\\\" y=\\\\\"0\\\\\" width=\\\\\"100\\\\\" height=\\\\\"20\\\\\"/><textFieldExpression><![CDATA[$F{name}]]></textFieldExpression></textField></band></detail></jasperReport>\"}')",
|
||||
"Bash(curl -s -o /dev/null -w \"Streamlit: %{http_code}\\\\n\" http://localhost:8501)",
|
||||
"Bash(grep -v \"Complete$\")",
|
||||
"Bash(git pull *)",
|
||||
"Bash(pip search *)",
|
||||
"Read",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Bash",
|
||||
"Git",
|
||||
"Npm",
|
||||
"Pip",
|
||||
"Grep",
|
||||
"Glob",
|
||||
"Bash(rm -rf components/* assets/* style.css)",
|
||||
"Bash(mkdir -p api stores components utils)"
|
||||
]
|
||||
}
|
||||
}
|
||||
+42
-4
@@ -2,12 +2,20 @@
|
||||
LLM_BACKEND=cloud
|
||||
|
||||
# 云端提供商:openai 或 anthropic
|
||||
LLM_PROVIDER=openai
|
||||
LLM_PROVIDER=anthropic
|
||||
|
||||
# 云端配置(OpenAI 兼容)
|
||||
# Anthropic 兼容 API(MiniMax 等,优先使用)
|
||||
ANTHROPIC_API_KEY=sk-xxxx
|
||||
ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic
|
||||
|
||||
# OpenAI 兼容 API(fallback,当 ANTHROPIC_* 未设置时使用)
|
||||
OPENAI_API_KEY=sk-xxxx
|
||||
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
LLM_MODEL=gpt-4o
|
||||
|
||||
LLM_MODEL=MiniMax-M2.7
|
||||
|
||||
# 默认 max_tokens(各生成节点可覆盖为更高值)
|
||||
LLM_MAX_TOKENS=8192
|
||||
|
||||
# 本地大语言模型(Ollama)
|
||||
LOCAL_LLM_MODEL=qwen2.5-coder:7b
|
||||
@@ -22,8 +30,28 @@ VALIDATION_SERVICE_URL=http://localhost:8001/validate
|
||||
# Chroma 持久化目录
|
||||
CHROMA_PERSIST_DIR=./db/chroma
|
||||
|
||||
# ---- RAG / 向量知识库 (rag_jrxml 子模块) ----
|
||||
# 嵌入模型
|
||||
RAG_EMBED_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
||||
# JRXML 模板源目录 (rag 子模块内已含 107 个模板)
|
||||
RAG_JRXML_SOURCE=./rag/jrxml_source
|
||||
# 分块输出目录
|
||||
RAG_CHUNKER_OUTPUT=./rag/jrxml_chunker_output
|
||||
# 向量输出目录
|
||||
RAG_EMBEDDINGS_DIR=./rag/embeddings
|
||||
# ChromaDB 知识库路径
|
||||
RAG_CHROMA_PATH=./db/chroma
|
||||
# ChromaDB 集合名称
|
||||
RAG_COLLECTION_NAME=jrxml_chunks
|
||||
# GPU 加速
|
||||
RAG_USE_GPU=true
|
||||
# FP16 半精度
|
||||
RAG_USE_FP16=true
|
||||
# 向量化批处理大小
|
||||
RAG_BATCH_SIZE=64
|
||||
|
||||
# 最大自动修正尝试次数
|
||||
MAX_RETRY=3
|
||||
MAX_RETRY=5
|
||||
|
||||
# 上下文压缩阈值(token 数)
|
||||
CONTEXT_MAX_TOKENS=6000
|
||||
@@ -34,8 +62,18 @@ CONTEXT_KEEP_RECENT=4
|
||||
# 会话持久化目录
|
||||
SESSIONS_DIR=./sessions
|
||||
|
||||
# 日志目录和级别
|
||||
LOG_DIR=./logs
|
||||
LOG_LEVEL=DEBUG
|
||||
|
||||
# 状态快照保留数量(用于撤销操作)
|
||||
HISTORY_MAX_SNAPSHOTS=10
|
||||
|
||||
# 意图识别模型(默认使用主 LLM 模型)
|
||||
# INTENT_MODEL=gpt-4o-mini
|
||||
|
||||
# OCR 字段提取配置
|
||||
# 是否使用 GPU 加速 OCR(需要 CUDA 驱动和 GPU 版 EasyOCR/PaddleOCR)
|
||||
OCR_USE_GPU=false
|
||||
# OCR 文本置信度最低阈值(0-1),低于此值的元素将被忽略
|
||||
OCR_CONFIDENCE_THRESHOLD=0.5
|
||||
|
||||
+33
@@ -11,6 +11,34 @@ dist/
|
||||
# 数据库
|
||||
db/chroma/
|
||||
sessions/
|
||||
logs/
|
||||
db/
|
||||
# 自动评测 (Mavis AI)
|
||||
.mavis/
|
||||
|
||||
# 上传文件
|
||||
uploads/
|
||||
|
||||
# Java JARs & compiled classes
|
||||
lib/java/*.jar
|
||||
lib/java/*.class
|
||||
|
||||
# 渲染临时文件
|
||||
tmp/
|
||||
|
||||
# OCR 临时输出
|
||||
ocr_raw_positions.json
|
||||
|
||||
# Playwright E2E 测试产物
|
||||
frontend/test-results/
|
||||
|
||||
# RAG 管线中间产物 (rag 子模块内)
|
||||
rag/jrxml_chunker_output/
|
||||
rag/embeddings/
|
||||
rag/models/
|
||||
rag/__pycache__/
|
||||
rag/chroma_db/
|
||||
rag/jrxml_source_chunks/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
@@ -21,3 +49,8 @@ sessions/
|
||||
# 系统文件
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
db/chroma-bak/chroma.sqlite3
|
||||
db/chroma-bak/ec8b65c1-913e-4fa3-b073-b7663c97cf15/data_level0.bin
|
||||
db/chroma-bak/ec8b65c1-913e-4fa3-b073-b7663c97cf15/header.bin
|
||||
db/chroma-bak/ec8b65c1-913e-4fa3-b073-b7663c97cf15/length.bin
|
||||
db/chroma-bak/ec8b65c1-913e-4fa3-b073-b7663c97cf15/link_lists.bin
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "rag"]
|
||||
path = rag
|
||||
url = http://www.1415243231.top:8418/panda/rag_jrxml.git
|
||||
@@ -0,0 +1,585 @@
|
||||
# CLAUDE.md — JRXML 生成代理
|
||||
|
||||
## 项目概述
|
||||
|
||||
一个**本地桌面应用**,通过自然语言多轮对话帮助非技术用户创建 JasperReports 模板(JRXML 文件)。核心技术栈:Vue 3 前端 + FastAPI SSE 后端 + LangGraph 状态机 + LLM 生成/修改 + 自动验证修正循环。
|
||||
|
||||
**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML → 自动验证 → 失败则自动修正(最多5次) → 重试耗尽后失败上下文自动注入下一轮 → 返回可编译的 JRXML 文件。
|
||||
|
||||
## 启动命令
|
||||
|
||||
**方式 1 — 一键启动(Windows)**:双击 `start.bat`,自动打开三个窗口分别运行验证服务、后端 API、前端开发服务器。停止用 `stop.bat`。
|
||||
|
||||
**方式 2 — 手动启动**:
|
||||
```bash
|
||||
# 终端 1 — 验证服务(必须先启动)
|
||||
python -m uvicorn validation_service.main:app --port 8001 --host 0.0.0.0
|
||||
|
||||
# 终端 2 — 后端 API(SSE + REST)
|
||||
python -m uvicorn api_server:app --port 8000 --host 0.0.0.0
|
||||
|
||||
# 终端 3 — 前端开发服务器
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
浏览器打开 `http://localhost:5173`。
|
||||
|
||||
## 当前配置(.env)
|
||||
|
||||
- **OCR**: PaddleOCR(精确识别首选,ppocr-v4)→ EasyOCR(回退,ch_sim+en),两者均未安装时仅返回图片元信息
|
||||
- **LLM**: `cloud` / `anthropic` → MiniMax Anthropic 兼容 API (`MiniMax-M2.7`)
|
||||
- Base URL: `https://api.minimaxi.com/anthropic`
|
||||
- 认证: Anthropic SDK 自动读取 `ANTHROPIC_API_KEY`(fallback `OPENAI_API_KEY`)
|
||||
- **嵌入模型**: `local` / `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2`
|
||||
- **向量库**: ChromaDB 持久化在 `./db/chroma`
|
||||
- **验证服务**: FastAPI `localhost:8001`
|
||||
- **日志**: JSON 格式化,`logs/app.log` + `logs/llm.log`,中国时区 (UTC+8)
|
||||
- **MAX_RETRY**: 5
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
前端 (Vue 3 + Vite, 端口 5173)
|
||||
│ src/
|
||||
│ ├── api/client.ts SSE 客户端 + fetch 封装
|
||||
│ ├── stores/chat.ts Pinia: 消息/流式/节点进度
|
||||
│ ├── stores/session.ts Pinia: 会话管理
|
||||
│ ├── stores/kb.ts Pinia: KB 状态管理(多租户知识库)
|
||||
│ ├── components/
|
||||
│ │ ├── Sidebar.vue 会话列表 + 下载 + 历史版本
|
||||
│ │ ├── ChatMessages.vue 消息列表渲染
|
||||
│ │ ├── ProcessSection.vue 过程折叠区(替代 StreamingMessage + NodeProgress)
|
||||
│ │ ├── UnifiedInput.vue 统一输入框(文本+文件拖拽/粘贴/芯片,含 .jrxml)
|
||||
│ │ ├── SummaryCard.vue 结果摘要卡片(含耗时)
|
||||
│ │ ├── KbSelector.vue KB 下拉选择器(对话中切换知识库)
|
||||
│ │ └── KbManager.vue KB 管理面板(创建/上传/构建/删除)
|
||||
│ └── utils/format.ts 工具函数
|
||||
│
|
||||
▼ HTTP + SSE (Server-Sent Events)
|
||||
│
|
||||
api_server.py (FastAPI, 端口 8000)
|
||||
│ POST /api/sessions/{id}/chat → SSE 流式响应
|
||||
│ CRUD /api/sessions/... → 会话管理
|
||||
│ POST /api/upload → 文件上传
|
||||
│ GET /api/download/... → JRXML 下载
|
||||
│ GET /api/health, /api/config
|
||||
│
|
||||
│ 包装 LangGraph Agent(不变)──► agent/
|
||||
▼
|
||||
validation_service/ (FastAPI, 端口 8001) — 不变
|
||||
```
|
||||
|
||||
## 关键文件映射
|
||||
|
||||
| 文件 | 职责 | 修改频率 |
|
||||
|------|------|---------|
|
||||
| `api_server.py` | FastAPI SSE 后端,REST API + 流式推送 | **高** |
|
||||
| `frontend/src/` | Vue 3 聊天 UI(替代旧 app.py) | **高** |
|
||||
| `agent/state.py` | AgentState 类型定义(~40 字段) | 低 |
|
||||
| `agent/nodes.py` | 18 个工作流节点 + 流式生成 + 错误记录 | **高** |
|
||||
| `agent/graph.py` | 状态图编译 + 路由函数 + node_start 回调 | 中 |
|
||||
| `prompts/loader.py` | Prompt 加载器(从 .md 文件热重载) | 低 |
|
||||
| `prompts/*.md` | 10 个独立 Prompt 模板 | **高** |
|
||||
| `backend/llm.py` | LLM 工厂,统一 `_BaseLLM` 接口(invoke + stream)+ `_LLMLoggingWrapper` | 中 |
|
||||
| `backend/logger.py` | 集中日志模块:JSON 格式化 + trace_id + 独立 llm.log | 低 |
|
||||
| `backend/rag_adapter.py` | RAGSearcher 单例,语义搜索接口 | 中 |
|
||||
| `backend/error_kb.py` | ErrorKB — 错误指纹去重 + ChromaDB 持久化 + 语义检索 | 中 |
|
||||
| `backend/file_parser.py` | 文件解析: PDF/DOCX/XLSX/XLS/DOC/图片(EasyOCR→PaddleOCR回退)/文本 | 中 |
|
||||
| `backend/layout_analyzer.py` | A4模板分析: 比例检测/EasyOCR→PaddleOCR元素提取/行分组/JRXML行匹配/布局schema提取 | 中 |
|
||||
| `backend/ocr_extractor.py` | OCR字段精确提取: 4策略(exact→kv_pair→regex→table_match) + 置信度 | 中 |
|
||||
| `backend/annotation_detector.py` | 批注检测: 圈选(cv2 HoughCircles) + 箭头(HoughLinesP聚类) + OCR关联 + LLM格式化 | 中 |
|
||||
| `backend/embeddings.py` | 嵌入模型工厂 (HuggingFace/OpenAI) | 低 |
|
||||
| `backend/validation.py` | 验证服务 HTTP 客户端 | 低 |
|
||||
| `backend/session.py` | 会话 JSON 文件 CRUD(含 kb_id) | 低 |
|
||||
| `backend/kb_manager.py` | 用户+知识库 CRUD(多租户,原子 JSON 持久化) | 中 |
|
||||
| `backend/kb_searcher.py` | 知识库隔离搜索 + 模板检索(per-KB ChromaDB) | 中 |
|
||||
| `backend/kb_parser.py` | KB 解析管道:文件解析→字段提取→chunk切割→向量嵌入 | 中 |
|
||||
| `backend/field_matcher.py` | OCR↔KB 字段匹配:Embedding 粗筛 + LLM 精确确认 | 中 |
|
||||
| `agent/datasource.py` | 数据源模式解析:$P{{xxx}} 参数 vs JDBC 直连 | 低 |
|
||||
| `agent/jrxml_windower.py` | JRXML Band 级窗口化引擎:拆解/切分/重组/元素计数校验 | 中 |
|
||||
| `validation_service/main.py` | FastAPI 验证服务 | 低 |
|
||||
| `scripts/init_default_kb.py` | 多租户默认 KB 初始化(默认用户 + 预置 KB) | 低 |
|
||||
|
||||
## 关键约定
|
||||
|
||||
1. **LLM 调用接口**: 所有节点通过 `get_llm().invoke(prompt)` 同步调用,或用 `get_llm().stream(prompt)` 流式调用。三个后端(Anthropic/OpenAI/Ollama)通过 `_BaseLLM` 统一接口。
|
||||
|
||||
2. **流式生成**: generate/modify_jrxml/correct_jrxml 使用 `get_stream_writer()` 发射自定义事件,UI 通过 `stream_mode=["updates", "custom"]` 捕获逐字输出。
|
||||
|
||||
3. **JRXML 提取**: `_extract_jrxml()` 处理 LLM 响应 —— 去掉 markdown 代码块标记,提取 XML 内容。
|
||||
|
||||
4. **状态持久化**: 每个会话存为 `sessions/{session_id}.json`,LangGraph 节点间通过 AgentState dict 传递。
|
||||
|
||||
5. **Token 计数**: 使用 `tiktoken` (gpt-4o encoder) 估算,不管实际模型是什么。
|
||||
|
||||
6. **RAG 子模块**: `rag/` 是一个独立的 git submodule,其内部的生成产物 (`models/`, `embeddings/`, `chroma_db/`, `jrxml_source_chunks/`) 不在 git 中。
|
||||
|
||||
## Prompt 模板位置
|
||||
|
||||
所有 Prompt 在 `prompts/` 目录,`.md` 文件可直接编辑,无需重启应用:
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `prompts/intent_classify.md` | 8 分类意图识别 |
|
||||
| `prompts/initial_generation.md` | 首次生成 JRXML |
|
||||
| `prompts/modification.md` | 修改现有 JRXML |
|
||||
| `prompts/correction.md` | 自动修正错误 |
|
||||
| `prompts/explain_error.md` | 错误转人话 |
|
||||
| `prompts/compression.md` | 对话压缩摘要 |
|
||||
| `prompts/consult.md` | 咨询解答 |
|
||||
| `prompts/skeleton_generation.md` | 分层生成-骨架 |
|
||||
| `prompts/refine_layout.md` | 分层生成-精调 |
|
||||
| `prompts/field_mapping.md` | 分层生成-字段映射 |
|
||||
|
||||
## 新增功能 (v2)
|
||||
|
||||
### 流式输出 + 节点平铺
|
||||
- LLM 生成时逐字展示 XML(不再是空白等待)
|
||||
- 节点以"处理过程"折叠区展开,不相互覆盖
|
||||
- 完成后自动折叠,展示总结卡片
|
||||
|
||||
### 错误自增长知识库
|
||||
- `backend/error_kb.py` — ChromaDB 集合 `jrxml_error_cases`
|
||||
- 错误指纹去重(标准化 + MD5):相同结构错误不重复录入
|
||||
- 记录内容:错误信息 + 修正前后 JRXML + 修正 prompt + 工具链
|
||||
- `retrieve` 节点自动注入历史修正案例
|
||||
- 流程:correct_jrxml 保存 last_error_case → validate 通过时自动入库
|
||||
|
||||
### 文件上传(已迁移至 Vue 3 前端)
|
||||
- **对话区域上传**: `UnifiedInput.vue` 统一输入框支持文本 + 文件拖拽/粘贴/选择按钮,支持图片/PDF/DOCX/XLSX/文本/`.jrxml`
|
||||
- **文件预览芯片**: 上传后显示在对话区域,可逐文件移除(自动清理临时文件)
|
||||
- 侧边栏多文件上传(可逐文件移除,向后兼容保留)
|
||||
- 支持: PDF(pdfplumber+PIL) / DOCX(python-docx) / XLSX(openpyxl) / 图片(PIL+EasyOCR优先→PaddleOCR回退) / 纯文本
|
||||
- 上传文本自动注入下一条消息前缀
|
||||
- 根据 `can_use_vision()` 判断是否走原生多模态(当前 MiniMax 不支持)
|
||||
|
||||
### 对话区域文件粘贴/拖拽 (v3, 已迁移至 Vue 3)
|
||||
|
||||
原 Streamlit 方案(`st.html()` 注入 + `sessionStorage` 桥接)已废弃。当前由 `UnifiedInput.vue` 原生处理 paste/drop/dragover 事件,通过 `stores/chat.ts` 上传文件到 `/api/upload`,`_process_files()` 在 `api_server.py` 中统一处理。
|
||||
|
||||
### A4 模板识别
|
||||
- `backend/layout_analyzer.py` — 三种处理路径:
|
||||
- **完整 A4**: 比例匹配 + OCR 元素 → 全量布局描述
|
||||
- **行片段 + 有现有报表**: 行匹配到 JRXML section → 定位修改
|
||||
- **行片段 + 无现有报表**: 按 A4 模板生成完整报表
|
||||
- PaddleOCR(可选安装)提供精确元素位置/字号
|
||||
- 行分组:Y 轴容差自动聚类;行匹配:文本相似度搜索 JRXML band
|
||||
|
||||
### 会话历史下载
|
||||
- `AgentState.jrxml_versions` 追踪每次生成/修改的版本
|
||||
- 侧边栏"历史版本"折叠区,每版本独立下载按钮
|
||||
|
||||
### 预览修复
|
||||
- `route_after_save` 新增意图判断:预览/导出跳过验证直通 finalize
|
||||
|
||||
### 结构化日志系统
|
||||
- `backend/logger.py` — JSON 格式化 + trace_id + 国际时区
|
||||
- `_LLMLoggingWrapper` — 包装所有 LLM 后端,记录完整 prompt/response
|
||||
- `@log_node` / `@_log_route` — 装饰器自动记录节点和路由
|
||||
- 日志分离: `logs/app.log` (业务) + `logs/llm.log` (AI 调用)
|
||||
|
||||
## 新增功能 (v3/v4)
|
||||
|
||||
### OCR 单据字段精确提取 (v3)
|
||||
- `backend/ocr_extractor.py` — 4 策略优先级提取: exact_match → kv_pair → regex → table_match
|
||||
- PaddleOCR 首次识别后将原始结果(含所有文本元素 + bbox坐标)持久化
|
||||
- `_format_ocr_context()` — 将 OCR 结果(字段 + 原始元素坐标)格式化为 LLM prompt 注入
|
||||
- OCR 结果在 `modify_jrxml` 和 `generate` 节点中自动注入 prompt
|
||||
- `process_input` 节点在上传图片时自动触发 OCR 字段提取
|
||||
- 结果持久化到会话文件(`save_session_node` / `load_session_node`)
|
||||
|
||||
### 多模态聊天输入 + 多格式文件 (v4, 已迁移至 Vue 3)
|
||||
|
||||
原 Streamlit `st_multimodal_chatinput` 组件已废弃。当前由 `UnifiedInput.vue` 实现粘贴/拖拽/文件选择,`api_server.py:_process_files()` 统一处理上传文件(含 `.jrxml` 模板提取)。
|
||||
|
||||
新增文件格式支持: XLSX (openpyxl)、XLS (xlrd)、DOC (olefile)
|
||||
|
||||
### 批注检测 (v4)
|
||||
- `backend/annotation_detector.py` — 识别用户在手写单据上的圈选和箭头标记
|
||||
- **圆圈检测**: 红色通道增强 → HoughCircles → 圆形度验证
|
||||
- **箭头检测**: Canny边缘 → HoughLinesP → 线段方向聚类 → 端点边缘密度判定方向
|
||||
- **OCR 关联**: 批注与附近 OCR 文本元素关联(15% 图片尺寸内)
|
||||
- **LLM 注入**: `format_annotation_context()` 将批注结果格式化为中文提示
|
||||
- `process_input` 节点在 OCR 提取后自动运行批注检测
|
||||
- `annotation_result` 字段持久化到 AgentState + 会话文件
|
||||
|
||||
### OCR 上下文提示增强 (v3/v4)
|
||||
- `prompts/modification.md` — 新增 `{ocr_context}` 占位符
|
||||
- `modify_jrxml` 节点 — 将 OCR 上下文注入 modification prompt
|
||||
- OCR 上下文包含: 结构化字段、全部文本元素(含坐标)、批注检测结果
|
||||
|
||||
## 新增功能 (v5)
|
||||
|
||||
### 分层精确生成
|
||||
- 解决 A4 报表图片 OCR 元素过多(数百个)导致 LLM prompt 超长的问题
|
||||
- **3 阶段管线**(仅对 `initial_generation` + 有布局 schema 时触发):
|
||||
1. `generate_skeleton` — 压缩的布局 schema → 骨架 JRXML (`$F{field_N}` 占位)
|
||||
2. `refine_layout` — 采样坐标(表头+首行数据+末行)→ 像素级位置精调
|
||||
3. `map_fields` — OCR 字段名 → 替换占位符
|
||||
- `backend/layout_analyzer.py` — 新增 `extract_layout_schema()`: 列聚类 + 区域分类 + schema_text
|
||||
- `agent/graph.py` — 新增 `route_after_retrieve()`: 有 schema 走 3 阶段,无 schema 走原有 1-shot
|
||||
- `prompts/` — 新增 `skeleton_generation.md`, `refine_layout.md`, `field_mapping.md`
|
||||
- 文本请求和所有其他意图零行为变更
|
||||
|
||||
## 已知注意点
|
||||
|
||||
- **环境变量优先级**: `backend/llm.py` 使用 `load_dotenv(override=True)` 确保 `.env` 值**始终覆盖**系统环境变量。曾因系统级 `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` 覆盖 `.env` 中的 MiniMax URL,导致 401 认证失败。新增 LLM 相关环境变量时,必须在 `.env` 中显式设置 `ANTHROPIC_*` 变量(而非仅设 `OPENAI_*` fallback),否则残留的系统环境变量会污染请求目标。
|
||||
- **Anthropic SDK**: 使用原始 `anthropic` 包(非 `langchain-anthropic`),因为需要直连 MiniMax 兼容端点。API Key 优先读 `ANTHROPIC_API_KEY`,fallback `OPENAI_API_KEY`。Anthropic SDK 会自动将 key 放入 `x-api-key` header。
|
||||
- **MiniMax 模型名称**: `MiniMax-M2.7`(不是 `minimax-2.7`),大小写敏感。
|
||||
- **日志分析**: 通过 `trace_id` 字段可追踪一次请求的全链路。LLM 调用日志在 `logs/llm.log`,包含完整 prompt 和 response(各截断 10000 字符)。
|
||||
- **验证服务结构检查**: 字段引用一致性 (`$F{field}` vs `<field>` 声明)、SQL SELECT 存在性、pageWidth/pageHeight/name 属性。
|
||||
- **XSD 校验可选**: 需要 `validation_service/schemas/jasperreport_7_0_6.xsd` 存在。
|
||||
- **rag 子模块**: 内部有独立的管线脚本(`batch_chunker.py` → `embed_chunks.py` → `import_to_chroma.py`),通常不需要在主项目中运行。
|
||||
- **OCR 引擎**: 优先 PaddleOCR 2.9.x(精确识别,`pip install paddleocr`),回退 EasyOCR 1.7+。两者均未安装时仅返回图片元信息。PaddlePaddle 3.x 在 Windows 上有 ONEDNN bug,固定在 2.6.x。
|
||||
- **OCR 字段提取**: `process_input` 自动检测上传图片,调用 `OcrExtractor` 提取常见中文字段(发票代码/号码/金额/日期等),提取结果自动注入 LLM 上下文。
|
||||
- **会话持久化**: `session_id` 现已包含在 `save_session_node` 的持久化字段中,避免切换会话时因 `session_id` 丢失导致的无限 rerun bug。`create_session` 存盘前强制写入 `agent_state["session_id"] = sid`。`load_session_node` 不从磁盘覆盖 `session_id`。切换会话增加 `_last_switched_to` 哨兵防止重复触发。
|
||||
- **MAX_RETRY**: 默认 5 次。重试耗尽后 `pending_failure_context` 记录失败信息,下次用户输入时自动注入。
|
||||
- **验证最小内容检查**: 验证服务额外检查至少 1 个 `<band>` + 1 个 `<textField>` 或 `<staticText>`,拦截空壳 JRXML。
|
||||
- **XLSX 支持 (v3)**: 需要 `openpyxl>=3.1.0`(已加入 requirements.txt)。表格按工作表逐行读取,单元格用 `|` 分隔。
|
||||
- **粘贴/拖拽**: `UnifiedInput.vue` 原生处理 paste/drop 事件,单文件上限 20MB。文件通过 `/api/upload` 上传至 `uploads/` 目录。
|
||||
- **torchvision**: `transformers` 库的懒加载需要 `torchvision`,已作为依赖安装。
|
||||
- **opencv-python-headless**: 批注检测(圈选/箭头)依赖,通过 `pip install -r requirements.txt` 安装。
|
||||
- **前端文件输入**: `UnifiedInput.vue` 原生处理文本输入 + 文件拖拽/粘贴/选择,替代原 Streamlit `st-multimodal-chatinput` 组件。
|
||||
- **xlwt**: 仅在测试中使用(生成 .xls 测试文件)。
|
||||
- **分层精确生成**: 3 阶段管线仅在 `layout_schema.total_rows > 0` 时触发。文本请求和 `modify_report` 等意图不受影响,走原有 `generate` 节点。中间阶段(骨架/精调)跳过验证,只有最终 mapped 结果进入 `validate`。
|
||||
|
||||
## 新增功能 (v6)
|
||||
|
||||
### 5-Issue Fix — 图片解析 Bug + 前端功能补全
|
||||
|
||||
**Fix 1 — 图片后缀 dot 缺失**: `file_parser.py` 后缀规范化(`"jpg"` → `".jpg"`),`api_server.py` 使用 `Path.suffix` 替代 `rsplit`。所有图片上传之前均因后缀不匹配回退到文本解析器,OCR/布局分析从未实际触发。
|
||||
|
||||
**Fix 2 — Vue 前端功能补全**:
|
||||
- `ProcessSection.vue` 替代 `StreamingMessage.vue` + `NodeProgress.vue`,使用 `<details>`/`<summary>` 原生可折叠区域
|
||||
- `Sidebar.vue` 新增历史版本下载列表(`jrxml_versions` 索引下载)
|
||||
- `UnifiedInput.vue` 已集成文件拖拽/粘贴/芯片/移除(v5 已完成)
|
||||
|
||||
**Fix 3 — OCR 两层日志**: `agent/nodes.py` 新增 `_log_ocr_layers()` — `[内容层]` OCR 文本+字段提取,`[位置层]` 布局 schema 列×行+区域分类,`[合并]` 管线选择(3阶段 vs 单阶段)
|
||||
|
||||
**Fix 4 — 全过程流式输出+自动折叠**:
|
||||
- `api_server.py` `node_start` 事件携带 `step_index`
|
||||
- `chat.ts` 新增 `ProcessSection[]` 模型:per-section stream routing、完成自动折叠、运行中自动展开
|
||||
- `ProcessSection.vue` 渲染步骤编号/标签/耗时/内容(XML 代码高亮)
|
||||
|
||||
**Fix 5 — 消息耗时显示**: `api_server.py` `agent_complete` 事件新增 `total_duration_ms`,`SummaryCard.vue` 显示总耗时,`chat.ts` 暴露 `lastDurationMs` + `formatDuration()`
|
||||
|
||||
|
||||
## 已安装的 Claude Code 插件/Skills
|
||||
|
||||
| 插件 | 来源 | 关键 Skill |
|
||||
|------|------|-----------|
|
||||
| `superpowers` | `obra/superpowers-marketplace` | `tdd-workflow`(红-绿-重构)、`verification-loop`(修复验证)、`systematic-debugging`(根因分析) |
|
||||
| `example-skills` | `anthropics/skills` | `webapp-testing`(Playwright E2E 浏览器自动化)、`skill-creator` |
|
||||
|
||||
**测试工作流**:需求澄清 → TDD 红-绿-重构 → `webapp-testing` 浏览器验证 → `verification-loop` 确认 → 提交。
|
||||
|
||||
**E2E 测试前置条件**:Chrome 已安装 (`C:\Program Files\Google\Chrome\Application\chrome.exe`),Playwright MCP Bridge 扩展需手动安装。
|
||||
|
||||
## 更新 (v7 — 2026-05-22)
|
||||
|
||||
### 会话持久化 & 多轮对话记忆修复
|
||||
|
||||
**原子写入** (`backend/session.py`): `save_session` 改用 tempfile + os.replace 原子写入,防止进程崩溃时 JSON 截断导致会话损坏。
|
||||
|
||||
**graph.stream 状态修复** (`api_server.py`): LangGraph 的 `graph.stream()`
|
||||
只产出事件,不修改传入的 `agent_state`。`_run_graph_sync` 改为手动收集每个节点的
|
||||
返回 dict 并 `agent_state.update()`,确保 done 事件到达时 agent_state 已是完整状态。
|
||||
此修复解决了第二次请求时 `current_jrxml` 为空、导致多轮对话"失忆"的问题。
|
||||
|
||||
**save_session 调用时机**: 从 `stream_and_save` 末尾移至 `_sse_generator` 中 done 分支
|
||||
(yield `agent_complete` 之前),消除前端 `refreshFromApi()` 的竞态。
|
||||
|
||||
### OCR 管线打通
|
||||
|
||||
**uploaded_file_path 传递** (`api_server.py`): `_process_files` 返回的 `uploaded_paths`
|
||||
注入 `agent_state["uploaded_file_path"]`,使 `process_input` 节点的 `OcrExtractor` 字段
|
||||
精确提取和 `annotation_detector` 批注检测得以触发。此前 `uploaded_file_path` 始终为空,
|
||||
第二层 OCR 从未执行。
|
||||
|
||||
### 前端体验改进
|
||||
|
||||
**下载区常驻** (`Sidebar.vue`): 下载区域始终可见,无文件时显示灰色"暂无下载文件",
|
||||
生成完成后自动出现下载链接。
|
||||
|
||||
**侧边栏自动刷新** (`stores/session.ts`, `App.vue`): 新增 `refreshFromApi()` 方法,
|
||||
`agent_complete` 后自动从 API 重新加载会话状态,下载按钮无需手动刷新即可出现。
|
||||
|
||||
**节点进度完整展示** (`api_server.py`): 移除 `node_complete` 事件的 SKIP_NODES 过滤,
|
||||
所有节点(包括加载会话等内部节点)的 start/complete 事件均正常发送,前端可看到
|
||||
完整流转(running → done)。
|
||||
|
||||
### modification_request 宽松化
|
||||
|
||||
原有 `status == "pass"` 条件去除:只要 `current_jrxml` 存在即设置
|
||||
`user_modification_request`,确保修改意图的请求能携带完整上下文。
|
||||
|
||||
## 更新 (v8 — 2026-05-22)
|
||||
|
||||
### Prompt 花括号转义修复
|
||||
|
||||
**问题**: `skeleton_generation.md` 中 `$F{field_1}` 是给 LLM 看的占位字段名指令,
|
||||
但 Python `.format()` 把 `{field_1}` 当作格式化占位符,因缺少对应 kwarg 抛出 `KeyError: 'field_1'`。
|
||||
所有图片上传触发的 `generate_skeleton` 节点均因此崩溃。
|
||||
|
||||
**修复**: 3 个 prompt 文件中 6 处 `{field_N}` / `{...}` 转义为 `{{field_N}}` / `{{...}}`:
|
||||
- `prompts/skeleton_generation.md` — `$F{field_1}` → `$F{{field_1}}`
|
||||
- `prompts/field_mapping.md` — 4 处
|
||||
- `prompts/refine_layout.md` — 1 处
|
||||
|
||||
Python 将 `{{` 输出为字面量 `{`,LLM 看到的内容不变。
|
||||
|
||||
## 更新 (v9 — 2026-05-22)
|
||||
|
||||
### 测试基础设施全面补齐
|
||||
|
||||
**单元测试** (76 测试):
|
||||
- `tests/test_session.py` — 27 测试:会话 CRUD、原子写入、唯一 ID、损坏 JSON 跳过
|
||||
- `tests/test_error_kb.py` — 24 测试:指纹去重、关键词提取(中/英/JRXML)、ErrorKB CRUD、搜索、统计
|
||||
- `tests/test_agent.py` — 5 个软断言强化为严格断言(`status`/`current_jrxml` 存在性检查)
|
||||
- 已有测试:`test_ocr_extraction.py`(49)、`test_layered_generation.py`(19)、`test_validation.py`(6)、`test_file_parser_formats.py`(4)、`test_annotation_detector.py`(7)、`test_e2e_ocr.py`(3)
|
||||
|
||||
**集成测试** (25 测试, `tests/test_api_integration.py`):
|
||||
- FastAPI TestClient 全覆盖:健康检查、配置、会话 CRUD、文件上传、下载、Chat SSE、安全边界(路径穿越/非法 JSON/大 payload)
|
||||
- Mock LangGraph graph 避免真实 LLM 调用
|
||||
|
||||
**E2E 测试** (8 测试, `frontend/tests/e2e/main-flows.spec.ts`):
|
||||
- Playwright 浏览器自动化:页面加载、侧边栏、会话管理、聊天流程、输入 UX
|
||||
- 全量 API Mock(`page.route`)无需后端运行
|
||||
- 配置: `frontend/playwright.config.ts`, `npm run test:e2e`
|
||||
|
||||
**运行测试**:
|
||||
```bash
|
||||
# 全部单元+集成测试
|
||||
cd D:\Idea Project\jaspersoft && python -m pytest tests/ -v
|
||||
|
||||
# 仅 E2E(需要前端 dev server)
|
||||
cd frontend && npx playwright test
|
||||
```
|
||||
|
||||
### Bug 修复: create_session 参数缺失
|
||||
|
||||
`backend/session.py` — `create_session()` 新增可选参数 `session_id: Optional[str] = None`。
|
||||
`api_server.py:507` 调用 `create_session(session_id=session_id)` 时之前会抛出 `TypeError`。
|
||||
|
||||
## 更新 (v10 — 2026-05-23)
|
||||
|
||||
### 5-Fix — 生成可靠性全面加固
|
||||
|
||||
**问题诊断**: 上传车辆历史卡片图片后,`map_fields` 节点 LLM 返回 0 字符,导致 ~11,500 字符的骨架 JRXML 被空字符串覆盖,修正循环无法恢复,最终输出 934 字符的占位桩(与原始图片内容完全不符)。
|
||||
|
||||
**Fix 1 — 空响应保护**: 所有 5 个生成节点(`generate_skeleton`, `refine_layout`, `map_fields`, `modify_jrxml`, `correct_jrxml`)增加空响应守卫。LLM 返回空字符串时拒绝更新 `current_jrxml`,保留前一有效版本。
|
||||
|
||||
**Fix 2 — max_tokens 扩容**: `backend/llm.py` — `max_tokens` 从 4096 → 8192。MiniMax-M2.7 支持最大 131K 输出 token,8192 在生成复杂 JRXML(通常 5000-15000 字符)时提供充裕空间。
|
||||
|
||||
**Fix 3 — 快照回退**: 5 个生成节点在 LLM 输出 JRXML 短于 200 字符时,回退到生成前的 `prev_jrxml` 版本,防止 LLM 输出无意义短文本污染状态。
|
||||
|
||||
**Fix 4 — 修正循环注入 OCR 上下文**: `correct_jrxml` 节点将 OCR 提取结果(`ocr_context`)和布局 schema(`layout_schema_text`)注入修正 prompt。此前修正节点"盲修"——只看到 JRXML 和编译错误,不理解原始单据的字段结构和布局意图。
|
||||
|
||||
**Fix 5 — 滚动续写机制**: 当 LLM 输出因 `max_tokens` 限制被截断(JRXML 不以 `</jasperReport>` 结尾),自动发送续写请求(附最后 800 字符锚点),最多 3 轮(1 正常 + 2 续写)。
|
||||
|
||||
- `backend/llm.py` — `MiniMaxLLM.stream()` 捕获 `stop_reason`,`_LLMLoggingWrapper` 在 `max_tokens` 截断时记录 WARNING
|
||||
- `agent/nodes.py` — 新增 `_generate_with_continuation()` 辅助函数,5 个生成节点全部重构使用
|
||||
- `_extract_jrxml()` — 正则表达式支持命名空间前缀 JRXML(`<\w+:jasperReport`)
|
||||
- 内容去重:续写文本直接拼接,依赖 `_extract_jrxml` 提取完整 XML
|
||||
|
||||
**MAX_RETRY 调整**: 默认值从 3 → 5(环境变量 `MAX_RETRY`),配合续写机制确保复杂报表有充分修正机会。
|
||||
|
||||
**JRXML 提取命名空间兼容**: `_extract_jrxml()` 和 `_generate_with_continuation()` 的完整性检查统一支持 `</ns0:jasperReport>` 等命名空间前缀闭合标签。
|
||||
|
||||
## 更新 (v11 — 2026-05-23)
|
||||
|
||||
### Java 渲染管线 + 像素级对比
|
||||
|
||||
**目标**: 将 JRXML 渲染为 PNG 图片,与用户上传的原始图片进行 SSIM(结构相似性)像素级对比。
|
||||
|
||||
**Java 依赖** (`lib/java/`):
|
||||
| JAR | 用途 |
|
||||
|-----|------|
|
||||
| `jasperreports-6.21.0.jar` (5.8MB) | 核心库,**必须用 6.x**(7.x 仅支持 Jackson XML 格式) |
|
||||
| `commons-digester-2.1.jar` | XML 解析(6.x 使用 Digester 2.x) |
|
||||
| `commons-logging-1.3.5.jar`, `commons-collections4-4.5.0.jar`, `commons-beanutils-1.10.1.jar`, `commons-lang3-3.17.0.jar` | 基础依赖 |
|
||||
| `itext-2.1.7.jar` | PDF 生成 |
|
||||
| `jfreechart-1.5.5.jar` | 图表 |
|
||||
| `ecj-3.38.0.jar` | Eclipse JDT 编译器(报表表达式编译) |
|
||||
|
||||
**Java 工具** (`lib/java/`):
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `JrxmlRenderer.java` | JRXML → PNG 渲染器 |
|
||||
| `JrxmlDebug.java` | 诊断:SAX/JRXmlLoader/compile 三层测试 |
|
||||
| `JrxmlGen.java` | 参考:程序化构建 JasperDesign → 序列化为 XML |
|
||||
|
||||
**Python 渲染封装** (`agent/nodes.py`):
|
||||
- `_render_jrxml_to_png(jrxml, output_path, scale)` — 调用 Java `JrxmlRenderer`
|
||||
- `_compute_pixel_similarity(rendered_png, reference_image)` — OpenCV + scikit-image SSIM 对比
|
||||
|
||||
**像素对比流程**: validate 节点 XSD 通过 → 有 `uploaded_file_path` → Java 渲染 → SSIM 对比 → SSIM < 0.4 且 diff > 60% → 标记 fail → 注入 correct_jrxml 修正上下文
|
||||
|
||||
**手动渲染**: `java -cp ".;jasperreports-6.21.0.jar;..." JrxmlRenderer input.jrxml output.png 2.0`
|
||||
|
||||
### 内容保真度 + 修正去重 (v10 补充)
|
||||
|
||||
- `_check_ocr_fidelity(jrxml, state)` — OCR 字段名/元素数/列数三重检查
|
||||
- `correct_jrxml` 去重检测:输入输出相同 → `retry_count += 2`
|
||||
- `prompts/correction.md` — 一次只修复第1个错误 + 输出不可与输入相同 + 命名空间严格指定
|
||||
- `prompts/skeleton_generation.md`, `prompts/modification.md` — 明确命名空间约束
|
||||
|
||||
### consult_answer 前端显示修复
|
||||
|
||||
- `api_server.py` — `agent_complete` SSE 事件新增 `consult_answer` 字段
|
||||
|
||||
## 更新 (v12 — 2026-05-23)
|
||||
|
||||
### 多租户知识库系统
|
||||
|
||||
**核心架构**:用户自行维护多套知识库,每套 KB 拥有独立的文件存储、JSON 元数据和 ChromaDB 向量集合。会话可绑定不同 KB,LLM 基于 KB 中的字段定义和 JRXML 模板生成报表。
|
||||
|
||||
**存储架构**:
|
||||
```
|
||||
kb_data/
|
||||
├── users.json # 用户注册表
|
||||
└── {user_id}/
|
||||
├── profile.json
|
||||
└── {kb_id}/
|
||||
├── meta.json # KB 元数据 + 字段定义 + 模板索引
|
||||
├── raw/ # 原始上传文件
|
||||
├── chunks.json # RAG chunks(含 JRXML 模板完整文本)
|
||||
└── chroma/ # KB 专属 ChromaDB
|
||||
```
|
||||
|
||||
**新增后端模块**:
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `backend/kb_manager.py` | 用户+KB CRUD:create_user/list_users/create_kb/list_kbs/get_kb/delete_kb/update_kb_meta/get_kb_raw_dir/get_kb_chroma_path。原子 JSON 写入(tempfile + os.replace) |
|
||||
| `backend/kb_parser.py` | KB 解析管道:`parse_jrxml_fields()` XML 提取参数/字段/查询 → `process_file_for_kb()` 处理多种格式(jrxml/zip/tar/pdf/docx/xlsx/md) → `chunk_file_results()` 切割 → `build_kb_from_files()` 全管线(parse→chunk→embed→update meta) |
|
||||
| `backend/kb_searcher.py` | `KBChromaSearcher` 类:per-KB ChromaDB 懒连接。`search()` 语义搜索、`search_templates()` 仅搜索 JRXML 模板 chunk、`add_chunks()` 向量写入。全局 searcher 缓存 `_searchers: dict` |
|
||||
| `backend/field_matcher.py` | OCR↔KB 字段匹配:1) Embedding 粗筛(余弦相似度 top-3)2) LLM 精确确认。返回 `{"工单号": "billNo", ...}` 映射 |
|
||||
| `agent/datasource.py` | 数据源模式:`resolve_datasource_mode()` 检测用户意图 → `"parameter"`(默认 $P{xxx})或 `"jdbc"`(SQL 直连)。未配置 DB 时生成反问消息 |
|
||||
| `scripts/init_default_kb.py` | 默认 KB 初始化:创建默认用户 → 解析 `rag/jrxml_source/` 下的 17 个 JRXML + 16 个 MD → chunk + embed → ChromaDB |
|
||||
|
||||
**新增 API 端点**(api_server.py):
|
||||
|
||||
```
|
||||
POST /api/users # 创建用户
|
||||
GET /api/users # 用户列表
|
||||
GET /api/users/{user_id} # 用户详情
|
||||
DELETE /api/users/{user_id} # 删除用户
|
||||
GET /api/users/{user_id}/kbs # KB 列表
|
||||
POST /api/users/{user_id}/kbs # 创建 KB
|
||||
GET /api/kbs/{kb_id} # KB 详情
|
||||
DELETE /api/kbs/{kb_id} # 删除 KB
|
||||
POST /api/kbs/{kb_id}/upload # 上传文件到 KB
|
||||
POST /api/kbs/{kb_id}/build # 构建 KB(chunk→embed)
|
||||
GET /api/kbs/{kb_id}/status # KB 状态
|
||||
GET /api/kbs/{kb_id}/fields # KB 字段+模板列表
|
||||
GET /api/kbs/{kb_id}/search?q=&type= # KB 语义搜索
|
||||
PUT /api/sessions/{session_id}/kb # 绑定会话-KB
|
||||
GET /api/sessions/{session_id}/kb # 获取会话绑定的 KB
|
||||
```
|
||||
|
||||
**三条模板获取路径**:
|
||||
|
||||
1. **管理页预上传**:用户上传文件到 KB → 解析管道 → chunks + ChromaDB → 对话选择 KB → retrieve 节点从 KB 检索
|
||||
2. **对话框即时上传**:用户拖入 `.jrxml` → `_parse_jrxml_file()` → 注入 `agent_state["uploaded_template_jrxml"]` → 生成节点直接使用该模板
|
||||
3. **口头引用模板**:用户说"根据标准结算单模板" → `_detect_template_intent()` 正则匹配 → `retrieve()` 在 KB 中搜索模板 → 注入 `kb_template_jrxml`
|
||||
|
||||
**模板上下文注入**:所有 6 个生成节点(generate/generate_skeleton/refine_layout/map_fields/modify_jrxml/correct_jrxml)通过 `_build_template_context(state)` 获取模板上下文,优先级:聊天上传 > KB 检索 > KB 字段定义。6 个 prompt 模板全部新增 `{template_context}` 占位符。
|
||||
|
||||
**前端新增**:
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `stores/kb.ts` | Pinia store:用户列表、KB 列表、当前选择、字段/模板缓存、CRUD 操作、会话绑定 |
|
||||
| `components/KbSelector.vue` | 对话顶部 KB 下拉选择器 + 管理按钮 |
|
||||
| `components/KbManager.vue` | 模态面板:创建 KB、上传文件(支持 .jrxml/.md/.xlsx/.docx/.pdf/.csv/.zip 等)、构建、删除 |
|
||||
|
||||
**API Server 增强**:
|
||||
- `_process_files()` 检测 `.jrxml` 文件 → 提取参数/字段/查询/页面尺寸 → 注入 `uploaded_template_jrxml` + `uploaded_template_params`
|
||||
- `agent/state.py` 新增 10 个字段:`kb_id`, `kb_fields`, `kb_field_mapping`, `uploaded_template_jrxml`, `uploaded_template_params`, `kb_template_jrxml`, `kb_template_name`, `datasource_mode`, `db_config`
|
||||
|
||||
**字段匹配管线**(`_match_ocr_to_kb` → 尚未集成到节点):OCR 提取中文字段名 → `match_ocr_to_kb()` 两阶段匹配 → 结果为 `{"工单号": "billNo"}` → `format_field_mapping_context()` 注入 prompt → LLM 使用 `$P{billNo}` 而非 `$P{工单号}`
|
||||
|
||||
## 更新 (v13 — 2026-05-24)
|
||||
|
||||
### 3 阶段管道内容丢失修复 — Band 级窗口化 + 程序化字段映射
|
||||
|
||||
**问题**:`generate_skeleton` 生成 34k 字符骨架 JRXML → `refine_layout` 将完整 34k 发给 LLM → LLM 重新生成简化版(~3k 字符,丢失 91.5%)。`map_fields` 同样存在字段映射时内容丢失问题。
|
||||
|
||||
**根因**:LLM 看到完整 JRXML 时倾向于"重新生成"而非"在原基础上修改坐标/字段名"。提示词调控无法可靠解决。
|
||||
|
||||
**修复方案**(按用户要求 — 程序化节点控制,不靠 LLM 提示词):
|
||||
|
||||
#### `refine_layout`:Band 级窗口化 LLM 精调
|
||||
|
||||
新增 `agent/jrxml_windower.py` — JRXML 拆解/切分/重组引擎:
|
||||
|
||||
| 函数 | 用途 |
|
||||
|------|------|
|
||||
| `decompose_jrxml()` | ET 安全解析 → 分离 header(field 声明/queryString 等,不发给 LLM)+ 所有 band |
|
||||
| `split_band_into_windows()` | 超过 4000 字符的 band 在元素闭合标签处切分为多个窗口 |
|
||||
| `reassemble_band_windows()` | 合并同一 band 的多个窗口结果 |
|
||||
| `reassemble_jrxml()` | header + 所有修改后 band + footer → 完整 JRXML |
|
||||
| `count_elements()` | 正则计数 textField/staticText/field(兼容命名空间前缀) |
|
||||
| `validate_element_count()` | 校验元素数变化,>10% 回退到前一版本 |
|
||||
|
||||
**LLM 每次只看到 ~2-4k 字符片段**,无法"重写整个报表"。header 部分完全不发给 LLM,原样保留。
|
||||
|
||||
#### `map_fields`:完全程序化替换(零 LLM 调用)
|
||||
|
||||
`_programmatic_map_fields()` — 纯正则替换 `$F{field_N}` → OCR 真实字段名,100% 确定性。
|
||||
|
||||
`_sanitize_field_name()` — 非 ASCII 字符(中文/日文)转义为 `_uXXXX_` Unicode 码点格式,确保 JRXML 合法。
|
||||
|
||||
#### 新增测试
|
||||
|
||||
| 文件 | 用例数 | 覆盖 |
|
||||
|------|--------|------|
|
||||
| `tests/test_jrxml_windower.py` | 28 | 拆解/往返重组/窗口切分/元素计数/命名空间/多 section 多 band |
|
||||
| `tests/test_programmatic_map_fields.py` | 20 | 字段声明替换/引用替换/中文转义/坐标保留/部分映射/空字段跳过 |
|
||||
|
||||
完整测试套件(385 项)无回归。
|
||||
|
||||
## 更新 (v14 — 2026-05-24)
|
||||
|
||||
### max_tokens per-node + 修正循环死锁修复
|
||||
|
||||
**问题 A — max_tokens 自限**: `backend/llm.py` 硬编码 `max_tokens=8192`。MiniMax M2.7 的 reasoning token 吃光 8192 输出预算后骨架生成为空(0 个可见字符)。其他节点(correct_jrxml/modify_jrxml)输入 68K+ 字符时输出也被截断。
|
||||
|
||||
**问题 B — ns:field 命名空间前缀正则失配**: `_programmatic_map_fields()` 正则 `<field\b` 匹配不到 `<ns0:field name="field_1">`,导致字段声明保持占位符但引用被替换为 OCR 字段名,校验报"used in expressions but not declared"。
|
||||
|
||||
**问题 C — 验证服务 502 修正死循环**: 验证服务(port 8001)未启动时,`validate_jrxml()` 返回 502。错误消息被当作 JRXML 校验错误送入 `explain_error → correct_jrxml`,LLM 尝试"修复"网络错误产出 HTML/markdown 等垃圾,循环 5 轮直到 retry_count 耗尽。
|
||||
|
||||
**问题 D — correct_jrxml 从未写回 current_jrxml**: 修正后的 JRXML 只写入 `conversation_history`,从不更新 `state["current_jrxml"]`,导致每轮 validate 看到同一份原始 JRXML,修正完全无效。这是 5 轮 jrxml_length 始终 4441 不变的根本原因。
|
||||
|
||||
**修复方案**:
|
||||
|
||||
#### 1. per-node max_tokens(`backend/llm.py` + `agent/nodes.py`)
|
||||
- `get_llm(caller, max_tokens=None)` — 新增可选 `max_tokens` 参数,透传到 `_build_raw_llm`
|
||||
- `MiniMaxLLM.__init__()` — 存储 `self._max_tokens`
|
||||
- `LLM_MAX_TOKENS` 环境变量覆盖默认 8192
|
||||
- 5 个生成节点 max_tokens 提升到 32768:`generate`, `generate_skeleton`, `refine_layout`, `modify_jrxml`, `correct_jrxml`
|
||||
- `generate_skeleton` 空响应自动重试(max_tokens=65536)
|
||||
|
||||
#### 2. ns:field 正则修复(`agent/nodes.py:548`)
|
||||
- `<field\b` → `<[\w:]*field\b` 兼容 `<ns0:field>`, `<field>` 等所有命名空间前缀
|
||||
|
||||
#### 3. 验证服务不可用防护
|
||||
- `backend/validation.py` — 区分 ConnectError/HTTPStatusError(5xx):返回 `service_unavailable: True`
|
||||
- `agent/nodes.py:validate` — 透传 `state["service_unavailable"]`
|
||||
- `agent/graph.py:route_after_validate` — `service_unavailable` 时直接 `finalize`,不进入修正循环
|
||||
|
||||
#### 4. correct_jrxml 输出合法性守卫
|
||||
- 新增 JRXML 有效性检查:输出不含 `<jasperReport` 且不含 `<?xml` 时,回退到前一版本
|
||||
- **Bug 修复**: `state["current_jrxml"] = jrxml` 写回修正结果
|
||||
|
||||
#### 5. 连续输出提取增强
|
||||
- `_strip_continuation_wrapper()` — 剥离续写响应中 LLM 重新添加的 markdown 代码块和自然语言前缀
|
||||
- `_extract_jrxml()` — 逐一检查多个 markdown 代码块,跳过非 JRXML 片段
|
||||
- `_generate_with_continuation()` — 续写轮次自动应用 `_strip_continuation_wrapper`
|
||||
|
||||
#### 新增环境变量
|
||||
|
||||
| 变量 | 描述 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `LLM_MAX_TOKENS` | 默认 max_tokens(各节点可覆盖) | 8192 |
|
||||
@@ -0,0 +1,114 @@
|
||||
# jaspersoft-fix 评测报告
|
||||
|
||||
**项目路径**: `D:\Idea Project\jaspersoft-fix`
|
||||
**评测时间**: 2026-05-25
|
||||
**评测维度**: 代码质量 · 安全与稳定性 · 工程实践 · 产品设计
|
||||
|
||||
---
|
||||
|
||||
## 综合评分
|
||||
|
||||
| 维度 | 评分 | 主要问题 |
|
||||
|------|------|----------|
|
||||
| 代码质量与架构 | 7.3/10 | nodes.py 1709行 God Module、无文件锁并发风险 |
|
||||
| 安全与稳定性 | P0×1 + P1×2 + P2×4 | llm.log 写全量 prompt、session 并发覆盖、无 magic bytes 校验 |
|
||||
| 工程实践 | 3.5/5 | 原子写入优秀、trace_id 传播良好、无 E2E 测试 |
|
||||
| 产品设计 | 4.2/5 | natural_explanation 透明、非 fix 报告误报进度不透明 |
|
||||
|
||||
---
|
||||
|
||||
## 一、代码质量与架构(7.3/10)
|
||||
|
||||
亮点:**原子写入**(tempfile+fsync+replace)设计优秀、v5 Band 级分层精确生成架构、前端 Vue3+Pinia 结构清晰。
|
||||
|
||||
主要问题:
|
||||
|
||||
| 问题 | 严重度 | 说明 |
|
||||
|------|--------|------|
|
||||
| nodes.py 过胖 | P1 | 1709行,14个工作流节点,应拆分到 `agent/utils.py` |
|
||||
| session.py 无文件锁 | P0 | 多用户并发写同一 session 会互相覆盖(无 flock/fcntl) |
|
||||
| 废弃 Vue 组件 | P1 | `StreamingMessage.vue`/`NodeProgress.vue` 仍在 frontend/components |
|
||||
|
||||
---
|
||||
|
||||
## 二、安全与稳定性
|
||||
|
||||
| 等级 | 数量 | 问题 |
|
||||
|------|------|------|
|
||||
| **P0** | 1 | `llm.log` 写全量 prompt(`prompt[:10000]`),API Key 可能泄露 |
|
||||
| **P1** | 2 | session 并发无锁(见 P0);文件上传无 magic bytes 校验 |
|
||||
| **P2** | 4 | LLM prompt 注入风险;ChromaDB 无认证;CORS 宽松;无 API 认证 |
|
||||
|
||||
已做好:`.env` 隔离、`sessions/` gitignore、SQL 注入防护(参数化查询)、hex session_id 校验防路径穿越。
|
||||
|
||||
**⚠️ llm.log 泄露风险**:
|
||||
`backend/llm.py` 第 47-49 行写 `prompt[:10000]` 到日志,第 66-67 行写 `response[:10000]`。prompt 中若含用户上传的文档内容(包含敏感字段名)或 API 调用上下文,可能被记录。需要脱敏。
|
||||
|
||||
---
|
||||
|
||||
## 三、工程实践(3.5/5)
|
||||
|
||||
亮点:原子写入(tempfile+fsync+replace)优秀、日志 trace_id 传播(contextvars)、JSONFormatter 结构化日志、`nodes.py` 的 namespace 检查修复(五轮修正失败根因)。
|
||||
|
||||
主要问题:
|
||||
|
||||
| 问题 | 严重度 |
|
||||
|------|--------|
|
||||
| 会话并发无文件锁 | P0 — 多用户并发写同一 session 会互相覆盖 |
|
||||
| 无 E2E 测试 | P1 — 无 Playwright 测试 |
|
||||
| 废弃 Vue 组件未删除 | P1 — `StreamingMessage.vue`/`NodeProgress.vue` |
|
||||
| 冷启动慢(llm.py 初始化) | P2 |
|
||||
|
||||
---
|
||||
|
||||
## 四、产品设计(4.2/5)
|
||||
|
||||
亮点:错误修正循环设计优秀、五轮自动修正+失败上下文注入、`SummaryCard.vue` 正确展示 `natural_explanation`(非 fix 报告误报"进度不透明"是错的)。
|
||||
|
||||
主要问题:
|
||||
|
||||
| 优先级 | 问题 |
|
||||
|--------|------|
|
||||
| P0 | 会话并发无文件锁(影响稳定性) |
|
||||
| P1 | `export_pdf` 未实现(需标记"敬请期待") |
|
||||
| P1 | 意图分类无用户确认机制 |
|
||||
| P2 | 流式输出无 XML 语法高亮 |
|
||||
| P2 | 空白状态无引导示例 |
|
||||
|
||||
---
|
||||
|
||||
## 优先修复路线图
|
||||
|
||||
### P0(立即修复)
|
||||
|
||||
1. **会话并发文件锁**:在 `save_session()` 加 `fcntl.flock()` 保护先读后写
|
||||
2. **LLM 日志脱敏**:prompt/response 中截断或替换 API Key 为 `[REDACTED]`
|
||||
|
||||
### P1(近期处理)
|
||||
|
||||
3. 删除废弃 Vue 组件(`StreamingMessage.vue`/`NodeProgress.vue`)
|
||||
4. 实现 `export_pdf` 或标记"敬请期待"
|
||||
5. 意图分类结果标签化供用户确认
|
||||
6. 添加 Playwright E2E 测试
|
||||
|
||||
### P2(有空再搞)
|
||||
|
||||
7. 流式输出 XML 语法高亮
|
||||
8. 空白状态引导示例
|
||||
|
||||
---
|
||||
|
||||
## 与 jaspersoft(非 fix)的关键差异
|
||||
|
||||
| 项目 | jaspersoft(非 fix) | jaspersoft-fix |
|
||||
|------|---------------------|----------------|
|
||||
| commit | `2d5183d` OCR fidelity reform | `0839ba9` WIP(rag + test image) |
|
||||
| namespace 前缀 | 未处理 | 已修复 `_extract_jrxml()` |
|
||||
| 五轮修正失败根因 | 旧评分公式 | 已修复(去掉 field_coverage 权重) |
|
||||
| OCR 自动发现文档类型 | 需手动 | 已实现 |
|
||||
| 进度透明度 | 非 fix 报告误报"不透明" | 实际展示 natural_explanation ✅ |
|
||||
|
||||
---
|
||||
|
||||
*评测时间: 2026-05-25 (Asia/Hong_Kong)*
|
||||
*评测工具: Mavis AI Agent*
|
||||
@@ -1,37 +1,60 @@
|
||||
# JRXML 生成代理
|
||||
|
||||
一个本地桌面应用程序,帮助非技术用户通过多轮自然语言对话创建 JasperReports 模板(JRXML)。
|
||||
一个本地桌面应用程序,通过多轮自然语言对话帮助非技术用户创建 JasperReports 模板(JRXML)。
|
||||
|
||||
## 功能
|
||||
|
||||
- **多轮聊天**:通过对话优化报表 -- 添加列、更改标题、添加汇总
|
||||
- **自动验证**:每次生成或修改后都会验证 JRXML
|
||||
- **自动修正**:如果验证失败,代理会分析错误并自动修正(最多 3 次)
|
||||
- **模板检索**:使用 Chroma 向量数据库检索相关的 JRXML 示例以获得更好的生成效果
|
||||
- **下载**:导出已验证的、可供 JasperReports 使用的 JRXML 文件
|
||||
- **多轮对话**:通过对话优化报表 — 添加列、更改标题、添加汇总
|
||||
- **自动验证**:每次生成或修改后验证 JRXML(结构检查 + XSD 校验 + 像素级对比)
|
||||
- **自动修正**:验证失败时分析错误并自动修正(最多 5 次)
|
||||
- **错误自增长知识库**:修正案例指纹去重入库,避免重复犯错
|
||||
- **模板检索**:ChromaDB 语义搜索 JRXML 示例和中文文档
|
||||
- **文件上传**:对话框拖拽/粘贴/选择,支持图片、PDF、Word、Excel、文本
|
||||
- **单据 OCR 识别**:上传报表图片后自动提取字段(4 策略优先级 + 置信度)
|
||||
- **批注检测**:识别手写单据上的圈选和箭头标记
|
||||
- **分层精确生成**:3 阶段管线(骨架→精调→字段映射),Band 级窗口化防止内容丢失
|
||||
- **多租户知识库**:独立 KB 管理,含字段定义 + JRXML 模板 + ChromaDB 向量检索
|
||||
- **Java 渲染管线**:JRXML → PNG 渲染 + SSIM 像素级对比
|
||||
- **下载**:导出经过验证的 JRXML 文件,含历史版本追溯
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
Streamlit 界面 (app.py)
|
||||
|
|
||||
LangGraph 代理 (agent/)
|
||||
|-- retrieve (Chroma/embeddings)
|
||||
|-- generate (LLM)
|
||||
|-- validate (FastAPI service)
|
||||
|-- explain + correct (auto-fix loop)
|
||||
|-- modify (multi-turn edits)
|
||||
|
|
||||
前端 (Vue 3 + Vite, 端口 5173)
|
||||
│ Pinia stores (chat / session / kb)
|
||||
│ 9 components (Sidebar, ChatMessages, ProcessSection, UnifiedInput,
|
||||
│ SummaryCard, KbSelector, KbManager, StreamingMessage, NodeProgress)
|
||||
▼ HTTP + SSE (Server-Sent Events)
|
||||
后端 API (FastAPI, 端口 8000)
|
||||
│ REST + SSE 流式推送
|
||||
│ 包装 LangGraph Agent ──► agent/ (18 节点状态机)
|
||||
│ ├─ process_input (文件解析 + OCR + 批注检测)
|
||||
│ ├─ manage_context (token 计数 + 压缩)
|
||||
│ ├─ classify_intent (8 类意图识别)
|
||||
│ ├─ retrieve (RAG + 错误 KB + KB 模板搜索)
|
||||
│ ├─ generate / generate_skeleton → refine_layout → map_fields
|
||||
│ ├─ validate (XSD + 结构 + 像素对比)
|
||||
│ ├─ explain_error + correct_jrxml (自动修正循环, 最多 5 次)
|
||||
│ └─ modify_jrxml / consult / undo / reset / preview / finalize
|
||||
├── backend/ 服务层
|
||||
│ ├─ llm (Anthropic SDK / OpenAI / Ollama)
|
||||
│ ├─ session (原子 JSON 持久化)
|
||||
│ ├─ validation (验证服务客户端)
|
||||
│ ├─ kb_manager / kb_parser / kb_searcher (多租户知识库)
|
||||
│ ├─ field_matcher (OCR↔KB 字段匹配)
|
||||
│ ├─ ocr_extractor / layout_analyzer / annotation_detector (OCR 管线)
|
||||
│ ├─ rag_adapter / error_kb / embeddings (向量检索)
|
||||
│ └─ file_parser / logger / jrxml_reorder
|
||||
▼
|
||||
FastAPI 验证服务 (:8001)
|
||||
|-- Structural checks (field references, SQL, page dimensions)
|
||||
|-- XSD schema validation (if jasperreport.xsd available)
|
||||
└─ 结构检查 + XSD Schema + 最小内容校验
|
||||
```
|
||||
|
||||
## 前置要求
|
||||
|
||||
- Python 3.11+
|
||||
- 完整的编译验证需要:JDK 21 + JasperReports 7.0.6
|
||||
- OpenAI 兼容的 API 密钥(或本地 Ollama)
|
||||
- JDK 21+(Java JRXML→PNG 渲染管线使用)
|
||||
- Anthropic 兼容 API 密钥(MiniMax M2.7 等)
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -47,29 +70,32 @@ pip install -r requirements.txt
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
编辑 `.env` 配置您的 API 密钥和偏好设置。
|
||||
编辑 `.env` 配置 API 密钥和偏好设置。
|
||||
|
||||
### 3. 初始化知识库
|
||||
|
||||
```bash
|
||||
python scripts/init_kb.py
|
||||
python scripts/init_default_kb.py
|
||||
```
|
||||
|
||||
### 4. 启动验证服务
|
||||
### 4. 启动服务
|
||||
|
||||
**一键启动(推荐)**:双击 `start.bat`,自动启动验证服务、后端 API、前端开发服务器。停止用 `stop.bat`。
|
||||
|
||||
**手动启动**(需要三个终端):
|
||||
|
||||
在一个终端中运行:
|
||||
```bash
|
||||
python -m uvicorn validation_service.main:app --port 8001
|
||||
# 终端 1 — 验证服务(必须先启动)
|
||||
python -m uvicorn validation_service.main:app --port 8001 --host 0.0.0.0
|
||||
|
||||
# 终端 2 — 后端 API(SSE + REST)
|
||||
python -m uvicorn api_server:app --port 8000 --host 0.0.0.0
|
||||
|
||||
# 终端 3 — 前端开发服务器
|
||||
cd frontend && npm install && npm run dev
|
||||
```
|
||||
|
||||
### 5. 启动 Streamlit 界面
|
||||
|
||||
在另一个终端中运行:
|
||||
```bash
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
在浏览器中打开 http://localhost:8501。
|
||||
浏览器打开 `http://localhost:5173`。
|
||||
|
||||
## 使用示例
|
||||
|
||||
@@ -82,66 +108,132 @@ streamlit run app.py
|
||||
第三轮 - 修改:
|
||||
> "将标题改为 '2024 员工目录' 并加粗"
|
||||
|
||||
每一轮都会自动验证和修正 JRXML。
|
||||
|
||||
## 验证服务(当前限制)
|
||||
|
||||
由于完整的 JasperReports 7.0.6 编译需要 JDK 21,当前的验证执行以下检查:
|
||||
|
||||
1. 结构检查:字段声明一致性、SQL 查询存在性、页面尺寸、报表名称
|
||||
2. XSD schema 验证:如果 `validation_service/schemas/jasperreport_7_0_6.xsd` 可用
|
||||
|
||||
要进行完整的编译验证,请将 `jasper-validator.jar` 放在 `validation_service/` 目录并更新 `main.py`。
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
pytest tests/test_validation.py -v
|
||||
pytest tests/test_agent.py -v
|
||||
pytest tests/ -v
|
||||
```
|
||||
每一轮自动验证和修正 JRXML。上传报表图片后自动触发 OCR 识别 + 分层精确生成。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
jrxml-agent/
|
||||
app.py Streamlit 聊天界面
|
||||
agent/
|
||||
state.py AgentState 定义
|
||||
nodes.py 图节点(generate, validate, modify 等)
|
||||
graph.py LangGraph 状态机
|
||||
backend/
|
||||
llm.py LLM 工厂(OpenAI / Ollama)
|
||||
embeddings.py 嵌入模型工厂
|
||||
validation.py 验证服务客户端
|
||||
validation_service/
|
||||
main.py FastAPI 验证服务器
|
||||
validate.bat Windows 启动器
|
||||
jaspersoft/
|
||||
api_server.py FastAPI SSE 后端(REST + 流式推送)
|
||||
start.bat / stop.bat 一键启停脚本
|
||||
start.py Python 启动器
|
||||
|
||||
agent/ LangGraph 工作流引擎
|
||||
state.py AgentState 类型定义(~40 字段)
|
||||
nodes.py 18 个工作流节点实现
|
||||
graph.py 状态图编译 + 9 个路由函数
|
||||
datasource.py 数据源模式解析(参数 vs JDBC)
|
||||
jrxml_windower.py Band 级拆解/切分/重组引擎
|
||||
|
||||
backend/ 后端服务层
|
||||
llm.py LLM 工厂(Anthropic SDK / OpenAI / Ollama)
|
||||
logger.py 结构化 JSON 日志(trace_id + UTC+8)
|
||||
validation.py 验证服务 HTTP 客户端
|
||||
session.py 会话 JSON 原子持久化 CRUD
|
||||
embeddings.py 嵌入模型工厂(HuggingFace / OpenAI)
|
||||
rag_adapter.py RAG 语义搜索适配器
|
||||
error_kb.py 错误自增长知识库(指纹去重 + ChromaDB)
|
||||
file_parser.py 多格式文件解析(PDF/DOCX/XLSX/XLS/DOC/图片/文本)
|
||||
layout_analyzer.py A4 模板布局分析(布局 schema 提取 + 列聚类)
|
||||
ocr_extractor.py OCR 字段精确提取(4 策略 + 置信度)
|
||||
annotation_detector.py 批注检测(圈选 + 箭头 + OCR 关联)
|
||||
kb_manager.py 多租户知识库 CRUD(用户 + KB)
|
||||
kb_parser.py KB 解析管道(解析→chunk→embed)
|
||||
kb_searcher.py Per-KB ChromaDB 搜索适配器
|
||||
field_matcher.py OCR↔KB 字段匹配(Embedding + LLM 两阶段)
|
||||
jrxml_reorder.py JRXML 元素重排序(XSD sequence 合规)
|
||||
|
||||
prompts/ LLM Prompt 模板(热重载)
|
||||
loader.py Prompt 加载器
|
||||
*.md 10 个 Prompt 模板文件
|
||||
|
||||
frontend/ Vue 3 + Vite 前端
|
||||
src/
|
||||
api/client.ts SSE 客户端 + fetch 封装
|
||||
stores/
|
||||
chat.ts Pinia: 消息/流式/节点进度
|
||||
session.ts Pinia: 会话管理
|
||||
kb.ts Pinia: 知识库状态
|
||||
components/
|
||||
Sidebar.vue 会话列表 + 下载 + 历史版本
|
||||
ChatMessages.vue 消息列表渲染
|
||||
ProcessSection.vue 过程折叠区(<details>/<summary>)
|
||||
StreamingMessage.vue 流式消息显示
|
||||
NodeProgress.vue 节点进度指示器
|
||||
UnifiedInput.vue 统一输入框(文本 + 文件拖拽/粘贴/芯片)
|
||||
SummaryCard.vue 结果摘要卡片(含耗时)
|
||||
KbSelector.vue KB 下拉选择器
|
||||
KbManager.vue KB 管理面板(上传/构建/删除)
|
||||
|
||||
validation_service/ 独立验证服务(端口 8001)
|
||||
main.py FastAPI 验证端点
|
||||
schemas/
|
||||
jasperreport_7_0_6.xsd JasperReports XSD Schema
|
||||
|
||||
lib/java/ Java JRXML 渲染管线
|
||||
JrxmlRenderer.java JRXML → PNG 渲染器
|
||||
JrxmlDebug.java 诊断工具
|
||||
JrxmlGen.java 参考:程序化 JasperDesign
|
||||
jasperreports-6.21.0.jar 核心 JasperReports 库
|
||||
|
||||
tests/ Python 测试(19 文件, ~385 测试)
|
||||
test_session.py 会话 CRUD(27)
|
||||
test_ocr_extraction.py OCR 字段提取(49)
|
||||
test_jrxml_windower.py Band 窗口化(28)
|
||||
test_api_integration.py API 集成(25)
|
||||
test_error_kb.py 错误 KB(24)
|
||||
test_programmatic_map_fields.py 字段映射(20)
|
||||
test_layered_generation.py 分层生成(19)
|
||||
test_annotation_detector.py 批注检测(7)
|
||||
test_validation.py 验证服务(6)
|
||||
test_file_parser_formats.py 文件解析(4)
|
||||
test_e2e_ocr.py OCR E2E(3)
|
||||
test_agent.py / test_kb_*.py /
|
||||
|
||||
data/
|
||||
sample_templates/ 知识库的 JRXML 模板
|
||||
corrections/ 错误修正案例
|
||||
sample_templates/ JRXML 样本模板
|
||||
corrections/ 错误修正案例
|
||||
|
||||
scripts/
|
||||
init_kb.py Chroma 知识库初始化脚本
|
||||
tests/
|
||||
test_validation.py 验证服务测试
|
||||
test_agent.py 代理集成测试
|
||||
db/chroma/ Chroma 持久化目录
|
||||
requirements.txt
|
||||
.env.example
|
||||
README.md
|
||||
init_default_kb.py 多租户默认知识库初始化
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 描述 | 默认值 |
|
||||
|----------|-------------|---------|
|
||||
| LLM_BACKEND | cloud 或 local | cloud |
|
||||
| OPENAI_API_KEY | OpenAI API 密钥 | - |
|
||||
| OPENAI_BASE_URL | API 基础 URL | https://api.openai.com/v1 |
|
||||
| LLM_MODEL | 模型名称 | gpt-4o |
|
||||
| LOCAL_LLM_MODEL | Ollama 模型 | qwen2.5-coder:7b |
|
||||
| EMBED_BACKEND | local 或 cloud | local |
|
||||
| LOCAL_EMBED_MODEL | 嵌入模型 | Qwen/Qwen3-Embedding-0.6B |
|
||||
| VALIDATION_SERVICE_URL | 验证端点 | http://localhost:8001/validate |
|
||||
| CHROMA_PERSIST_DIR | Chroma 存储位置 | ./db/chroma |
|
||||
| MAX_RETRY | 自动修正尝试次数 | 3 |
|
||||
|------|------|--------|
|
||||
| `LLM_BACKEND` | LLM 后端: cloud / local | cloud |
|
||||
| `LLM_PROVIDER` | 云端提供商: anthropic / openai | anthropic |
|
||||
| `ANTHROPIC_API_KEY` | Anthropic 兼容 API 密钥(优先) | - |
|
||||
| `ANTHROPIC_BASE_URL` | Anthropic 兼容 Base URL | https://api.minimaxi.com/anthropic |
|
||||
| `OPENAI_API_KEY` | OpenAI 兼容 API 密钥(fallback) | - |
|
||||
| `OPENAI_BASE_URL` | OpenAI 兼容 Base URL | https://api.openai.com/v1 |
|
||||
| `LLM_MODEL` | 模型名称 | MiniMax-M2.7 |
|
||||
| `LLM_MAX_TOKENS` | 默认 max_tokens(各节点可覆盖) | 8192 |
|
||||
| `LOCAL_LLM_MODEL` | Ollama 模型 | qwen2.5-coder:7b |
|
||||
| `EMBED_BACKEND` | 嵌入模型后端: local / cloud | local |
|
||||
| `LOCAL_EMBED_MODEL` | 本地嵌入模型 | Qwen/Qwen3-Embedding-0.6B |
|
||||
| `VALIDATION_SERVICE_URL` | 验证服务端点 | http://localhost:8001/validate |
|
||||
| `CHROMA_PERSIST_DIR` | ChromaDB 持久化目录 | ./db/chroma |
|
||||
| `MAX_RETRY` | 自动修正最大尝试次数 | 5 |
|
||||
| `CONTEXT_MAX_TOKENS` | 上下文压缩阈值 | 6000 |
|
||||
| `CONTEXT_KEEP_RECENT` | 保留最近 N 轮对话 | 4 |
|
||||
| `SESSIONS_DIR` | 会话持久化目录 | ./sessions |
|
||||
| `LOG_DIR` | 日志目录 | ./logs |
|
||||
| `LOG_LEVEL` | 日志级别 | DEBUG |
|
||||
| `HISTORY_MAX_SNAPSHOTS` | 状态快照保留数 | 10 |
|
||||
| `OCR_USE_GPU` | OCR GPU 加速 | false |
|
||||
| `OCR_CONFIDENCE_THRESHOLD` | OCR 置信度最低阈值 | 0.5 |
|
||||
| `RAG_EMBED_MODEL` | RAG 嵌入模型 | paraphrase-multilingual-MiniLM-L12-v2 |
|
||||
| `RAG_JRXML_SOURCE` | JRXML 模板源目录 | ./rag/jrxml_source |
|
||||
| `RAG_COLLECTION_NAME` | ChromaDB 集合名 | jrxml_chunks |
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
# 全部单元 + 集成测试
|
||||
cd D:\Idea Project\jaspersoft && python -m pytest tests/ -v
|
||||
|
||||
# 仅 E2E 测试(需要前端 dev server 运行)
|
||||
cd frontend && npx playwright test
|
||||
```
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""数据源模式解析模块。
|
||||
|
||||
默认使用 $P{xxx} 参数模式;用户可选择 JDBC 直连模式。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from agent.state import AgentState
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def resolve_datasource_mode(state: AgentState) -> str:
|
||||
"""返回数据源模式: "parameter" 或 "jdbc"。
|
||||
|
||||
优先读取 state 中已设定的模式,否则根据用户输入检测。
|
||||
"""
|
||||
existing = state.get("datasource_mode", "")
|
||||
if existing in ("parameter", "jdbc"):
|
||||
return existing
|
||||
|
||||
user_input = state.get("user_input", "")
|
||||
if _detect_jdbc_intent(user_input):
|
||||
return "jdbc"
|
||||
return "parameter"
|
||||
|
||||
|
||||
def _detect_jdbc_intent(user_input: str) -> bool:
|
||||
"""检测用户是否想要 JDBC 直连数据库模式。"""
|
||||
patterns = [
|
||||
r"(直连|直连数据库|数据库直连)",
|
||||
r"(从|在)(数据库|DB|MySQL|PostgreSQL|Oracle|SQL Server)\w*",
|
||||
r"(jdbc|JDBC)",
|
||||
r"(连接|连)(数据库|DB)",
|
||||
r"(查询|select|SELECT)\s",
|
||||
]
|
||||
for pat in patterns:
|
||||
if re.search(pat, user_input):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _sanitize_url(url: str) -> str:
|
||||
"""剥离 JDBC URL 中的 user:password@ 片段,防止泄露到 LLM prompt。"""
|
||||
return re.sub(r"://[^@]*@", "://***:***@", url)
|
||||
|
||||
|
||||
def build_datasource_context(mode: str, kb_fields: list, db_config: Optional[dict] = None) -> str:
|
||||
"""构建数据源上下文字符串,注入生成 prompt。"""
|
||||
if mode == "jdbc":
|
||||
if not db_config or not db_config.get("url"):
|
||||
return (
|
||||
"[数据源模式: JDBC]\n"
|
||||
"⚠ 用户想要 JDBC 直连模式,但尚未配置数据库连接信息。\n"
|
||||
"请先生成带 $P{xxx} 参数占位符的 JRXML,并提醒用户配置 JDBC 连接。"
|
||||
)
|
||||
safe_url = _sanitize_url(db_config.get("url", ""))
|
||||
return (
|
||||
"[数据源模式: JDBC]\n"
|
||||
f"连接URL: {safe_url}\n"
|
||||
f"驱动: {db_config.get('driver', '')}\n"
|
||||
"请使用 <queryString><![CDATA[...]]></queryString> 中的 SQL 查询。"
|
||||
)
|
||||
|
||||
# parameter mode
|
||||
if kb_fields:
|
||||
field_list = "\n".join(
|
||||
f"| {f['name']} | {f.get('description', '')} | {f.get('type', 'java.lang.String')} |"
|
||||
for f in kb_fields
|
||||
)
|
||||
return (
|
||||
"[数据源模式: 参数]\n"
|
||||
"使用 $P{xxx} 参数模式,以下为可用参数:\n"
|
||||
f"| 参数名 | 含义 | 类型 |\n|---|---|---|\n{field_list}"
|
||||
)
|
||||
return "[数据源模式: 参数]\n使用 $P{xxx} 参数模式生成 JRXML。"
|
||||
|
||||
|
||||
def configure_jdbc(state: AgentState, url: str = "", driver: str = "",
|
||||
username: str = "", password: str = "") -> dict:
|
||||
"""配置 JDBC 连接并返回更新字段。
|
||||
|
||||
注意:db_config 会被存入 AgentState 并持久化到会话文件。
|
||||
生产环境应使用外部密钥管理服务,避免明文存储密码。
|
||||
"""
|
||||
return {
|
||||
"datasource_mode": "jdbc",
|
||||
"db_config": {
|
||||
"url": url,
|
||||
"driver": driver or "com.mysql.cj.jdbc.Driver",
|
||||
"username": username,
|
||||
"password": password,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def ask_db_config(state: AgentState) -> Optional[str]:
|
||||
"""如果用户选了 JDBC 模式但未配置 DB 连接,返回反问消息。"""
|
||||
mode = resolve_datasource_mode(state)
|
||||
if mode == "jdbc":
|
||||
db_config = state.get("db_config", {})
|
||||
if not db_config or not db_config.get("url"):
|
||||
return (
|
||||
"您选择了数据库直连模式,请提供以下信息:\n"
|
||||
"1. JDBC URL(如 jdbc:mysql://localhost:3306/dbname)\n"
|
||||
"2. 数据库用户名\n"
|
||||
"3. 数据库密码\n"
|
||||
"4. 驱动类名(可选,默认 com.mysql.cj.jdbc.Driver)"
|
||||
)
|
||||
return None
|
||||
+126
-22
@@ -1,5 +1,6 @@
|
||||
"""LangGraph JRXML 生成代理的状态图定义。"""
|
||||
|
||||
import functools
|
||||
import os
|
||||
from typing import Literal
|
||||
|
||||
@@ -15,6 +16,9 @@ from agent.nodes import (
|
||||
classify_intent,
|
||||
retrieve,
|
||||
generate,
|
||||
generate_skeleton,
|
||||
refine_layout,
|
||||
map_fields,
|
||||
modify_jrxml,
|
||||
handle_consult,
|
||||
handle_undo,
|
||||
@@ -25,14 +29,41 @@ from agent.nodes import (
|
||||
correct_jrxml,
|
||||
finalize,
|
||||
)
|
||||
from backend.logger import get_logger
|
||||
|
||||
load_dotenv()
|
||||
MAX_RETRY = int(os.getenv("MAX_RETRY", "3"))
|
||||
load_dotenv(override=True)
|
||||
MAX_RETRY = int(os.getenv("MAX_RETRY", "5"))
|
||||
|
||||
_graph_log = get_logger("agent")
|
||||
|
||||
|
||||
def _log_route(route_name: str):
|
||||
"""装饰器:自动记录路由决策。"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(state: AgentState, *args, **kwargs):
|
||||
target = func(state, *args, **kwargs)
|
||||
_graph_log.info(
|
||||
f"[路由] {route_name} → {target}",
|
||||
extra={
|
||||
"route": route_name,
|
||||
"target": target,
|
||||
"session_id": state.get("session_id", ""),
|
||||
"intent": state.get("intent", ""),
|
||||
"status": state.get("status", ""),
|
||||
"has_jrxml": bool(state.get("current_jrxml", "").strip()),
|
||||
"retry_count": state.get("retry_count", 0),
|
||||
},
|
||||
)
|
||||
return target
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
# ============================================================
|
||||
# 路由函数
|
||||
# ============================================================
|
||||
|
||||
@_log_route("route_by_intent")
|
||||
def route_by_intent(state: AgentState) -> Literal[
|
||||
"retrieve", "modify_jrxml", "save_session",
|
||||
"handle_consult", "handle_undo", "handle_reset"
|
||||
@@ -59,32 +90,61 @@ def route_by_intent(state: AgentState) -> Literal[
|
||||
return "retrieve"
|
||||
|
||||
|
||||
@_log_route("route_after_retrieve")
|
||||
def route_after_retrieve(state: AgentState) -> Literal["generate", "generate_skeleton"]:
|
||||
"""当 layout_schema 存在时走三层精确生成,否则走原有 1-shot。"""
|
||||
layout_schema = state.get("layout_schema")
|
||||
if layout_schema and isinstance(layout_schema, dict) and layout_schema.get("total_rows", 0) > 0:
|
||||
return "generate_skeleton"
|
||||
return "generate"
|
||||
|
||||
|
||||
@_log_route("route_after_generate")
|
||||
def route_after_generate(state: AgentState) -> Literal["save_session"]:
|
||||
return "save_session"
|
||||
|
||||
|
||||
@_log_route("route_after_modify")
|
||||
def route_after_modify(state: AgentState) -> Literal["save_session"]:
|
||||
return "save_session"
|
||||
|
||||
|
||||
@_log_route("route_after_undo")
|
||||
def route_after_undo(state: AgentState) -> Literal["save_session"]:
|
||||
return "save_session"
|
||||
|
||||
|
||||
def route_after_save(state: AgentState) -> Literal["validate"]:
|
||||
@_log_route("route_after_save")
|
||||
def route_after_save(state: AgentState) -> Literal["validate", "finalize"]:
|
||||
# 预览/导出意图跳过验证,直接完成
|
||||
intent = state.get("intent", "")
|
||||
if intent in ("preview_report", "export_pdf", "export_jrxml"):
|
||||
return "finalize"
|
||||
# JRXML 为空时跳过验证/修正循环(生成失败等场景)
|
||||
if not state.get("current_jrxml", "").strip():
|
||||
return "finalize"
|
||||
return "validate"
|
||||
|
||||
|
||||
@_log_route("route_after_validate")
|
||||
def route_after_validate(state: AgentState) -> Literal["finalize", "explain_error"]:
|
||||
if state.get("status") == "pass":
|
||||
return "finalize"
|
||||
# JRXML 为空时跳过 explain→correct 修正循环
|
||||
if not state.get("current_jrxml", "").strip():
|
||||
return "finalize"
|
||||
# 验证服务不可用时跳过修正循环,避免对网络错误进行无效修正
|
||||
if state.get("service_unavailable"):
|
||||
return "finalize"
|
||||
return "explain_error"
|
||||
|
||||
|
||||
@_log_route("route_after_explain")
|
||||
def route_after_explain(state: AgentState) -> Literal["correct_jrxml"]:
|
||||
return "correct_jrxml"
|
||||
|
||||
|
||||
@_log_route("route_after_correct")
|
||||
def route_after_correct(state: AgentState) -> Literal["validate", "finalize"]:
|
||||
retry = state.get("retry_count", 0)
|
||||
if retry >= MAX_RETRY:
|
||||
@@ -96,28 +156,51 @@ def route_after_correct(state: AgentState) -> Literal["validate", "finalize"]:
|
||||
# 图构建
|
||||
# ============================================================
|
||||
|
||||
def build_graph() -> StateGraph:
|
||||
def build_graph(on_node_start=None) -> StateGraph:
|
||||
"""构建 LangGraph 状态图。
|
||||
|
||||
Args:
|
||||
on_node_start: 可选回调,在每个节点开始执行时调用。
|
||||
签名: on_node_start(node_name: str) -> None
|
||||
用于 SSE 流式推送 node_start 事件。
|
||||
"""
|
||||
workflow = StateGraph(AgentState)
|
||||
|
||||
def _wrap(name, fn):
|
||||
"""包装节点函数,在开始执行时触发 on_node_start 回调。"""
|
||||
if on_node_start is None:
|
||||
return fn
|
||||
|
||||
@functools.wraps(fn)
|
||||
def wrapped(state, *args, **kwargs):
|
||||
on_node_start(name)
|
||||
return fn(state, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
# 现有节点
|
||||
workflow.add_node("load_session", load_session_node)
|
||||
workflow.add_node("process_input", process_input)
|
||||
workflow.add_node("manage_context", manage_context)
|
||||
workflow.add_node("save_session", save_session_node)
|
||||
workflow.add_node("retrieve", retrieve)
|
||||
workflow.add_node("generate", generate)
|
||||
workflow.add_node("modify_jrxml", modify_jrxml)
|
||||
workflow.add_node("validate", validate)
|
||||
workflow.add_node("explain_error", explain_error)
|
||||
workflow.add_node("correct_jrxml", correct_jrxml)
|
||||
workflow.add_node("finalize", finalize)
|
||||
workflow.add_node("load_session", _wrap("load_session", load_session_node))
|
||||
workflow.add_node("process_input", _wrap("process_input", process_input))
|
||||
workflow.add_node("manage_context", _wrap("manage_context", manage_context))
|
||||
workflow.add_node("save_session", _wrap("save_session", save_session_node))
|
||||
workflow.add_node("retrieve", _wrap("retrieve", retrieve))
|
||||
workflow.add_node("generate", _wrap("generate", generate))
|
||||
workflow.add_node("modify_jrxml", _wrap("modify_jrxml", modify_jrxml))
|
||||
workflow.add_node("validate", _wrap("validate", validate))
|
||||
workflow.add_node("explain_error", _wrap("explain_error", explain_error))
|
||||
workflow.add_node("correct_jrxml", _wrap("correct_jrxml", correct_jrxml))
|
||||
workflow.add_node("finalize", _wrap("finalize", finalize))
|
||||
|
||||
# 新增节点:意图识别
|
||||
workflow.add_node("save_state_snapshot", save_state_snapshot)
|
||||
workflow.add_node("classify_intent", classify_intent)
|
||||
workflow.add_node("handle_consult", handle_consult)
|
||||
workflow.add_node("handle_undo", handle_undo)
|
||||
workflow.add_node("handle_reset", handle_reset)
|
||||
workflow.add_node("save_state_snapshot", _wrap("save_state_snapshot", save_state_snapshot))
|
||||
workflow.add_node("classify_intent", _wrap("classify_intent", classify_intent))
|
||||
workflow.add_node("handle_consult", _wrap("handle_consult", handle_consult))
|
||||
workflow.add_node("handle_undo", _wrap("handle_undo", handle_undo))
|
||||
workflow.add_node("handle_reset", _wrap("handle_reset", handle_reset))
|
||||
|
||||
# 新增节点:分层精确生成(阶段一~三)
|
||||
workflow.add_node("generate_skeleton", _wrap("generate_skeleton", generate_skeleton))
|
||||
workflow.add_node("refine_layout", _wrap("refine_layout", refine_layout))
|
||||
workflow.add_node("map_fields", _wrap("map_fields", map_fields))
|
||||
|
||||
# ---- 入口和前置流程 ----
|
||||
workflow.set_entry_point("load_session")
|
||||
@@ -141,12 +224,28 @@ def build_graph() -> StateGraph:
|
||||
)
|
||||
|
||||
# ---- 初始生成分支 ----
|
||||
workflow.add_edge("retrieve", "generate")
|
||||
workflow.add_conditional_edges(
|
||||
"retrieve",
|
||||
route_after_retrieve,
|
||||
{
|
||||
"generate": "generate",
|
||||
"generate_skeleton": "generate_skeleton",
|
||||
},
|
||||
)
|
||||
# 原有 1-shot 路径
|
||||
workflow.add_conditional_edges(
|
||||
"generate",
|
||||
route_after_generate,
|
||||
{"save_session": "save_session"},
|
||||
)
|
||||
# 分层精确生成 3 阶段路径
|
||||
workflow.add_edge("generate_skeleton", "refine_layout")
|
||||
workflow.add_edge("refine_layout", "map_fields")
|
||||
workflow.add_conditional_edges(
|
||||
"map_fields",
|
||||
route_after_generate,
|
||||
{"save_session": "save_session"},
|
||||
)
|
||||
|
||||
# ---- 修改分支 ----
|
||||
workflow.add_conditional_edges(
|
||||
@@ -166,7 +265,7 @@ def build_graph() -> StateGraph:
|
||||
workflow.add_conditional_edges(
|
||||
"save_session",
|
||||
route_after_save,
|
||||
{"validate": "validate"},
|
||||
{"validate": "validate", "finalize": "finalize"},
|
||||
)
|
||||
|
||||
# ---- 验证 → 修正循环 ----
|
||||
@@ -222,4 +321,9 @@ def create_initial_state() -> AgentState:
|
||||
updated_at="",
|
||||
intent="",
|
||||
history_states=[],
|
||||
jrxml_versions=[],
|
||||
last_error_case={},
|
||||
pending_failure_context={},
|
||||
layout_schema={},
|
||||
ocr_elements=[],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
"""JRXML 窗口化拆解与重组工具。
|
||||
|
||||
用于 3 阶段生成管道的 refine_layout 和 map_fields 节点:
|
||||
- 将大段 JRXML 按 band 拆解为独立窗口
|
||||
- 每个窗口独立发送给 LLM 进行坐标精调
|
||||
- 重组所有窗口 + 校验元素完整性
|
||||
|
||||
调用者: agent/nodes.py (refine_layout, map_fields)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
from backend.logger import get_logger
|
||||
|
||||
_windower_log = get_logger("jrxml.windower")
|
||||
|
||||
# 需要按 section 拆解的 band 容器标签
|
||||
_SECTION_TAGS = {
|
||||
"title", "pageHeader", "columnHeader", "detail", "columnFooter",
|
||||
"pageFooter", "lastPageFooter", "summary", "noData", "background",
|
||||
}
|
||||
|
||||
# 不发给 LLM 的 header 元素(原样保留)
|
||||
_HEADER_TAGS = {
|
||||
"property", "propertyExpression", "import", "template", "reportFont",
|
||||
"style", "subDataset", "scriptlet", "parameter", "queryString",
|
||||
"field", "sortField", "variable", "filterExpression", "group",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class BandInfo:
|
||||
"""单个 band 的拆解信息。"""
|
||||
section_name: str # 所属 section 名,如 "title", "detail"
|
||||
band_index: int # 在该 section 中的序号(0-based)
|
||||
band_xml: str # 完整 <band ...>...</band> 原始 XML
|
||||
element_count: int # textField + staticText 数量
|
||||
char_length: int # 字符数
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
"""用于日志和 prompt 的标识。"""
|
||||
if self.band_index > 0:
|
||||
return f"{self.section_name}_band{self.band_index}"
|
||||
return self.section_name
|
||||
|
||||
|
||||
@dataclass
|
||||
class JRXMLParts:
|
||||
"""JRXML 拆解结果。"""
|
||||
declaration: str # <?xml version="1.0"?>(如有)
|
||||
root_open: str # <jasperReport ...>
|
||||
header_xml: str # fields/params/queryString 等(不发给 LLM)
|
||||
bands: list[BandInfo] # 按出现顺序
|
||||
footer: str # </jasperReport>
|
||||
|
||||
@property
|
||||
def band_count(self) -> int:
|
||||
return len(self.bands)
|
||||
|
||||
@property
|
||||
def total_elements(self) -> int:
|
||||
return sum(b.element_count for b in self.bands)
|
||||
|
||||
|
||||
# ── 拆解 ──────────────────────────────────────────────────────────
|
||||
|
||||
def decompose_jrxml(jrxml: str) -> Optional[JRXMLParts]:
|
||||
"""将 JRXML 字符串拆解为 header + bands + footer 三部分。
|
||||
|
||||
使用 defusedxml.ElementTree 进行安全解析。
|
||||
返回 None 表示解析失败。
|
||||
"""
|
||||
try:
|
||||
root = ET.fromstring(jrxml)
|
||||
except ET.ParseError as e:
|
||||
_windower_log.error("JRXML 解析失败: %s", e)
|
||||
return None
|
||||
|
||||
tag = _local_tag(root.tag)
|
||||
if tag != "jasperReport":
|
||||
_windower_log.error("根元素不是 jasperReport: %s", tag)
|
||||
return None
|
||||
|
||||
# 提取 XML 声明
|
||||
declaration = ""
|
||||
if jrxml.strip().startswith("<?xml"):
|
||||
decl_end = jrxml.find("?>")
|
||||
if decl_end != -1:
|
||||
declaration = jrxml[:decl_end + 2]
|
||||
|
||||
# 提取根元素属性来重建 root_open
|
||||
root_open = _build_root_open(jrxml, root)
|
||||
|
||||
# 分离 header 子元素和 section 子元素
|
||||
header_children = []
|
||||
section_children = [] # (section_tag, child_elem)
|
||||
|
||||
for child in root:
|
||||
child_tag = _local_tag(child.tag)
|
||||
if child_tag in _HEADER_TAGS:
|
||||
header_children.append(child)
|
||||
elif child_tag in _SECTION_TAGS:
|
||||
section_children.append((child_tag, child))
|
||||
|
||||
# 构建 header_xml:序列化所有 header 子元素
|
||||
header_parts = []
|
||||
for child in header_children:
|
||||
header_parts.append(_elem_to_string(child))
|
||||
header_xml = "\n".join(header_parts)
|
||||
|
||||
# 提取 bands:每个 section 内可能有多个 <band>
|
||||
bands = []
|
||||
for sec_tag, sec_elem in section_children:
|
||||
for bi, band_elem in enumerate(sec_elem):
|
||||
band_local = _local_tag(band_elem.tag)
|
||||
if band_local != "band":
|
||||
continue
|
||||
band_xml = _elem_to_string(band_elem)
|
||||
ec = _count_elements_in_text(band_xml)
|
||||
bands.append(BandInfo(
|
||||
section_name=sec_tag,
|
||||
band_index=bi,
|
||||
band_xml=band_xml,
|
||||
element_count=ec,
|
||||
char_length=len(band_xml),
|
||||
))
|
||||
|
||||
# 提取 footer:</jasperReport> 闭合标签
|
||||
footer = _extract_footer(jrxml)
|
||||
|
||||
parts = JRXMLParts(
|
||||
declaration=declaration,
|
||||
root_open=root_open,
|
||||
header_xml=header_xml,
|
||||
bands=bands,
|
||||
footer=footer,
|
||||
)
|
||||
_windower_log.info(
|
||||
"JRXML 拆解完成: %d bands, %d 个元素, header %d 字符",
|
||||
len(bands), parts.total_elements, len(header_xml),
|
||||
)
|
||||
return parts
|
||||
|
||||
|
||||
# ── 窗口切分 ──────────────────────────────────────────────────────
|
||||
|
||||
# 安全的元素边界:在这些闭合标签后切分
|
||||
_SAFE_SPLIT_CLOSING = re.compile(
|
||||
r"</(?:[\w:]+:)?(?:textField|staticText|line|rectangle|ellipse|image|"
|
||||
r"frame|subreport|elementGroup|break|componentElement)>\s*"
|
||||
)
|
||||
|
||||
|
||||
def split_band_into_windows(band: BandInfo, max_chars: int = 4000) -> list[str]:
|
||||
"""将一个 band 的 XML 在元素边界处切分为多个窗口。
|
||||
|
||||
每个窗口是合法的 XML 片段(完整的 <band>...</band>),
|
||||
大小不超过 max_chars。
|
||||
"""
|
||||
if band.char_length <= max_chars:
|
||||
return [band.band_xml]
|
||||
|
||||
inner = _extract_band_inner(band.band_xml)
|
||||
if not inner:
|
||||
return [band.band_xml]
|
||||
|
||||
segments = _split_at_boundaries(inner, _SAFE_SPLIT_CLOSING)
|
||||
if len(segments) <= 1:
|
||||
return [band.band_xml]
|
||||
|
||||
windows = _greedy_aggregate(segments, band.band_xml, max_chars)
|
||||
return windows
|
||||
|
||||
|
||||
# ── 重组 ──────────────────────────────────────────────────────────
|
||||
|
||||
def _recalc_band_height(band_xml: str, margin: int = 20) -> str:
|
||||
"""根据波段内所有子元素的 y + height 重新计算波段 height。"""
|
||||
max_bottom = 0
|
||||
for m in re.finditer(r'<reportElement\b([^>]*)/>', band_xml):
|
||||
attrs = m.group(1)
|
||||
ym = re.search(r'\sy\s*=\s*"(\d+)"', attrs)
|
||||
hm = re.search(r'\sheight\s*=\s*"(\d+)"', attrs)
|
||||
if ym and hm:
|
||||
bottom = int(ym.group(1)) + int(hm.group(1))
|
||||
if bottom > max_bottom:
|
||||
max_bottom = bottom
|
||||
if max_bottom == 0:
|
||||
return band_xml
|
||||
new_height = max_bottom + margin
|
||||
return re.sub(
|
||||
r'(<band\b[^>]*\sheight\s*=\s*)"(\d+)"',
|
||||
rf'\g<1>"{new_height}"',
|
||||
band_xml,
|
||||
count=1,
|
||||
)
|
||||
|
||||
|
||||
def reassemble_band_windows(modified_windows: list[str]) -> str:
|
||||
"""将多个窗口的修改结果重新合并为一个 band XML。
|
||||
|
||||
策略:取第一个窗口的开头(band 标签)和最后一个窗口的结尾(/band 标签),
|
||||
中间拼接所有窗口内部的元素内容。
|
||||
"""
|
||||
if len(modified_windows) == 1:
|
||||
return _recalc_band_height(modified_windows[0])
|
||||
|
||||
first = modified_windows[0]
|
||||
band_open_end = first.find(">")
|
||||
if band_open_end == -1:
|
||||
return _recalc_band_height("\n".join(modified_windows))
|
||||
band_open = first[:band_open_end + 1]
|
||||
|
||||
last = modified_windows[-1]
|
||||
band_close = _extract_band_close(last)
|
||||
|
||||
inner_parts = []
|
||||
for win in modified_windows:
|
||||
inner = _extract_band_inner(win)
|
||||
if inner:
|
||||
inner_parts.append(inner)
|
||||
|
||||
return _recalc_band_height(band_open + "\n" + "\n".join(inner_parts) + "\n" + band_close)
|
||||
|
||||
|
||||
def reassemble_jrxml(parts: JRXMLParts, modified_bands: dict[str, str]) -> str:
|
||||
"""将修改后的 bands 与 header/footer 重新组装为完整 JRXML。
|
||||
|
||||
modified_bands 的 key 格式为 "{section_name}_band{index}" 或 "{section_name}"(index=0 时)。
|
||||
"""
|
||||
result = []
|
||||
if parts.declaration:
|
||||
result.append(parts.declaration)
|
||||
result.append(parts.root_open)
|
||||
if parts.header_xml.strip():
|
||||
result.append(parts.header_xml)
|
||||
|
||||
current_section = None
|
||||
for band in parts.bands:
|
||||
if band.section_name != current_section:
|
||||
if current_section is not None:
|
||||
result.append(f"</{current_section}>")
|
||||
current_section = band.section_name
|
||||
result.append(f"<{current_section}>")
|
||||
|
||||
modified = modified_bands.get(band.label, band.band_xml)
|
||||
result.append(modified)
|
||||
|
||||
if current_section is not None:
|
||||
result.append(f"</{current_section}>")
|
||||
|
||||
result.append(parts.footer)
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
# ── 元素计数与校验 ────────────────────────────────────────────────
|
||||
|
||||
_ELEMENT_RE = re.compile(r"<(?:[\w:]+:)?(textField|staticText|field)\b", re.IGNORECASE)
|
||||
|
||||
|
||||
def count_elements(jrxml: str) -> int:
|
||||
"""正则计数 JRXML 中的 textField + staticText + field 声明。"""
|
||||
return len(_ELEMENT_RE.findall(jrxml))
|
||||
|
||||
|
||||
def validate_element_count(original: str, modified: str, stage: str) -> dict:
|
||||
"""校验修改前后的元素数变化。
|
||||
|
||||
返回:
|
||||
{"ok": bool, "original": int, "modified": int, "change_pct": float}
|
||||
变化 > 10% 时 ok=False,调用方应回退。
|
||||
"""
|
||||
orig = count_elements(original)
|
||||
mod = count_elements(modified)
|
||||
if orig == 0:
|
||||
return {"ok": True, "original": 0, "modified": mod, "change_pct": 0}
|
||||
change = abs(mod - orig) / orig
|
||||
ok = change <= 0.10
|
||||
if not ok:
|
||||
_windower_log.error(
|
||||
"%s 元素数变化过大: %d → %d (%.1f%%)",
|
||||
stage, orig, mod, change * 100,
|
||||
)
|
||||
elif change > 0.05:
|
||||
_windower_log.warning(
|
||||
"%s 元素数有差异: %d → %d (%.1f%%)",
|
||||
stage, orig, mod, change * 100,
|
||||
)
|
||||
return {"ok": ok, "original": orig, "modified": mod, "change_pct": round(change, 4)}
|
||||
|
||||
|
||||
# ── 内部工具函数 ──────────────────────────────────────────────────
|
||||
|
||||
def _local_tag(tag: str) -> str:
|
||||
"""去除 XML 命名空间前缀。"""
|
||||
return tag.split("}")[-1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _elem_to_string(elem: ET.Element) -> str:
|
||||
"""将 ElementTree 元素序列化为字符串(使用 defusedxml 的 tostring)。"""
|
||||
raw = ET.tostring(elem, encoding="unicode")
|
||||
return raw.strip()
|
||||
|
||||
|
||||
def _build_root_open(jrxml: str, root: ET.Element) -> str:
|
||||
"""从原始文本重建 <jasperReport ...> 开头标签。"""
|
||||
m = re.search(r"<jasperReport\b[^>]*>", jrxml, re.IGNORECASE)
|
||||
if m:
|
||||
return m.group(0)
|
||||
attrs = []
|
||||
for k, v in root.attrib.items():
|
||||
attrs.append(f'{k}="{v}"')
|
||||
return "<jasperReport " + " ".join(attrs) + ">"
|
||||
|
||||
|
||||
def _extract_footer(jrxml: str) -> str:
|
||||
"""提取 </jasperReport> 闭合标签。"""
|
||||
m = re.search(r"</(?:[\w:]+:)?jasperReport>\s*$", jrxml, re.IGNORECASE)
|
||||
if m:
|
||||
return m.group(0).rstrip()
|
||||
return "</jasperReport>"
|
||||
|
||||
|
||||
_BAND_CLOSE_RE = re.compile(r"</(?:[\w:]+:)?band>\s*$", re.IGNORECASE)
|
||||
|
||||
def _extract_band_close(band_xml: str) -> str:
|
||||
"""提取 band 的闭合标签(兼容命名空间前缀),如 '</ns0:band>' 或 '</band>'。"""
|
||||
m = _BAND_CLOSE_RE.search(band_xml)
|
||||
return m.group(0).rstrip() if m else "</band>"
|
||||
|
||||
def _extract_band_inner(band_xml: str) -> str:
|
||||
"""提取 <band ...> 和 </ns0:band> 之间的内容(兼容命名空间前缀)。"""
|
||||
tag_end = band_xml.find(">")
|
||||
if tag_end == -1:
|
||||
return ""
|
||||
close_m = _BAND_CLOSE_RE.search(band_xml)
|
||||
if not close_m:
|
||||
return band_xml[tag_end + 1:].strip()
|
||||
return band_xml[tag_end + 1:close_m.start()].strip()
|
||||
|
||||
|
||||
def _split_at_boundaries(text: str, boundary_re: re.Pattern) -> list[str]:
|
||||
"""在正则匹配的闭合标签处切分文本。
|
||||
|
||||
返回切分后的片段列表(分隔符附加到前一个片段末尾)。
|
||||
"""
|
||||
segments = []
|
||||
last_end = 0
|
||||
for m in boundary_re.finditer(text):
|
||||
end = m.end()
|
||||
segments.append(text[last_end:end])
|
||||
last_end = end
|
||||
if last_end < len(text):
|
||||
segments.append(text[last_end:])
|
||||
elif not segments:
|
||||
segments.append(text)
|
||||
return segments
|
||||
|
||||
|
||||
def _greedy_aggregate(segments: list[str], band_xml: str, max_chars: int) -> list[str]:
|
||||
"""贪心聚合:将片段组合成不超过 max_chars 的窗口。
|
||||
|
||||
每个窗口包上 <band ...> 和 </band> 标签。
|
||||
"""
|
||||
tag_end = band_xml.find(">")
|
||||
band_open = band_xml[:tag_end + 1] if tag_end != -1 else "<band>"
|
||||
band_close = _extract_band_close(band_xml)
|
||||
overhead = len(band_open) + len(band_close) + 1 # +1 for \n
|
||||
|
||||
windows = []
|
||||
current = []
|
||||
current_len = overhead
|
||||
|
||||
for seg in segments:
|
||||
seg_len = len(seg)
|
||||
if current and current_len + seg_len > max_chars:
|
||||
windows.append(band_open + "\n" + "".join(current) + "\n" + band_close)
|
||||
current = [seg]
|
||||
current_len = overhead + seg_len
|
||||
else:
|
||||
current.append(seg)
|
||||
current_len += seg_len
|
||||
|
||||
if current:
|
||||
windows.append(band_open + "\n" + "".join(current) + "\n" + band_close)
|
||||
|
||||
return windows
|
||||
|
||||
|
||||
def _count_elements_in_text(xml_text: str) -> int:
|
||||
"""统计 XML 文本中的 textField + staticText 数量。"""
|
||||
return len(_ELEMENT_RE.findall(xml_text))
|
||||
+1327
-172
File diff suppressed because it is too large
Load Diff
@@ -31,3 +31,34 @@ class AgentState(TypedDict, total=False):
|
||||
# 需求3:意图识别
|
||||
intent: str
|
||||
history_states: List[dict]
|
||||
|
||||
# 需求4:JRXML 版本历史(用于下载历史版本)
|
||||
jrxml_versions: List[dict]
|
||||
|
||||
# 需求5:错误自增长(记录修正前的状态,供 validate 节点判断是否入知识库)
|
||||
last_error_case: dict
|
||||
|
||||
# 需求6:失败上下文传递 — 重试耗尽后暂存失败信息,下次用户输入时自动注入
|
||||
pending_failure_context: dict
|
||||
|
||||
# 需求7:OCR 单据字段精确提取结果
|
||||
ocr_extraction_result: dict
|
||||
uploaded_file_path: str
|
||||
|
||||
# 需求8:图片批注检测(圈选/箭头标记)
|
||||
annotation_result: dict
|
||||
|
||||
# 需求9:分层精确生成
|
||||
layout_schema: dict # extract_layout_schema() 输出,列+区域结构
|
||||
ocr_elements: list # OCR 原始行数据(用于阶段二坐标采样)
|
||||
|
||||
# 需求10:多租户知识库
|
||||
kb_id: str # 当前会话绑定的知识库 ID
|
||||
kb_fields: list # KB 提取的字段定义 [{name, description, type, required}]
|
||||
kb_field_mapping: dict # OCR 字段 → KB 字段映射 {"工单号": "billNo", ...}
|
||||
uploaded_template_jrxml: str # 对话中上传的 JRXML 模板原文
|
||||
uploaded_template_params: list # 解析出的参数 [{name, type}]
|
||||
kb_template_jrxml: str # 从 KB 检索到的模板 JRXML
|
||||
kb_template_name: str # 检索到的模板名称
|
||||
datasource_mode: str # "parameter" 或 "jdbc"
|
||||
db_config: dict # JDBC 连接配置
|
||||
|
||||
+949
@@ -0,0 +1,949 @@
|
||||
"""JRXML Agent API Server — FastAPI + SSE streaming.
|
||||
|
||||
Replaces the Streamlit UI (app.py) with a REST + SSE backend.
|
||||
The LangGraph agent pipeline is wrapped unchanged.
|
||||
|
||||
SSE Event Types:
|
||||
node_start — 节点开始执行
|
||||
node_complete — 节点执行完成(含详情)
|
||||
stream_token — LLM 逐字输出
|
||||
agent_complete — 全图执行完成
|
||||
agent_error — 执行异常
|
||||
|
||||
Usage:
|
||||
python -m uvicorn api_server:app --host 0.0.0.0 --port 8000
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import contextvars
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import queue
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, BackgroundTasks
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse, FileResponse
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
from agent.graph import build_graph
|
||||
from agent.state import AgentState
|
||||
from backend.logger import get_logger, generate_trace_id, set_trace_id, get_trace_id
|
||||
from backend.session import (
|
||||
create_session,
|
||||
load_session,
|
||||
save_session,
|
||||
list_all_sessions,
|
||||
delete_session,
|
||||
get_session_state,
|
||||
SESSIONS_DIR,
|
||||
)
|
||||
from backend.file_parser import parse_file
|
||||
from backend.layout_analyzer import analyze_layout, extract_layout_schema
|
||||
from backend.kb_manager import (
|
||||
create_user, list_users, get_user, delete_user,
|
||||
create_kb, list_kbs, get_kb, update_kb_meta, delete_kb,
|
||||
get_kb_raw_dir,
|
||||
)
|
||||
from backend.kb_parser import parse_jrxml_fields, build_kb_from_files
|
||||
from backend.kb_searcher import search_kb, search_templates_in_kb
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 常量(从 app.py 迁移)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
NODE_LABELS = {
|
||||
"load_session": "加载会话",
|
||||
"process_input": "记录输入",
|
||||
"manage_context": "管理上下文",
|
||||
"save_state_snapshot": "保存快照",
|
||||
"classify_intent": "识别意图",
|
||||
"retrieve": "检索模板",
|
||||
"generate": "生成 JRXML",
|
||||
"modify_jrxml": "修改 JRXML",
|
||||
"validate": "验证",
|
||||
"explain_error": "分析错误",
|
||||
"correct_jrxml": "自动修正",
|
||||
"finalize": "完成",
|
||||
"handle_consult": "咨询回答",
|
||||
"handle_undo": "撤销操作",
|
||||
"handle_reset": "重置会话",
|
||||
"save_session": "保存会话",
|
||||
"generate_skeleton": "生成骨架",
|
||||
"refine_layout": "精调布局",
|
||||
"map_fields": "映射字段",
|
||||
}
|
||||
|
||||
INTENT_LABELS = {
|
||||
"initial_generation": "新建报表",
|
||||
"modify_report": "修改报表",
|
||||
"preview_report": "预览报表",
|
||||
"export_pdf": "导出 PDF",
|
||||
"export_jrxml": "下载 JRXML",
|
||||
"undo_modification": "撤销修改",
|
||||
"consult_question": "咨询问题",
|
||||
"reset_session": "重置会话",
|
||||
}
|
||||
|
||||
SKIP_NODES = {"load_session", "process_input", "manage_context",
|
||||
"save_state_snapshot", "save_session"}
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 日志 & 路径
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
_api_log = get_logger("api")
|
||||
UPLOADS_DIR = Path(os.getenv("UPLOADS_DIR", "./uploads"))
|
||||
MAX_UPLOAD_SIZE = 50 * 1024 * 1024 # 50 MB
|
||||
|
||||
def _check_session_id(session_id: str) -> None:
|
||||
"""校验 session_id 合法性(防路径穿越),非法时抛出 HTTPException(400)。"""
|
||||
from backend.session import validate_session_id
|
||||
if not validate_session_id(session_id):
|
||||
raise HTTPException(status_code=400, detail=f"Invalid session_id: {session_id!r}")
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 图编译(全局单例,带 node_start 回调)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
# 当前请求的事件队列(单个用户桌面应用)
|
||||
_current_event_queue: Optional[queue.Queue] = None
|
||||
_step_counter: contextvars.ContextVar[int] = contextvars.ContextVar('_step_counter', default=0)
|
||||
|
||||
|
||||
def _on_node_start(node_name: str):
|
||||
"""全局 node_start 回调 — 将事件推入当前请求的事件队列。"""
|
||||
q = _current_event_queue
|
||||
if q is not None:
|
||||
_step_counter.set(_step_counter.get() + 1)
|
||||
q.put(("node_start", {
|
||||
"node": node_name,
|
||||
"label": NODE_LABELS.get(node_name, node_name),
|
||||
"step_index": _step_counter.get(),
|
||||
}))
|
||||
|
||||
|
||||
_graph = build_graph(on_node_start=_on_node_start)
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 文件注册表(内存中,桌面应用级别可接受)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
_file_registry: dict[str, dict] = {} # file_id → {path, filename, content_type, size}
|
||||
|
||||
|
||||
def _ensure_upload_dir(session_id: str = "") -> Path:
|
||||
d = UPLOADS_DIR / session_id if session_id else UPLOADS_DIR
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# SSE 辅助
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
def _extract_detail(node_name: str, node_state: dict) -> str:
|
||||
"""从节点状态中提取详情文本(用于 node_complete 事件)。"""
|
||||
if node_name == "classify_intent":
|
||||
intent = node_state.get("intent", "")
|
||||
return f"意图: {INTENT_LABELS.get(intent, intent)}"
|
||||
elif node_name == "retrieve":
|
||||
ctx = node_state.get("retrieved_context", "")
|
||||
return f"找到 {len(ctx)} 字符参考模板" if ctx else "未匹配到模板"
|
||||
elif node_name in ("generate", "modify_jrxml", "correct_jrxml",
|
||||
"generate_skeleton", "refine_layout", "map_fields"):
|
||||
jrxml = node_state.get("current_jrxml", "")
|
||||
return f"生成 {len(jrxml)} 字符 JRXML"
|
||||
elif node_name == "validate":
|
||||
status = node_state.get("status", "")
|
||||
if status == "pass":
|
||||
return "验证通过 ✓"
|
||||
err = node_state.get("error_msg", "")
|
||||
return f"验证失败: {err[:80]}"
|
||||
elif node_name == "explain_error":
|
||||
expl = node_state.get("natural_explanation", "")
|
||||
return expl[:120]
|
||||
elif node_name == "handle_consult":
|
||||
ans = node_state.get("consult_answer", "")
|
||||
return ans[:150]
|
||||
return ""
|
||||
|
||||
|
||||
def _run_graph_sync(agent_state: AgentState, event_q: queue.Queue):
|
||||
"""在后台线程中运行 graph.stream(),将所有事件推入队列。
|
||||
|
||||
graph.stream() 只产出事件,不修改传入的 agent_state。
|
||||
因此需要手动收集每个节点的返回并合并到 agent_state。
|
||||
"""
|
||||
try:
|
||||
for event in _graph.stream(agent_state, stream_mode=["updates", "custom"]):
|
||||
event_q.put(event)
|
||||
# 将节点更新合并到 agent_state
|
||||
if isinstance(event, tuple) and len(event) == 2:
|
||||
mode, data = event
|
||||
if mode == "updates" and isinstance(data, dict):
|
||||
for node_state in data.values():
|
||||
if isinstance(node_state, dict):
|
||||
agent_state.update({k: v for k, v in node_state.items() if v is not None})
|
||||
# 在 graph 完成后立即保存 session,防止 SSE 流中断导致数据丢失
|
||||
sid = agent_state.get("session_id", "")
|
||||
if sid:
|
||||
try:
|
||||
save_session(sid, agent_state)
|
||||
except Exception as exc:
|
||||
_api_log.error("图运行中保存会话失败", extra={
|
||||
"session_id": sid,
|
||||
"error": str(exc),
|
||||
"traceback": traceback.format_exc(),
|
||||
})
|
||||
event_q.put(("done", {"reason": "graph_completed"}))
|
||||
except Exception as exc:
|
||||
event_q.put(("error", {
|
||||
"error": str(exc),
|
||||
"traceback": traceback.format_exc(),
|
||||
}))
|
||||
|
||||
|
||||
async def _sse_generator(agent_state: AgentState, session_id: str = "") -> str:
|
||||
"""SSE 事件生成器 —— 在后台线程运行图,异步产出 SSE 字符串。"""
|
||||
global _current_event_queue
|
||||
|
||||
_step_counter.set(0)
|
||||
t_start = time.time()
|
||||
event_q: queue.Queue = queue.Queue()
|
||||
_current_event_queue = event_q
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
future = loop.run_in_executor(None, _run_graph_sync, agent_state, event_q)
|
||||
|
||||
# 从队列读取事件,写 SSE(用 short sleep 做非阻塞轮询)
|
||||
while True:
|
||||
# 先排空队列中的所有事件
|
||||
had_events = False
|
||||
while True:
|
||||
try:
|
||||
item = event_q.get_nowait()
|
||||
had_events = True
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
kind = item[0]
|
||||
if kind == "done":
|
||||
_current_event_queue = None
|
||||
total_ms = round((time.time() - t_start) * 1000)
|
||||
if session_id:
|
||||
save_session(session_id, agent_state)
|
||||
versions = agent_state.get("jrxml_versions", [])
|
||||
last_ver = versions[-1] if versions else {}
|
||||
yield _sse_line("agent_complete", {
|
||||
"reason": "done",
|
||||
"intent": agent_state.get("intent", ""),
|
||||
"status": agent_state.get("status", ""),
|
||||
"jrxml_length": len(agent_state.get("current_jrxml", "")),
|
||||
"error_msg": agent_state.get("error_msg", ""),
|
||||
"natural_explanation": agent_state.get("natural_explanation", ""),
|
||||
"consult_answer": agent_state.get("consult_answer", ""),
|
||||
"retry_count": agent_state.get("retry_count", 0),
|
||||
"total_duration_ms": total_ms,
|
||||
"ocr_extraction_result": agent_state.get("ocr_extraction_result", {}),
|
||||
"versions": len(versions),
|
||||
"has_failed_version": last_ver.get("status") == "fail" if last_ver else False,
|
||||
"failed_version_index": len(versions) - 1 if last_ver.get("status") == "fail" else -1,
|
||||
})
|
||||
await future
|
||||
return
|
||||
|
||||
elif kind == "error":
|
||||
_current_event_queue = None
|
||||
yield _sse_line("agent_error", item[1])
|
||||
await future
|
||||
return
|
||||
|
||||
elif kind == "node_start":
|
||||
yield _sse_line("node_start", item[1])
|
||||
|
||||
else:
|
||||
# mode=data 来自 graph.stream()
|
||||
mode, data = item
|
||||
if mode == "updates":
|
||||
for node_name, node_state in data.items():
|
||||
detail = _extract_detail(node_name, node_state)
|
||||
yield _sse_line("node_complete", {
|
||||
"node": node_name,
|
||||
"label": NODE_LABELS.get(node_name, node_name),
|
||||
"detail": detail,
|
||||
})
|
||||
elif mode == "custom":
|
||||
cd = data
|
||||
if cd.get("type") == "stream":
|
||||
yield _sse_line("stream_token", {
|
||||
"text": cd.get("text", ""),
|
||||
"type": "stream",
|
||||
})
|
||||
|
||||
if not had_events:
|
||||
await asyncio.sleep(0.05)
|
||||
yield ": keepalive\n\n"
|
||||
|
||||
|
||||
def _sse_line(event_type: str, data: dict) -> str:
|
||||
"""构造单条 SSE 消息。"""
|
||||
payload = json.dumps(data, ensure_ascii=False)
|
||||
return f"event: {event_type}\ndata: {payload}\n\n"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# FastAPI 应用
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
app = FastAPI(
|
||||
title="JRXML Agent API",
|
||||
version="5.0",
|
||||
description="JRXML 报表生成代理 — 前后端分离 API",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 健康检查 & 配置
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health():
|
||||
return {
|
||||
"status": "ok",
|
||||
"version": "5.0",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/config")
|
||||
async def config():
|
||||
safe = {}
|
||||
for key in ("LLM_PROVIDER", "OCR_ENGINE", "EMBEDDING_PROVIDER",
|
||||
"MAX_RETRY", "CONTEXT_MAX_TOKENS", "CONTEXT_KEEP_RECENT"):
|
||||
val = os.getenv(key, "")
|
||||
safe[key] = val
|
||||
return {"config": safe}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 会话 CRUD
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.post("/api/sessions")
|
||||
async def create_new_session():
|
||||
data = create_session()
|
||||
return {
|
||||
"session_id": data["session_id"],
|
||||
"session_name": data["session_name"],
|
||||
"created_at": data["created_at"],
|
||||
"updated_at": data["updated_at"],
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/sessions")
|
||||
async def list_sessions():
|
||||
return {"sessions": list_all_sessions()}
|
||||
|
||||
|
||||
@app.get("/api/sessions/{session_id}")
|
||||
async def get_session(session_id: str):
|
||||
_check_session_id(session_id)
|
||||
data = get_session_state(session_id)
|
||||
if data is None:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
return {
|
||||
"session_id": data.get("session_id"),
|
||||
"session_name": data.get("session_name"),
|
||||
"created_at": data.get("created_at"),
|
||||
"updated_at": data.get("updated_at"),
|
||||
"agent_state": data.get("agent_state", {}),
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/api/sessions/{session_id}")
|
||||
async def remove_session(session_id: str):
|
||||
_check_session_id(session_id)
|
||||
ok = delete_session(session_id)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="会话不存在或已删除")
|
||||
return {"status": "deleted", "session_id": session_id}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 用户管理
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.post("/api/users")
|
||||
async def create_new_user(payload: dict):
|
||||
name = payload.get("name", "").strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="用户名不能为空")
|
||||
try:
|
||||
user = create_user(name)
|
||||
return user
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/users")
|
||||
async def list_all_users():
|
||||
return {"users": list_users()}
|
||||
|
||||
|
||||
@app.get("/api/users/{user_id}")
|
||||
async def get_user_info(user_id: str):
|
||||
user = get_user(user_id)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return user
|
||||
|
||||
|
||||
@app.delete("/api/users/{user_id}")
|
||||
async def remove_user(user_id: str):
|
||||
ok = delete_user(user_id)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return {"status": "deleted", "user_id": user_id}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 知识库 CRUD
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/users/{user_id}/kbs")
|
||||
async def list_user_kbs(user_id: str):
|
||||
return {"kbs": list_kbs(user_id)}
|
||||
|
||||
|
||||
@app.post("/api/users/{user_id}/kbs")
|
||||
async def create_user_kb(user_id: str, payload: dict):
|
||||
name = payload.get("name", "").strip()
|
||||
description = payload.get("description", "")
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="知识库名称不能为空")
|
||||
try:
|
||||
kb = create_kb(user_id, name, description)
|
||||
return kb
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/kbs/{kb_id}")
|
||||
async def get_kb_info(kb_id: str):
|
||||
kb = get_kb(kb_id)
|
||||
if kb is None:
|
||||
raise HTTPException(status_code=404, detail="知识库不存在")
|
||||
return kb
|
||||
|
||||
|
||||
@app.delete("/api/kbs/{kb_id}")
|
||||
async def remove_kb(kb_id: str):
|
||||
ok = delete_kb(kb_id)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="知识库不存在")
|
||||
return {"status": "deleted", "kb_id": kb_id}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 知识库文件上传
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.post("/api/kbs/{kb_id}/upload")
|
||||
async def upload_to_kb(kb_id: str, file: UploadFile = File(...)):
|
||||
kb = get_kb(kb_id)
|
||||
if kb is None:
|
||||
raise HTTPException(status_code=404, detail="知识库不存在")
|
||||
|
||||
raw_dir = get_kb_raw_dir(kb_id)
|
||||
if raw_dir is None:
|
||||
raise HTTPException(status_code=500, detail="知识库存储目录不可用")
|
||||
|
||||
raw_dir.mkdir(parents=True, exist_ok=True)
|
||||
safe_name = Path(file.filename or "upload").name
|
||||
dest = raw_dir / safe_name
|
||||
|
||||
content = await file.read()
|
||||
if len(content) > MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(status_code=413, detail="文件大小超过 50MB 上限")
|
||||
|
||||
dest.write_bytes(content)
|
||||
|
||||
from backend.kb_parser import process_file_for_kb
|
||||
result = process_file_for_kb(kb_id, str(dest), source_name=safe_name)
|
||||
|
||||
_api_log.info("KB文件上传", extra={
|
||||
"kb_id": kb_id, "file": safe_name, "type": result.get("type"),
|
||||
})
|
||||
|
||||
return {
|
||||
"filename": safe_name,
|
||||
"type": result.get("type", ""),
|
||||
"error": result.get("error"),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/kbs/{kb_id}/build")
|
||||
async def build_kb(kb_id: str):
|
||||
"""构建知识库:对已上传的文件执行 chunk → embed 管线。"""
|
||||
from backend.kb_parser import build_kb_from_files as build_fn
|
||||
raw_dir = get_kb_raw_dir(kb_id)
|
||||
if raw_dir is None or not raw_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="知识库无已上传文件")
|
||||
|
||||
files = [str(p) for p in raw_dir.iterdir() if p.is_file()]
|
||||
if not files:
|
||||
raise HTTPException(status_code=400, detail="知识库无文件,请先上传")
|
||||
|
||||
result = build_fn(kb_id, files)
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/api/kbs/{kb_id}/status")
|
||||
async def kb_status(kb_id: str):
|
||||
kb = get_kb(kb_id)
|
||||
if kb is None:
|
||||
raise HTTPException(status_code=404, detail="知识库不存在")
|
||||
return {
|
||||
"kb_id": kb_id,
|
||||
"name": kb.get("name", ""),
|
||||
"field_count": len(kb.get("fields", [])),
|
||||
"template_count": len(kb.get("templates", [])),
|
||||
"file_count": kb.get("file_count", 0),
|
||||
"chunk_count": kb.get("chunk_count", 0),
|
||||
"parse_status": kb.get("parse_status", "empty"),
|
||||
"created_at": kb.get("created_at", ""),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/kbs/{kb_id}/fields")
|
||||
async def kb_fields(kb_id: str):
|
||||
kb = get_kb(kb_id)
|
||||
if kb is None:
|
||||
raise HTTPException(status_code=404, detail="知识库不存在")
|
||||
return {"fields": kb.get("fields", []), "templates": kb.get("templates", [])}
|
||||
|
||||
|
||||
@app.get("/api/kbs/{kb_id}/search")
|
||||
async def kb_search(kb_id: str, q: str = "", type: str = ""):
|
||||
if not q:
|
||||
raise HTTPException(status_code=400, detail="查询参数 q 不能为空")
|
||||
if type == "template":
|
||||
results = search_templates_in_kb(kb_id, q, k=5)
|
||||
else:
|
||||
ctx = search_kb(kb_id, q, k=5)
|
||||
return {"query": q, "context": ctx}
|
||||
return {"query": q, "results": results}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 会话-知识库绑定
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.put("/api/sessions/{session_id}/kb")
|
||||
async def bind_session_kb(session_id: str, payload: dict):
|
||||
_check_session_id(session_id)
|
||||
kb_id = payload.get("kb_id", "").strip()
|
||||
data = load_session(session_id)
|
||||
if data is None:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
|
||||
agent_state = data.get("agent_state", {})
|
||||
if kb_id:
|
||||
kb = get_kb(kb_id)
|
||||
if kb is None:
|
||||
raise HTTPException(status_code=404, detail="知识库不存在")
|
||||
agent_state["kb_id"] = kb_id
|
||||
agent_state["kb_fields"] = kb.get("fields", [])
|
||||
else:
|
||||
agent_state.pop("kb_id", None)
|
||||
agent_state.pop("kb_fields", None)
|
||||
|
||||
save_session(session_id, agent_state)
|
||||
return {"session_id": session_id, "kb_id": kb_id or None}
|
||||
|
||||
|
||||
@app.get("/api/sessions/{session_id}/kb")
|
||||
async def get_session_kb(session_id: str):
|
||||
_check_session_id(session_id)
|
||||
data = load_session(session_id)
|
||||
if data is None:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
agent_state = data.get("agent_state", {})
|
||||
kb_id = agent_state.get("kb_id", "")
|
||||
result = {"kb_id": kb_id, "kb_fields": agent_state.get("kb_fields", [])}
|
||||
if kb_id:
|
||||
kb = get_kb(kb_id)
|
||||
if kb:
|
||||
result["kb_name"] = kb.get("name", "")
|
||||
result["templates"] = kb.get("templates", [])
|
||||
return result
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 文件上传
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.post("/api/upload")
|
||||
async def upload_file(file: UploadFile = File(...), session_id: str = ""):
|
||||
if session_id:
|
||||
_check_session_id(session_id)
|
||||
file_id = uuid.uuid4().hex[:12]
|
||||
_ensure_upload_dir(session_id)
|
||||
|
||||
# 保留原始文件名
|
||||
safe_name = Path(file.filename or "upload.bin").name
|
||||
dest = _ensure_upload_dir(session_id) / f"{file_id}_{safe_name}"
|
||||
|
||||
content = await file.read()
|
||||
if len(content) > MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(status_code=413, detail="文件大小超过 50MB 上限")
|
||||
|
||||
dest.write_bytes(content)
|
||||
|
||||
content_type = file.content_type or mimetypes.guess_type(safe_name)[0] or "application/octet-stream"
|
||||
|
||||
_file_registry[file_id] = {
|
||||
"path": str(dest),
|
||||
"filename": safe_name,
|
||||
"content_type": content_type,
|
||||
"size": len(content),
|
||||
}
|
||||
|
||||
_api_log.info("文件上传", extra={
|
||||
"file_id": file_id, "file_name": safe_name, "size": len(content),
|
||||
})
|
||||
|
||||
return {
|
||||
"file_id": file_id,
|
||||
"filename": safe_name,
|
||||
"content_type": content_type,
|
||||
"size": len(content),
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 文件处理辅助
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
def _parse_jrxml_file(file_path: str) -> dict:
|
||||
"""解析上传的 JRXML 文件,提取模板参数和字段。
|
||||
|
||||
Returns:
|
||||
{jrxml_text, parameters: [{name, type}], fields: [{name, type}],
|
||||
query: str, report_name: str, page_width: str, page_height: str}
|
||||
"""
|
||||
jrxml_info = parse_jrxml_fields(file_path)
|
||||
try:
|
||||
raw_xml = Path(file_path).read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
raw_xml = ""
|
||||
return {
|
||||
"jrxml_text": raw_xml,
|
||||
"parameters": jrxml_info.get("parameters", []),
|
||||
"fields": jrxml_info.get("fields", []),
|
||||
"query": jrxml_info.get("query", ""),
|
||||
"report_name": jrxml_info.get("report_name", ""),
|
||||
"page_width": jrxml_info.get("page_width", ""),
|
||||
"page_height": jrxml_info.get("page_height", ""),
|
||||
"error": jrxml_info.get("error"),
|
||||
}
|
||||
|
||||
|
||||
def _process_files(file_ids: list[str], session_id: str) -> dict:
|
||||
"""处理上传的文件:解析 → 布局分析 → 提取 schema 文本。
|
||||
JRXML 文件额外解析为模板上下文注入 agent_state。
|
||||
|
||||
Returns:
|
||||
{full_prompt_prefix, uploaded_paths, layout_schema, ocr_text,
|
||||
jrxml_template: dict | None}
|
||||
"""
|
||||
if not file_ids:
|
||||
return {"full_prompt_prefix": "", "uploaded_paths": [],
|
||||
"layout_schema": {}, "ocr_text": "", "jrxml_template": None}
|
||||
|
||||
parts = []
|
||||
uploaded_paths = []
|
||||
layout_schema = {}
|
||||
ocr_text = ""
|
||||
jrxml_template = None
|
||||
|
||||
for fid in file_ids:
|
||||
info = _file_registry.get(fid)
|
||||
if not info:
|
||||
_api_log.warning("文件ID未注册", extra={"file_id": fid})
|
||||
continue
|
||||
|
||||
file_path = info["path"]
|
||||
uploaded_paths.append(file_path)
|
||||
suffix = Path(info["filename"]).suffix.lower()
|
||||
|
||||
# JRXML 文件 → 解析为模板
|
||||
if suffix == ".jrxml":
|
||||
jrxml_template = _parse_jrxml_file(file_path)
|
||||
if jrxml_template.get("error"):
|
||||
parts.append(f"[JRXML 模板: {info['filename']}]\n解析失败: {jrxml_template['error']}")
|
||||
else:
|
||||
params = jrxml_template["parameters"]
|
||||
fields = jrxml_template["fields"]
|
||||
param_desc = "\n".join(
|
||||
f" - {p['name']} ({p.get('type', 'String')})" for p in params
|
||||
) if params else " (无参数)"
|
||||
field_desc = "\n".join(
|
||||
f" - {f['name']} ({f.get('type', 'String')})" for f in fields
|
||||
) if fields else " (无字段)"
|
||||
parts.append(
|
||||
f"[上传的 JRXML 模板: {jrxml_template['report_name'] or info['filename']}]\n"
|
||||
f"页面尺寸: {jrxml_template['page_width']}x{jrxml_template['page_height']}\n"
|
||||
f"参数列表:\n{param_desc}\n"
|
||||
f"字段列表:\n{field_desc}\n"
|
||||
f"SQL查询: {jrxml_template['query'] or '(无)'}\n"
|
||||
f"--- XML 内容 ---\n{jrxml_template['jrxml_text']}"
|
||||
)
|
||||
continue
|
||||
|
||||
parsed = parse_file(file_path, suffix)
|
||||
if parsed.get("error"):
|
||||
parts.append(f"[文件: {info['filename']}]\n解析失败: {parsed['error']}")
|
||||
continue
|
||||
|
||||
parts.append(f"[文件: {info['filename']}]\n{parsed['text']}")
|
||||
|
||||
# 图片文件 → 布局分析
|
||||
if info["content_type"] and info["content_type"].startswith("image/"):
|
||||
layout = analyze_layout(file_path)
|
||||
if layout.get("is_a4_template"):
|
||||
parts.append(
|
||||
f"\n[A4模板布局]\n"
|
||||
f"表格行数: {layout.get('total_rows', 0)}, "
|
||||
f"总元素: {layout.get('total_elements', 0)}, "
|
||||
f"比例: {layout.get('a4_confidence', '')}"
|
||||
)
|
||||
if layout.get("description"):
|
||||
parts.append(f"\n[布局描述]\n{layout['description']}")
|
||||
|
||||
schema = extract_layout_schema(layout)
|
||||
if schema and schema.get("total_rows", 0) > 0:
|
||||
layout_schema = schema
|
||||
schema_text = schema.get("schema_text", "")
|
||||
if schema_text:
|
||||
parts.append(f"\n[布局Schema]\n{schema_text}")
|
||||
|
||||
# OCR 元素文本
|
||||
ocr_elements = layout.get("rows", [])
|
||||
if ocr_elements:
|
||||
ocr_lines = []
|
||||
for row in ocr_elements[:30]:
|
||||
texts = [e.get("text", "") for e in row.get("elements", [])]
|
||||
ocr_lines.append(" | ".join(texts))
|
||||
ocr_text = "\n".join(ocr_lines)
|
||||
if ocr_lines:
|
||||
parts.append(f"\n[OCR 识别文本]\n{ocr_text}")
|
||||
|
||||
return {
|
||||
"full_prompt_prefix": "\n\n".join(parts) if parts else "",
|
||||
"uploaded_paths": uploaded_paths,
|
||||
"layout_schema": layout_schema,
|
||||
"ocr_text": ocr_text,
|
||||
"jrxml_template": jrxml_template,
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 核心:SSE 聊天端点
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.post("/api/sessions/{session_id}/chat")
|
||||
async def chat(session_id: str, payload: dict):
|
||||
"""发送消息并获取 SSE 流式响应。
|
||||
|
||||
Body:
|
||||
{text: str, file_ids: [str, ...]}
|
||||
|
||||
Returns:
|
||||
text/event-stream (SSE)
|
||||
"""
|
||||
_check_session_id(session_id)
|
||||
text = payload.get("text", "").strip()
|
||||
file_ids = payload.get("file_ids", [])
|
||||
|
||||
if not text and not file_ids:
|
||||
raise HTTPException(status_code=400, detail="text 和 file_ids 均为空")
|
||||
|
||||
# ── 加载或创建会话 ──
|
||||
trace_id = generate_trace_id()
|
||||
set_trace_id(trace_id)
|
||||
|
||||
data = load_session(session_id)
|
||||
if data is None:
|
||||
data = create_session(session_id=session_id)
|
||||
_api_log.info("自动创建会话", extra={"session_id": session_id, "trace_id": trace_id})
|
||||
|
||||
agent_state: AgentState = data.get("agent_state", {})
|
||||
agent_state["session_id"] = session_id
|
||||
|
||||
# ── 处理文件 ──
|
||||
file_result = _process_files(file_ids, session_id)
|
||||
full_prompt = text
|
||||
if file_result["full_prompt_prefix"]:
|
||||
full_prompt = f"{file_result['full_prompt_prefix']}\n\n用户问题: {text}" if text else file_result["full_prompt_prefix"]
|
||||
|
||||
# ── 注入布局 schema(用于分层精确生成)──
|
||||
if file_result.get("layout_schema"):
|
||||
agent_state["layout_schema"] = file_result["layout_schema"]
|
||||
if file_result.get("ocr_text"):
|
||||
ocr_rows = [{"elements": [{"text": t} for t in line.split(" | ")]}
|
||||
for line in file_result["ocr_text"].split("\n") if line.strip()]
|
||||
if ocr_rows:
|
||||
agent_state["ocr_elements"] = ocr_rows
|
||||
if file_result.get("uploaded_paths"):
|
||||
agent_state["uploaded_file_path"] = file_result["uploaded_paths"][0]
|
||||
|
||||
# ── 注入 JRXML 模板(对话中上传的模板)──
|
||||
jrxml_tmpl = file_result.get("jrxml_template")
|
||||
if jrxml_tmpl and not jrxml_tmpl.get("error"):
|
||||
agent_state["uploaded_template_jrxml"] = jrxml_tmpl["jrxml_text"]
|
||||
agent_state["uploaded_template_params"] = jrxml_tmpl["parameters"]
|
||||
|
||||
# ── 设置本轮输入 ──
|
||||
if agent_state.get("current_jrxml"):
|
||||
agent_state["user_modification_request"] = full_prompt
|
||||
|
||||
agent_state["user_input"] = full_prompt
|
||||
agent_state["retry_count"] = 0
|
||||
|
||||
_api_log.info("对话请求", extra={
|
||||
"session_id": session_id,
|
||||
"trace_id": trace_id,
|
||||
"text_length": len(text),
|
||||
"file_count": len(file_ids),
|
||||
"prompt_total": len(full_prompt),
|
||||
})
|
||||
|
||||
# ── 返回 SSE 流 ──
|
||||
async def stream_and_save():
|
||||
# 如果上传了附件,先发送处理状态
|
||||
if file_ids:
|
||||
yield _sse_line("node_start", {
|
||||
"node": "process_attachments",
|
||||
"label": "正在处理附件",
|
||||
})
|
||||
yield _sse_line("node_complete", {
|
||||
"node": "process_attachments",
|
||||
"label": "正在处理附件",
|
||||
"detail": f"已解析 {len(file_ids)} 个文件",
|
||||
})
|
||||
async for sse_chunk in _sse_generator(agent_state, session_id):
|
||||
yield sse_chunk
|
||||
|
||||
return StreamingResponse(
|
||||
stream_and_save(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
"X-Trace-Id": trace_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 下载
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/sessions/{session_id}/download/latest")
|
||||
async def download_latest(session_id: str, background_tasks: BackgroundTasks):
|
||||
"""下载最新 JRXML 文件。"""
|
||||
_check_session_id(session_id)
|
||||
data = load_session(session_id)
|
||||
if data is None:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
|
||||
agent_state = data.get("agent_state", {})
|
||||
jrxml = agent_state.get("current_jrxml", "")
|
||||
if not jrxml:
|
||||
raise HTTPException(status_code=404, detail="该会话暂无 JRXML")
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".jrxml", delete=False,
|
||||
encoding="utf-8")
|
||||
tmp.write(jrxml)
|
||||
tmp.close()
|
||||
background_tasks.add_task(os.unlink, tmp.name)
|
||||
|
||||
return FileResponse(
|
||||
tmp.name,
|
||||
media_type="application/xml",
|
||||
filename=f"report_{session_id}.jrxml",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/sessions/{session_id}/download/{version}")
|
||||
async def download_version(session_id: str, version: int, background_tasks: BackgroundTasks):
|
||||
"""下载指定版本的 JRXML 文件。"""
|
||||
_check_session_id(session_id)
|
||||
data = load_session(session_id)
|
||||
if data is None:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
|
||||
versions = data.get("agent_state", {}).get("jrxml_versions", [])
|
||||
if version < 0 or version >= len(versions):
|
||||
raise HTTPException(status_code=404, detail="版本不存在")
|
||||
|
||||
jrxml = versions[version].get("jrxml", "")
|
||||
if not jrxml:
|
||||
raise HTTPException(status_code=404, detail="该版本内容为空")
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".jrxml", delete=False,
|
||||
encoding="utf-8")
|
||||
tmp.write(jrxml)
|
||||
tmp.close()
|
||||
background_tasks.add_task(os.unlink, tmp.name)
|
||||
|
||||
return FileResponse(
|
||||
tmp.name,
|
||||
media_type="application/xml",
|
||||
filename=f"report_{session_id}_v{version}.jrxml",
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 下载上传文件
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/files/{file_id}")
|
||||
async def download_file(file_id: str):
|
||||
info = _file_registry.get(file_id)
|
||||
if not info:
|
||||
raise HTTPException(status_code=404, detail="文件未找到")
|
||||
return FileResponse(info["path"], filename=info["filename"])
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 启动入口
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
port = int(os.getenv("API_PORT", "8000"))
|
||||
uvicorn.run("api_server:app", host="0.0.0.0", port=port, reload=False)
|
||||
@@ -1,318 +0,0 @@
|
||||
"""Streamlit 多轮对话 UI,用于 JRXML 生成代理。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import streamlit as st
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from agent.graph import build_graph, create_initial_state
|
||||
from backend.session import (
|
||||
create_session,
|
||||
load_session,
|
||||
delete_session,
|
||||
list_all_sessions,
|
||||
)
|
||||
|
||||
st.set_page_config(
|
||||
page_title="JRXML 代理",
|
||||
page_icon="📊",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
# ---- URL 参数:session_id ----
|
||||
query_params = st.query_params
|
||||
url_session_id = query_params.get("session_id", "")
|
||||
|
||||
# ---- 会话状态初始化 ----
|
||||
if "messages" not in st.session_state:
|
||||
st.session_state.messages = []
|
||||
if "graph" not in st.session_state:
|
||||
st.session_state.graph = build_graph()
|
||||
if "pending_action" not in st.session_state:
|
||||
st.session_state.pending_action = None
|
||||
|
||||
# 确定活跃的 session_id
|
||||
if "agent_state" not in st.session_state:
|
||||
if url_session_id:
|
||||
data = load_session(url_session_id)
|
||||
if data and data.get("agent_state"):
|
||||
st.session_state.agent_state = data["agent_state"]
|
||||
st.session_state.agent_state["session_id"] = url_session_id
|
||||
else:
|
||||
st.session_state.agent_state = create_initial_state()
|
||||
new_data = create_session(name="", agent_state=st.session_state.agent_state)
|
||||
st.session_state.agent_state["session_id"] = new_data["session_id"]
|
||||
st.session_state.agent_state["session_name"] = new_data["session_name"]
|
||||
st.session_state.agent_state["created_at"] = new_data["created_at"]
|
||||
else:
|
||||
st.session_state.agent_state = create_initial_state()
|
||||
new_data = create_session(name="", agent_state=st.session_state.agent_state)
|
||||
st.session_state.agent_state["session_id"] = new_data["session_id"]
|
||||
st.session_state.agent_state["session_name"] = new_data["session_name"]
|
||||
st.session_state.agent_state["created_at"] = new_data["created_at"]
|
||||
|
||||
current_session_id = st.session_state.agent_state.get("session_id", "")
|
||||
|
||||
|
||||
def run_agent(user_input: str) -> dict:
|
||||
"""运行代理图,返回最终状态。"""
|
||||
agent_state = st.session_state.agent_state
|
||||
|
||||
if agent_state.get("current_jrxml") and agent_state.get("status") == "pass":
|
||||
agent_state["user_modification_request"] = user_input
|
||||
|
||||
agent_state["user_input"] = user_input
|
||||
agent_state["retry_count"] = 0
|
||||
|
||||
final_state = None
|
||||
with st.chat_message("assistant"):
|
||||
status_placeholder = st.empty()
|
||||
jrxml_placeholder = st.empty()
|
||||
|
||||
for event in st.session_state.graph.stream(agent_state):
|
||||
for node_name, node_state in event.items():
|
||||
final_state = node_state
|
||||
if node_name == "classify_intent":
|
||||
intent = node_state.get("intent", "")
|
||||
intent_labels = {
|
||||
"initial_generation": "🆕 识别为新建报表请求",
|
||||
"modify_report": "✏️ 识别为修改报表请求",
|
||||
"preview_report": "👁 识别为预览请求",
|
||||
"export_pdf": "📄 识别为导出PDF请求",
|
||||
"export_jrxml": "📥 识别为导出JRXML请求",
|
||||
"undo_modification": "↩ 识别为撤销请求",
|
||||
"consult_question": "💬 识别为咨询问题",
|
||||
"reset_session": "🔄 识别为重置会话请求",
|
||||
}
|
||||
label = intent_labels.get(intent, f"🔍 意图: {intent}")
|
||||
status_placeholder.info(label)
|
||||
elif node_name == "generate":
|
||||
status_placeholder.info("🔧 正在生成 JRXML...")
|
||||
elif node_name == "modify_jrxml":
|
||||
status_placeholder.info("🔧 正在根据您的请求修改 JRXML...")
|
||||
elif node_name == "validate":
|
||||
if node_state.get("status") == "pass":
|
||||
status_placeholder.success("✅ 验证通过!")
|
||||
else:
|
||||
status_placeholder.warning("⚠ 验证失败,正在分析错误...")
|
||||
elif node_name == "explain_error":
|
||||
explanation = node_state.get("natural_explanation", "")
|
||||
status_placeholder.warning(f"🔍 {explanation}")
|
||||
elif node_name == "correct_jrxml":
|
||||
status_placeholder.info(f"🛠 正在自动修正(尝试 {node_state.get('retry_count', 1)})...")
|
||||
elif node_name == "handle_consult":
|
||||
pass
|
||||
elif node_name == "handle_undo":
|
||||
status_placeholder.info("↩ 已撤销上一步修改")
|
||||
elif node_name == "handle_reset":
|
||||
status_placeholder.info("🔄 会话已重置")
|
||||
elif node_name == "manage_context":
|
||||
pass
|
||||
elif node_name == "save_state_snapshot":
|
||||
pass
|
||||
elif node_name == "save_session":
|
||||
pass
|
||||
elif node_name == "finalize":
|
||||
pass
|
||||
|
||||
if final_state:
|
||||
st.session_state.agent_state = final_state
|
||||
intent = final_state.get("intent", "")
|
||||
|
||||
if intent == "consult_question":
|
||||
answer = final_state.get("consult_answer", "")
|
||||
st.session_state.messages.append({
|
||||
"role": "assistant",
|
||||
"content": answer,
|
||||
"type": "consult",
|
||||
})
|
||||
status_placeholder.empty()
|
||||
st.markdown(answer)
|
||||
elif intent in ("undo_modification", "reset_session"):
|
||||
# 消息已在节点中添加,不需要额外输出
|
||||
status_placeholder.empty()
|
||||
jrxml_placeholder.empty()
|
||||
elif intent in ("preview_report", "export_pdf", "export_jrxml"):
|
||||
jrxml = final_state.get("current_jrxml", "")
|
||||
if jrxml:
|
||||
st.session_state.messages.append({
|
||||
"role": "assistant",
|
||||
"content": jrxml,
|
||||
"type": "jrxml",
|
||||
})
|
||||
status_placeholder.success("✅ 当前报表")
|
||||
jrxml_placeholder.code(jrxml, language="xml")
|
||||
else:
|
||||
status_placeholder.warning("⚠ 当前没有报表可以预览或导出。")
|
||||
jrxml_placeholder.empty()
|
||||
elif final_state.get("status") == "pass":
|
||||
st.session_state.messages.append({
|
||||
"role": "assistant",
|
||||
"content": final_state.get("current_jrxml", ""),
|
||||
"type": "jrxml",
|
||||
})
|
||||
st.session_state.messages.append({
|
||||
"role": "assistant",
|
||||
"content": "✅ JRXML 生成成功!您可以从侧边栏下载文件,或继续修改。",
|
||||
"type": "success",
|
||||
})
|
||||
status_placeholder.success("✅ JRXML 验证通过!")
|
||||
jrxml_placeholder.code(final_state.get("current_jrxml", ""), language="xml")
|
||||
else:
|
||||
error_msg = final_state.get("error_msg", "未知错误")
|
||||
explanation = final_state.get("natural_explanation", "")
|
||||
retries = final_state.get("retry_count", 0)
|
||||
st.session_state.messages.append({
|
||||
"role": "assistant",
|
||||
"content": final_state.get("current_jrxml", ""),
|
||||
"type": "jrxml",
|
||||
})
|
||||
st.session_state.messages.append({
|
||||
"role": "assistant",
|
||||
"content": f"❌ 经过 {retries} 次重试后仍无法生成有效的 JRXML。\n\n**错误:** {error_msg}\n\n**解释:** {explanation}\n\n请重新描述您的需求或简化报表结构。",
|
||||
"type": "error_explanation",
|
||||
})
|
||||
status_placeholder.error(f"❌ 经过 {retries} 次重试后验证失败")
|
||||
jrxml_placeholder.text("")
|
||||
else:
|
||||
st.error("未产生结果,请重试。")
|
||||
|
||||
return final_state
|
||||
|
||||
|
||||
# ---- 侧边栏 ----
|
||||
with st.sidebar:
|
||||
st.title("📊 JRXML 代理")
|
||||
st.markdown("通过自然语言生成 JasperReports 模板。")
|
||||
st.divider()
|
||||
|
||||
# 会话管理
|
||||
st.markdown("### 会话管理")
|
||||
|
||||
sessions = list_all_sessions()
|
||||
session_options = {}
|
||||
for s in sessions:
|
||||
sid = s["session_id"]
|
||||
name = s.get("session_name", sid)
|
||||
updated = s.get("updated_at", "")[:16]
|
||||
session_options[f"{name} ({updated})"] = sid
|
||||
|
||||
selected_label = None
|
||||
for label, sid in session_options.items():
|
||||
if sid == current_session_id:
|
||||
selected_label = label
|
||||
break
|
||||
|
||||
selected = st.selectbox(
|
||||
"切换会话",
|
||||
options=list(session_options.keys()),
|
||||
index=list(session_options.keys()).index(selected_label) if selected_label else 0,
|
||||
key="session_selector",
|
||||
)
|
||||
|
||||
if selected and session_options.get(selected) != current_session_id:
|
||||
new_sid = session_options[selected]
|
||||
data = load_session(new_sid)
|
||||
if data and data.get("agent_state"):
|
||||
st.session_state.agent_state = data["agent_state"]
|
||||
st.session_state.messages = []
|
||||
st.rerun()
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("➕ 新建", use_container_width=True):
|
||||
new_data = create_session(name="", agent_state=create_initial_state())
|
||||
st.session_state.agent_state = create_initial_state()
|
||||
st.session_state.agent_state["session_id"] = new_data["session_id"]
|
||||
st.session_state.agent_state["session_name"] = new_data["session_name"]
|
||||
st.session_state.agent_state["created_at"] = new_data["created_at"]
|
||||
st.session_state.messages = []
|
||||
st.rerun()
|
||||
with col2:
|
||||
if st.button("🗑 删除", use_container_width=True):
|
||||
if current_session_id:
|
||||
delete_session(current_session_id)
|
||||
st.session_state.agent_state = create_initial_state()
|
||||
new_data = create_session(name="", agent_state=st.session_state.agent_state)
|
||||
st.session_state.agent_state["session_id"] = new_data["session_id"]
|
||||
st.session_state.agent_state["session_name"] = new_data["session_name"]
|
||||
st.session_state.agent_state["created_at"] = new_data["created_at"]
|
||||
st.session_state.messages = []
|
||||
st.rerun()
|
||||
|
||||
current_name = st.session_state.agent_state.get("session_name", "")
|
||||
st.caption(f"当前: {current_name} (`{current_session_id}`)")
|
||||
|
||||
st.divider()
|
||||
st.markdown("### 快捷操作")
|
||||
|
||||
has_jrxml = bool(st.session_state.agent_state.get("current_jrxml", "").strip())
|
||||
has_history = bool(st.session_state.agent_state.get("history_states", []))
|
||||
|
||||
qcol1, qcol2 = st.columns(2)
|
||||
with qcol1:
|
||||
if st.button("👁 预览", use_container_width=True, disabled=not has_jrxml):
|
||||
with st.spinner("正在准备预览..."):
|
||||
run_agent("预览报表")
|
||||
st.rerun()
|
||||
with qcol2:
|
||||
if st.button("↩ 撤销", use_container_width=True, disabled=not has_history):
|
||||
with st.spinner("正在撤销..."):
|
||||
run_agent("撤销上一步修改")
|
||||
st.rerun()
|
||||
|
||||
if st.button("🔄 重置会话", use_container_width=True):
|
||||
with st.spinner("正在重置..."):
|
||||
run_agent("重新来,清空当前报表")
|
||||
st.rerun()
|
||||
|
||||
st.divider()
|
||||
st.markdown("### 配置")
|
||||
llm_backend = os.getenv("LLM_BACKEND", "cloud")
|
||||
llm_model = os.getenv("LLM_MODEL", os.getenv("LOCAL_LLM_MODEL", "gpt-4o"))
|
||||
st.caption(f"大语言模型: {llm_backend} / {llm_model}")
|
||||
st.caption(f"最大重试次数: {os.getenv('MAX_RETRY', '3')}")
|
||||
st.caption(f"验证服务: {os.getenv('VALIDATION_SERVICE_URL', 'http://localhost:8001/validate')}")
|
||||
|
||||
st.divider()
|
||||
st.markdown("### 下载")
|
||||
final = st.session_state.agent_state.get("final_jrxml", "")
|
||||
if final:
|
||||
st.download_button(
|
||||
label="📥 下载 JRXML",
|
||||
data=final,
|
||||
file_name="report.jrxml",
|
||||
mime="application/xml",
|
||||
use_container_width=True,
|
||||
)
|
||||
|
||||
# ---- 标题 ----
|
||||
st.title("📝 JRXML 报表生成器")
|
||||
st.caption("用自然语言描述您的报表需求,我将逐步生成可用的 JRXML 模板。")
|
||||
|
||||
# ---- 聊天历史 ----
|
||||
for msg in st.session_state.messages:
|
||||
with st.chat_message(msg["role"]):
|
||||
if msg["role"] == "assistant" and msg.get("type") == "jrxml":
|
||||
with st.expander("查看生成的 JRXML", expanded=False):
|
||||
st.code(msg["content"], language="xml")
|
||||
elif msg["role"] == "assistant" and msg.get("type") == "error_explanation":
|
||||
st.warning(msg["content"])
|
||||
elif msg["role"] == "assistant" and msg.get("type") == "success":
|
||||
st.success(msg["content"])
|
||||
elif msg["role"] == "assistant" and msg.get("type") == "consult":
|
||||
st.info(msg["content"])
|
||||
else:
|
||||
st.markdown(msg["content"])
|
||||
|
||||
# ---- 聊天输入 ----
|
||||
if prompt := st.chat_input("描述您的报表需求..."):
|
||||
st.session_state.messages.append({"role": "user", "content": prompt})
|
||||
with st.chat_message("user"):
|
||||
st.markdown(prompt)
|
||||
run_agent(prompt)
|
||||
st.rerun()
|
||||
@@ -0,0 +1,331 @@
|
||||
"""批注检测器:识别图片上的圈选(圆)和箭头,定位用户要修改的字段。
|
||||
|
||||
依赖 OpenCV (cv2),从 PaddleOCR 传递依赖已安装。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass
|
||||
class Annotation:
|
||||
"""单个批注标记。"""
|
||||
type: str # "circle" | "arrow"
|
||||
bbox: dict # {"x": int, "y": int, "w": int, "h": int}
|
||||
center: tuple[int, int] # (cx, cy)
|
||||
nearby_texts: list[str] = field(default_factory=list)
|
||||
from_text: str = "" # 箭头出发点的文本
|
||||
to_text: str = "" # 箭头指向的文本
|
||||
from_pt: Optional[tuple[int, int]] = None
|
||||
to_pt: Optional[tuple[int, int]] = None
|
||||
|
||||
|
||||
def detect_annotations(image_path: str, ocr_elements: list[dict]) -> dict:
|
||||
"""检测图片上的手写批注(圈选 + 箭头),并与 OCR 文本关联。
|
||||
|
||||
Args:
|
||||
image_path: 图片文件路径
|
||||
ocr_elements: OCR 元素列表 [{"text": str, "bbox": {x,y,w,h}, "confidence": float}]
|
||||
|
||||
Returns:
|
||||
{"circles": [...], "arrows": [...], "total": int}
|
||||
"""
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
return {"circles": [], "arrows": [], "total": 0, "error": "无法读取图片"}
|
||||
|
||||
h, w = img.shape[:2]
|
||||
|
||||
circles = _detect_circles(img)
|
||||
arrows = _detect_arrows(img)
|
||||
|
||||
all_annotations = circles + arrows
|
||||
_correlate_with_ocr(all_annotations, ocr_elements, w, h)
|
||||
|
||||
result: dict = {
|
||||
"circles": [_annotation_to_dict(a) for a in circles],
|
||||
"arrows": [_annotation_to_dict(a) for a in arrows],
|
||||
"total": len(all_annotations),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _annotation_to_dict(a: Annotation) -> dict:
|
||||
d = {
|
||||
"type": a.type,
|
||||
"bbox": a.bbox,
|
||||
"center": list(a.center),
|
||||
"nearby_texts": a.nearby_texts,
|
||||
}
|
||||
if a.type == "arrow":
|
||||
d["from_text"] = a.from_text
|
||||
d["to_text"] = a.to_text
|
||||
if a.from_pt:
|
||||
d["from_pt"] = list(a.from_pt)
|
||||
if a.to_pt:
|
||||
d["to_pt"] = list(a.to_pt)
|
||||
return d
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 圆圈检测
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _detect_circles(img: np.ndarray) -> list[Annotation]:
|
||||
"""检测图片中可能是手绘批注的圆圈。"""
|
||||
h, w = img.shape[:2]
|
||||
b, g, r = cv2.split(img)
|
||||
red_enhanced = cv2.addWeighted(r.astype(np.float32), 1.5,
|
||||
g.astype(np.float32), -0.3, 0)
|
||||
red_enhanced = cv2.addWeighted(red_enhanced, 1.2,
|
||||
b.astype(np.float32), -0.3, 0)
|
||||
red_enhanced = np.clip(red_enhanced, 0, 255).astype(np.uint8)
|
||||
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
combined = cv2.addWeighted(gray, 0.5, red_enhanced, 0.5, 0)
|
||||
blurred = cv2.GaussianBlur(combined, (9, 9), 2)
|
||||
|
||||
min_radius = max(15, min(w, h) // 40)
|
||||
max_radius = min(200, max(w, h) // 8)
|
||||
|
||||
circles_raw = cv2.HoughCircles(
|
||||
blurred, cv2.HOUGH_GRADIENT, dp=1.2, minDist=min_radius * 2,
|
||||
param1=50, param2=30, minRadius=min_radius, maxRadius=max_radius,
|
||||
)
|
||||
|
||||
annotations: list[Annotation] = []
|
||||
|
||||
if circles_raw is not None:
|
||||
for cx, cy, r in circles_raw[0]:
|
||||
bbox = {
|
||||
"x": max(0, int(cx - r)),
|
||||
"y": max(0, int(cy - r)),
|
||||
"w": int(r * 2),
|
||||
"h": int(r * 2),
|
||||
}
|
||||
annotations.append(Annotation(
|
||||
type="circle",
|
||||
bbox=bbox,
|
||||
center=(int(cx), int(cy)),
|
||||
))
|
||||
|
||||
return annotations
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 箭头检测
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _detect_arrows(img: np.ndarray) -> list[Annotation]:
|
||||
"""检测图片中的手绘箭头(直线段 + 端点三角形)。"""
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
|
||||
|
||||
lines = cv2.HoughLinesP(
|
||||
edges, rho=1, theta=np.pi / 180, threshold=40,
|
||||
minLineLength=30, maxLineGap=15,
|
||||
)
|
||||
|
||||
if lines is None:
|
||||
return []
|
||||
|
||||
segments = [(x1, y1, x2, y2) for x1, y1, x2, y2 in lines[:, 0]]
|
||||
clusters = _cluster_segments(segments)
|
||||
|
||||
annotations: list[Annotation] = []
|
||||
for segs in clusters:
|
||||
if len(segs) < 2:
|
||||
continue
|
||||
all_pts = []
|
||||
for x1, y1, x2, y2 in segs:
|
||||
all_pts.append((x1, y1))
|
||||
all_pts.append((x2, y2))
|
||||
all_pts_arr = np.array(all_pts)
|
||||
max_dist = 0
|
||||
p1 = p2 = all_pts[0]
|
||||
for i in range(len(all_pts)):
|
||||
for j in range(i + 1, len(all_pts)):
|
||||
d = (all_pts[i][0] - all_pts[j][0]) ** 2 + (all_pts[i][1] - all_pts[j][1]) ** 2
|
||||
if d > max_dist:
|
||||
max_dist = d
|
||||
p1, p2 = all_pts[i], all_pts[j]
|
||||
|
||||
from_pt, to_pt = _find_arrow_direction(edges, p1, p2)
|
||||
|
||||
x1, y1 = from_pt
|
||||
x2, y2 = to_pt
|
||||
bbox = {
|
||||
"x": min(x1, x2),
|
||||
"y": min(y1, y2),
|
||||
"w": abs(x2 - x1),
|
||||
"h": abs(y2 - y1),
|
||||
}
|
||||
cx = (x1 + x2) // 2
|
||||
cy = (y1 + y2) // 2
|
||||
|
||||
annotations.append(Annotation(
|
||||
type="arrow",
|
||||
bbox=bbox,
|
||||
center=(cx, cy),
|
||||
from_pt=from_pt,
|
||||
to_pt=to_pt,
|
||||
))
|
||||
|
||||
return annotations
|
||||
|
||||
|
||||
def _cluster_segments(segments: list[tuple]) -> list[list[tuple]]:
|
||||
"""将线段按方向和空间距离聚类。"""
|
||||
clusters: list[list[tuple]] = []
|
||||
used = [False] * len(segments)
|
||||
|
||||
for i, (x1, y1, x2, y2) in enumerate(segments):
|
||||
if used[i]:
|
||||
continue
|
||||
cluster = [(x1, y1, x2, y2)]
|
||||
used[i] = True
|
||||
angle_i = math.atan2(y2 - y1, x2 - x1)
|
||||
|
||||
for j in range(i + 1, len(segments)):
|
||||
if used[j]:
|
||||
continue
|
||||
x3, y3, x4, y4 = segments[j]
|
||||
angle_j = math.atan2(y4 - y3, x4 - x3)
|
||||
angle_diff = abs(angle_i - angle_j)
|
||||
if angle_diff > math.pi:
|
||||
angle_diff = 2 * math.pi - angle_diff
|
||||
|
||||
if angle_diff < 0.35:
|
||||
d1 = math.hypot(x3 - x2, y3 - y2)
|
||||
d2 = math.hypot(x1 - x4, y1 - y4)
|
||||
d3 = math.hypot(x3 - x1, y3 - y1)
|
||||
d4 = math.hypot(x4 - x2, y4 - y2)
|
||||
if min(d1, d2, d3, d4) < 80:
|
||||
cluster.append((x3, y3, x4, y4))
|
||||
used[j] = True
|
||||
|
||||
clusters.append(cluster)
|
||||
|
||||
return clusters
|
||||
|
||||
|
||||
def _find_arrow_direction(edges: np.ndarray, p1: tuple, p2: tuple) -> tuple[tuple, tuple]:
|
||||
"""判断箭头的方向(哪端是箭头/三角形汇聚点)。"""
|
||||
r = 20
|
||||
h, w = edges.shape[:2]
|
||||
|
||||
def edge_density(cx, cy):
|
||||
x1 = max(0, int(cx - r))
|
||||
y1 = max(0, int(cy - r))
|
||||
x2 = min(w, int(cx + r))
|
||||
y2 = min(h, int(cy + r))
|
||||
roi = edges[y1:y2, x1:x2]
|
||||
if roi.size == 0:
|
||||
return 0
|
||||
return float(np.count_nonzero(roi)) / roi.size
|
||||
|
||||
d1 = edge_density(p1[0], p1[1])
|
||||
d2 = edge_density(p2[0], p2[1])
|
||||
|
||||
if d1 > d2 * 1.3:
|
||||
return p2, p1
|
||||
if d2 > d1 * 1.3:
|
||||
return p1, p2
|
||||
return p1, p2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OCR 关联
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _correlate_with_ocr(
|
||||
annotations: list[Annotation],
|
||||
ocr_elements: list[dict],
|
||||
img_w: int,
|
||||
img_h: int,
|
||||
) -> None:
|
||||
"""将批注与附近的 OCR 文本关联。"""
|
||||
if not ocr_elements:
|
||||
return
|
||||
|
||||
for ann in annotations:
|
||||
ax = ann.center[0]
|
||||
ay = ann.center[1]
|
||||
|
||||
near_texts: list[tuple[str, float]] = []
|
||||
|
||||
for elem in ocr_elements:
|
||||
bbox = elem.get("bbox", {})
|
||||
ex = bbox.get("x", 0) + bbox.get("w", 0) / 2
|
||||
ey = bbox.get("y", 0) + bbox.get("h", 0) / 2
|
||||
dist = math.hypot(ax - ex, ay - ey)
|
||||
max_dist = max(img_w, img_h) * 0.15
|
||||
if dist < max_dist:
|
||||
near_texts.append((elem.get("text", ""), dist))
|
||||
|
||||
near_texts.sort(key=lambda x: x[1])
|
||||
ann.nearby_texts = [t for t, _ in near_texts[:5]]
|
||||
|
||||
if ann.type == "arrow" and ann.from_pt and ann.to_pt:
|
||||
ann.from_text = _closest_text(ann.from_pt, ocr_elements, img_w, img_h)
|
||||
ann.to_text = _closest_text(ann.to_pt, ocr_elements, img_w, img_h)
|
||||
|
||||
|
||||
def _closest_text(pt: tuple[int, int], ocr_elements: list[dict], img_w: int, img_h: int) -> str:
|
||||
"""找到离 pt 最近的 OCR 文本。"""
|
||||
best_text = ""
|
||||
best_dist = max(img_w, img_h) * 0.12
|
||||
for elem in ocr_elements:
|
||||
bbox = elem.get("bbox", {})
|
||||
ex = bbox.get("x", 0) + bbox.get("w", 0) / 2
|
||||
ey = bbox.get("y", 0) + bbox.get("h", 0) / 2
|
||||
dist = math.hypot(pt[0] - ex, pt[1] - ey)
|
||||
if dist < best_dist:
|
||||
best_dist = dist
|
||||
best_text = elem.get("text", "")
|
||||
return best_text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LLM 上下文格式化
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def format_annotation_context(annotation_result: dict) -> str:
|
||||
"""将批注检测结果格式化为中文 LLM 提示文本。"""
|
||||
if not annotation_result or not isinstance(annotation_result, dict):
|
||||
return ""
|
||||
|
||||
circles = annotation_result.get("circles", [])
|
||||
arrows = annotation_result.get("arrows", [])
|
||||
total = annotation_result.get("total", len(circles) + len(arrows))
|
||||
|
||||
if total == 0:
|
||||
return ""
|
||||
|
||||
parts = ["[图片批注检测结果]"]
|
||||
|
||||
if circles:
|
||||
parts.append(f"\n检测到 {len(circles)} 个圈选标记:")
|
||||
for i, c in enumerate(circles):
|
||||
center = c.get("center", [0, 0])
|
||||
near = c.get("nearby_texts", [])
|
||||
parts.append(
|
||||
f" 圈{i+1}. 位置 ({center[0]},{center[1]})"
|
||||
f" — 圈选内容: {', '.join(near) if near else '(附近无文字)'}"
|
||||
)
|
||||
|
||||
if arrows:
|
||||
parts.append(f"\n检测到 {len(arrows)} 个箭头标记:")
|
||||
for i, a in enumerate(arrows):
|
||||
ft = a.get("from_text", "")
|
||||
tt = a.get("to_text", "")
|
||||
parts.append(f" 箭头{i+1}. 从「{ft}」→ 指向「{tt}」")
|
||||
|
||||
parts.append("\n请根据上述圈选/箭头定位用户要修改的报表字段。")
|
||||
return "\n".join(parts)
|
||||
+24
-2
@@ -1,4 +1,9 @@
|
||||
"""嵌入模型工厂:支持本地 sentence-transformers 和云端 API。"""
|
||||
"""嵌入模型工厂:支持本地 Sentence-Transformers 和云端 API。
|
||||
|
||||
调用方式:
|
||||
get_embeddings() → LangChain 兼容的 embeddings 对象
|
||||
get_st_model() → 原始 SentenceTransformer 实例
|
||||
"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
@@ -7,6 +12,7 @@ load_dotenv()
|
||||
|
||||
|
||||
def get_embeddings():
|
||||
"""返回 LangChain 兼容的 embeddings 对象(用于 langchain_chroma 等)。"""
|
||||
backend = os.getenv("EMBED_BACKEND", "local")
|
||||
if backend == "cloud":
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
@@ -22,5 +28,21 @@ def get_embeddings():
|
||||
except ImportError:
|
||||
from langchain_community.embeddings import HuggingFaceEmbeddings
|
||||
|
||||
model = os.getenv("LOCAL_EMBED_MODEL", "Qwen/Qwen3-Embedding-0.6B")
|
||||
model = os.getenv("RAG_EMBED_MODEL", os.getenv("LOCAL_EMBED_MODEL", "Qwen/Qwen3-Embedding-0.6B"))
|
||||
return HuggingFaceEmbeddings(model_name=model)
|
||||
|
||||
|
||||
def get_st_model():
|
||||
"""返回原始 SentenceTransformer 实例(与 rag_jrxml 子模块使用方式一致)。"""
|
||||
import torch
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
model_name = os.getenv("RAG_EMBED_MODEL", os.getenv("LOCAL_EMBED_MODEL", "Qwen/Qwen3-Embedding-0.6B"))
|
||||
use_gpu = os.getenv("RAG_USE_GPU", "true").lower() in ("true", "1")
|
||||
use_fp16 = os.getenv("RAG_USE_FP16", "true").lower() in ("true", "1")
|
||||
|
||||
device = "cuda" if (use_gpu and torch.cuda.is_available()) else "cpu"
|
||||
model = SentenceTransformer(model_name, device=device)
|
||||
if device == "cuda" and use_fp16:
|
||||
model = model.half()
|
||||
return model
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
"""错误自增长知识库 — 记录修正成功的错误案例,用于未来参考。
|
||||
|
||||
原则:
|
||||
- 仅记录"新错误"(指纹去重)
|
||||
- 必须包含完整的修正方案(prompt、工具链、前后 JRXML)
|
||||
- 存储于 ChromaDB,可被检索注入到生成 prompt 中
|
||||
|
||||
用法:
|
||||
from backend.error_kb import ErrorKB
|
||||
kb = ErrorKB()
|
||||
kb.record(error_msg, bad_jrxml, good_jrxml, correction_prompt)
|
||||
cases = kb.search("字段未声明", k=3)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
CHROMA_DIR = Path(os.getenv("CHROMA_PERSIST_DIR", "./db/chroma"))
|
||||
COLLECTION_NAME = "jrxml_error_cases"
|
||||
|
||||
|
||||
def _make_fingerprint(error_msg: str) -> str:
|
||||
"""生成错误指纹 — 标准化后取 hash,用于去重。
|
||||
|
||||
标准化规则:
|
||||
- 去除字段名、变量名等具体标识符(替换为占位符)
|
||||
- 小写化
|
||||
- 只保留错误的结构性特征
|
||||
"""
|
||||
text = error_msg.lower()
|
||||
# 替换变量名 / 字段名($F{xxx}, "name", 'value' 等)
|
||||
text = re.sub(r'\$f\{[^}]+\}', '$f{<FIELD>}', text)
|
||||
text = re.sub(r"'[^']*'", "'<VALUE>'", text)
|
||||
text = re.sub(r'"[^"]*"', '"<VALUE>"', text)
|
||||
# 替换数字
|
||||
text = re.sub(r'\b\d+\b', '<NUM>', text)
|
||||
# 压缩空白
|
||||
text = re.sub(r'\s+', ' ', text).strip()
|
||||
return hashlib.md5(text.encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
class ErrorKB:
|
||||
"""错误案例知识库 — 包装 ChromaDB 持久化。"""
|
||||
|
||||
def __init__(self):
|
||||
self._client = None
|
||||
self._collection = None
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if self._client is None:
|
||||
import chromadb
|
||||
self._client = chromadb.PersistentClient(path=str(CHROMA_DIR))
|
||||
return self._client
|
||||
|
||||
@property
|
||||
def collection(self):
|
||||
if self._collection is None:
|
||||
try:
|
||||
self._collection = self.client.get_collection(COLLECTION_NAME)
|
||||
except Exception:
|
||||
self._collection = self.client.create_collection(COLLECTION_NAME)
|
||||
return self._collection
|
||||
|
||||
def exists(self, error_msg: str) -> bool:
|
||||
"""检查错误是否已存在于知识库中(按指纹去重)。"""
|
||||
fp = _make_fingerprint(error_msg)
|
||||
try:
|
||||
results = self.collection.get(ids=[fp])
|
||||
return bool(results and results["ids"])
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def record(
|
||||
self,
|
||||
error_msg: str,
|
||||
bad_jrxml: str,
|
||||
good_jrxml: str,
|
||||
correction_prompt: str,
|
||||
model: str = "",
|
||||
retry_count: int = 0,
|
||||
) -> bool:
|
||||
"""记录一个成功修正的错误案例。
|
||||
|
||||
仅当指纹不重复时写入。返回 True 表示已记录,False 表示重复。
|
||||
"""
|
||||
if self.exists(error_msg):
|
||||
return False
|
||||
|
||||
fp = _make_fingerprint(error_msg)
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# 内容:结构化记录
|
||||
doc = json.dumps({
|
||||
"error": error_msg,
|
||||
"bad_jrxml_snippet": bad_jrxml[:2000],
|
||||
"good_jrxml_snippet": good_jrxml[:2000],
|
||||
"correction_prompt": correction_prompt[:1500],
|
||||
"model": model,
|
||||
"retry_count": retry_count,
|
||||
"recorded_at": now,
|
||||
"tools": ["validation_service", "llm_correction"],
|
||||
}, ensure_ascii=False)
|
||||
|
||||
# 元数据:用于检索过滤
|
||||
error_keywords = _extract_keywords(error_msg)
|
||||
metadata = {
|
||||
"fingerprint": fp,
|
||||
"error_keywords": ", ".join(error_keywords[:5]),
|
||||
"recorded_at": now,
|
||||
"retry_success": retry_count + 1, # 第几次修正成功的
|
||||
}
|
||||
|
||||
self.collection.add(
|
||||
ids=[fp],
|
||||
documents=[doc],
|
||||
metadatas=[metadata],
|
||||
)
|
||||
return True
|
||||
|
||||
def search(self, error_msg: str, k: int = 3) -> list[dict]:
|
||||
"""根据错误消息搜索相似的修正案例(ChromaDB 语义搜索)。
|
||||
|
||||
返回 [{error, fix_snippet, prompt, ...}, ...]
|
||||
"""
|
||||
keywords = _extract_keywords(error_msg)
|
||||
if not keywords:
|
||||
return []
|
||||
|
||||
query_text = " ".join(keywords)
|
||||
try:
|
||||
results = self.collection.query(
|
||||
query_texts=[query_text],
|
||||
n_results=k,
|
||||
include=["documents", "metadatas", "distances"],
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
output = []
|
||||
if not results["ids"] or not results["ids"][0]:
|
||||
return output
|
||||
|
||||
for i, doc_id in enumerate(results["ids"][0]):
|
||||
dist = results["distances"][0][i]
|
||||
try:
|
||||
data = json.loads(results["documents"][0][i])
|
||||
output.append({
|
||||
"id": doc_id,
|
||||
"error": data.get("error", ""),
|
||||
"fix_snippet": data.get("good_jrxml_snippet", ""),
|
||||
"prompt": data.get("correction_prompt", ""),
|
||||
"recorded_at": data.get("recorded_at", ""),
|
||||
"distance": dist,
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return output
|
||||
|
||||
def search_as_context(self, error_msg: str, k: int = 3) -> str:
|
||||
"""搜索并返回拼接好的错误案例上下文,可直接注入 LLM prompt。"""
|
||||
results = self.search(error_msg, k=k)
|
||||
if not results:
|
||||
return ""
|
||||
|
||||
parts = []
|
||||
for r in results:
|
||||
parts.append(
|
||||
f"[历史错误案例]\n"
|
||||
f"错误: {r['error'][:200]}\n"
|
||||
f"修正后 JRXML 片段:\n{r['fix_snippet'][:800]}\n"
|
||||
)
|
||||
return "\n---\n".join(parts)
|
||||
|
||||
def stats(self) -> dict:
|
||||
"""返回知识库统计信息。"""
|
||||
try:
|
||||
count = self.collection.count()
|
||||
return {"total_cases": count, "collection": COLLECTION_NAME}
|
||||
except Exception:
|
||||
return {"total_cases": 0, "collection": COLLECTION_NAME}
|
||||
|
||||
|
||||
def _extract_keywords(error_msg: str) -> list[str]:
|
||||
"""从错误消息中提取关键词(中文 + 英文 token)。"""
|
||||
# 中文字符作为独立关键词
|
||||
chinese = re.findall(r'[一-鿿]{2,}', error_msg)
|
||||
# 英文 camelCase / snake_case token
|
||||
english = re.findall(r'[a-zA-Z_][a-zA-Z0-9_]{2,}', error_msg)
|
||||
# JRXML 特有模式
|
||||
jrxml_patterns = re.findall(r'\$F\{[^}]*\}', error_msg)
|
||||
return chinese + english + jrxml_patterns
|
||||
|
||||
|
||||
# 全局单例
|
||||
_kb: Optional[ErrorKB] = None
|
||||
|
||||
|
||||
def get_error_kb() -> ErrorKB:
|
||||
global _kb
|
||||
if _kb is None:
|
||||
_kb = ErrorKB()
|
||||
return _kb
|
||||
|
||||
|
||||
def record_error(error_msg: str, bad_jrxml: str, good_jrxml: str,
|
||||
correction_prompt: str, model: str = "", retry_count: int = 0) -> bool:
|
||||
"""便捷函数:记录成功修正的错误案例。"""
|
||||
return get_error_kb().record(error_msg, bad_jrxml, good_jrxml,
|
||||
correction_prompt, model, retry_count)
|
||||
|
||||
|
||||
def search_error_cases(error_msg: str, k: int = 3) -> str:
|
||||
"""便捷函数:搜索历史错误案例并返回上下文字符串。"""
|
||||
return get_error_kb().search_as_context(error_msg, k=k)
|
||||
@@ -0,0 +1,136 @@
|
||||
"""OCR 字段 → KB 字段匹配模块。
|
||||
|
||||
两阶段匹配:
|
||||
1. Embedding 粗筛(相似度 top-3)
|
||||
2. LLM 精确确认
|
||||
|
||||
返回映射: {"工单号": "billNo", "客户名称": "customerName", ...}
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from backend.logger import get_logger
|
||||
|
||||
load_dotenv()
|
||||
|
||||
_match_log = get_logger("field_matcher")
|
||||
|
||||
|
||||
def _embed(text: str) -> list:
|
||||
"""获取文本的向量嵌入。"""
|
||||
from backend.rag_adapter import _get_searcher
|
||||
searcher = _get_searcher()
|
||||
if searcher._model is None:
|
||||
_ = searcher.model
|
||||
emb = searcher.model.encode(text, normalize_embeddings=True, show_progress_bar=False)
|
||||
return emb.tolist()
|
||||
|
||||
|
||||
def _cosine_similarity(a: list, b: list) -> float:
|
||||
"""余弦相似度(假设向量已归一化,点积即相似度)。"""
|
||||
return sum(x * y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def match_ocr_to_kb(ocr_fields: list[str], kb_fields: list[dict],
|
||||
llm=None) -> dict[str, str]:
|
||||
"""将 OCR 提取的字段名匹配到 KB 字段定义。
|
||||
|
||||
Args:
|
||||
ocr_fields: OCR 提取的中文字段名列表
|
||||
kb_fields: KB 字段定义 [{"name": "billNo", "description": "工单号", ...}]
|
||||
llm: 可选的 LLM 实例,用于精确确认
|
||||
|
||||
Returns:
|
||||
{"工单号": "billNo", "客户": "customerName", ...}
|
||||
"""
|
||||
if not ocr_fields or not kb_fields:
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
|
||||
# 阶段 1: Embedding 粗筛
|
||||
try:
|
||||
ocr_embs = {f: _embed(f) for f in ocr_fields}
|
||||
kb_embs = {f["name"]: _embed(f.get("description", f["name"])) for f in kb_fields}
|
||||
except Exception as e:
|
||||
_match_log.warning("Embedding 匹配失败,回退到 LLM: %s", e)
|
||||
return _match_via_llm(ocr_fields, kb_fields, llm)
|
||||
|
||||
candidates = {}
|
||||
for ocr_name, ocr_emb in ocr_embs.items():
|
||||
scored = []
|
||||
for kb_name, kb_emb in kb_embs.items():
|
||||
sim = _cosine_similarity(ocr_emb, kb_emb)
|
||||
scored.append((kb_name, sim))
|
||||
scored.sort(key=lambda x: x[1], reverse=True)
|
||||
candidates[ocr_name] = scored[:3]
|
||||
|
||||
# 阶段 2: LLM 精确确认
|
||||
if llm:
|
||||
confirmed = _match_via_llm(ocr_fields, kb_fields, llm, candidates)
|
||||
result.update(confirmed)
|
||||
else:
|
||||
for ocr_name, cands in candidates.items():
|
||||
if cands and cands[0][1] > 0.5:
|
||||
result[ocr_name] = cands[0][0]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _match_via_llm(ocr_fields: list[str], kb_fields: list[dict],
|
||||
llm, candidates: Optional[dict] = None) -> dict[str, str]:
|
||||
"""使用 LLM 精确确认字段映射。"""
|
||||
kb_desc = "\n".join(
|
||||
f"- {f['name']}: {f.get('description', '')} ({f.get('type', 'java.lang.String')})"
|
||||
for f in kb_fields
|
||||
)
|
||||
|
||||
candidates_hint = ""
|
||||
if candidates:
|
||||
cand_lines = []
|
||||
for ocr_name, cands in candidates.items():
|
||||
cand_str = ", ".join(f"{n}({s:.2f})" for n, s in cands)
|
||||
cand_lines.append(f" {ocr_name} -> 候选: {cand_str}")
|
||||
candidates_hint = (
|
||||
"向量相似度候选(仅供参考,请根据语义确认):\n"
|
||||
+ "\n".join(cand_lines)
|
||||
)
|
||||
|
||||
prompt = (
|
||||
"请将以下 OCR 识别的字段名匹配到知识库定义的字段。\n\n"
|
||||
f"OCR 字段: {json.dumps(ocr_fields, ensure_ascii=False)}\n\n"
|
||||
f"知识库字段:\n{kb_desc}\n\n"
|
||||
f"{candidates_hint}\n\n"
|
||||
"请以 JSON 对象格式输出映射关系,键为 OCR 字段名,值为 KB 字段名:\n"
|
||||
'{"工单号": "billNo", "客户名称": "customerName"}'
|
||||
)
|
||||
|
||||
try:
|
||||
response = llm.invoke(prompt)
|
||||
content = response.content if hasattr(response, "content") else str(response)
|
||||
start = content.find("{")
|
||||
end = content.rfind("}") + 1
|
||||
if start >= 0 and end > start:
|
||||
return json.loads(content[start:end])
|
||||
except Exception as e:
|
||||
_match_log.warning("LLM 字段匹配失败: %s", e)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def format_field_mapping_context(mapping: dict[str, str]) -> str:
|
||||
"""将字段映射格式化为 prompt 上下文字符串。"""
|
||||
if not mapping:
|
||||
return ""
|
||||
|
||||
lines = ["[字段映射 — OCR -> KB]",
|
||||
"请在 JRXML 中使用以下参数名:",
|
||||
"| OCR 字段 | JRXML 参数 |",
|
||||
"|---|---|"]
|
||||
for ocr_name, kb_name in mapping.items():
|
||||
lines.append(f"| {ocr_name} | $P{{{kb_name}}} |")
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,303 @@
|
||||
"""文件解析器:将上传文件转为文本,供 LLM 处理。
|
||||
|
||||
支持:
|
||||
- 图片 (.png/.jpg/.jpeg/.bmp) → OCR 提取文本
|
||||
- PDF (.pdf) → 文本提取
|
||||
- Word (.docx) → 文本提取
|
||||
- 纯文本 (.txt/.csv/.json/.xml) → 直接读取
|
||||
|
||||
策略选择:
|
||||
- 原生多模态: 模型支持图片时直接传文件(当前 MiniMax 不支持,自动退回文本转换)
|
||||
- 文本转换: 所有文件转为 UTF-8 文本后注入 prompt
|
||||
"""
|
||||
|
||||
import os
|
||||
import io
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import PIL.Image
|
||||
|
||||
MODELS_WITH_VISION = {
|
||||
"gpt-4o", "gpt-4-turbo", "gpt-4-vision-preview",
|
||||
"claude-3", "claude-3.5", "claude-4",
|
||||
"gemini-1.5", "gemini-2",
|
||||
}
|
||||
|
||||
|
||||
def can_use_vision(model: str = "") -> bool:
|
||||
"""检查当前模型是否支持原生多模态(图片直接上传)。"""
|
||||
if not model:
|
||||
model = os.getenv("LLM_MODEL", "")
|
||||
return any(v in model.lower() for v in MODELS_WITH_VISION)
|
||||
|
||||
|
||||
def parse_file(file_path: str, file_type: str = "") -> dict:
|
||||
"""解析任意文件为文本。
|
||||
|
||||
返回: {"text": str, "file_type": str, "method": str, "error": Optional[str]}
|
||||
"""
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
return {"text": "", "file_type": file_type, "method": "none", "error": "文件不存在"}
|
||||
|
||||
suffix = path.suffix.lower()
|
||||
if file_type:
|
||||
suffix = file_type if file_type.startswith(".") else f".{file_type}"
|
||||
|
||||
parsers = {
|
||||
".png": _parse_image,
|
||||
".jpg": _parse_image,
|
||||
".jpeg": _parse_image,
|
||||
".bmp": _parse_image,
|
||||
".webp": _parse_image,
|
||||
".pdf": _parse_pdf,
|
||||
".docx": _parse_docx,
|
||||
".xlsx": _parse_xlsx,
|
||||
".xls": _parse_xls,
|
||||
".doc": _parse_doc,
|
||||
}
|
||||
|
||||
parser = parsers.get(suffix)
|
||||
if parser:
|
||||
return parser(path)
|
||||
else:
|
||||
return _parse_text(path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 各类型解析器
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _parse_image(path: Path) -> dict:
|
||||
"""OCR 提取图片中的文字。优先 EasyOCR,回退 PaddleOCR。"""
|
||||
try:
|
||||
img = PIL.Image.open(path)
|
||||
info = f"[图片: {img.size[0]}x{img.size[1]}, {img.mode}]"
|
||||
except Exception:
|
||||
info = "[图片: 无法读取元数据]"
|
||||
|
||||
# 优先 PaddleOCR(精确识别)
|
||||
try:
|
||||
from paddleocr import PaddleOCR
|
||||
ocr = PaddleOCR(lang="ch")
|
||||
result = ocr.ocr(str(path))
|
||||
lines = []
|
||||
if result and result[0]:
|
||||
for line in result[0]:
|
||||
text = line[1][0] if len(line) > 1 else ""
|
||||
if text.strip():
|
||||
lines.append(text.strip())
|
||||
if lines:
|
||||
return {
|
||||
"text": f"{info}\n识别文本:\n" + "\n".join(lines),
|
||||
"file_type": "image",
|
||||
"method": "paddleocr",
|
||||
"error": None,
|
||||
}
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 回退 EasyOCR
|
||||
try:
|
||||
import easyocr
|
||||
import numpy as np
|
||||
reader = easyocr.Reader(["ch_sim", "en"], gpu=False, verbose=False)
|
||||
result = reader.readtext(np.array(img))
|
||||
lines = [text.strip() for (_, text, _) in result if text.strip()]
|
||||
if lines:
|
||||
return {
|
||||
"text": f"{info}\n识别文本:\n" + "\n".join(lines),
|
||||
"file_type": "image",
|
||||
"method": "easyocr",
|
||||
"error": None,
|
||||
}
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# OCR 不可用 → 返回图片元信息 + 安装提示
|
||||
return {
|
||||
"text": f"{info}\n(如需 OCR 文字识别,请安装: pip install easyocr)",
|
||||
"file_type": "image",
|
||||
"method": "metadata_only",
|
||||
"error": "OCR 引擎未安装,已返回图片元信息",
|
||||
}
|
||||
|
||||
|
||||
def _parse_pdf(path: Path) -> dict:
|
||||
"""提取 PDF 中的文本。"""
|
||||
try:
|
||||
import pdfplumber
|
||||
with pdfplumber.open(path) as pdf:
|
||||
pages = []
|
||||
for page in pdf.pages:
|
||||
text = page.extract_text()
|
||||
if text:
|
||||
pages.append(text)
|
||||
full = "\n\n".join(pages)
|
||||
return {
|
||||
"text": full,
|
||||
"file_type": "pdf",
|
||||
"method": "pdfplumber",
|
||||
"error": None,
|
||||
}
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# Fallback: 尝试 PyMuPDF
|
||||
try:
|
||||
import fitz
|
||||
doc = fitz.open(path)
|
||||
pages = []
|
||||
for page in doc:
|
||||
pages.append(page.get_text())
|
||||
doc.close()
|
||||
return {
|
||||
"text": "\n\n".join(pages),
|
||||
"file_type": "pdf",
|
||||
"method": "pymupdf",
|
||||
"error": None,
|
||||
}
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"text": "", "file_type": "pdf", "method": "none",
|
||||
"error": "PDF 解析需要安装 pdfplumber 或 PyMuPDF"}
|
||||
|
||||
|
||||
def _parse_docx(path: Path) -> dict:
|
||||
"""提取 Word 文档中的文本。"""
|
||||
try:
|
||||
from docx import Document
|
||||
doc = Document(path)
|
||||
paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
|
||||
# 同时提取表格内容
|
||||
for table in doc.tables:
|
||||
for row in table.rows:
|
||||
cells = [cell.text for cell in row.cells if cell.text.strip()]
|
||||
if cells:
|
||||
paragraphs.append(" | ".join(cells))
|
||||
return {
|
||||
"text": "\n\n".join(paragraphs),
|
||||
"file_type": "docx",
|
||||
"method": "python-docx",
|
||||
"error": None,
|
||||
}
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return {"text": "", "file_type": "docx", "method": "none",
|
||||
"error": "DOCX 解析需要安装 python-docx"}
|
||||
|
||||
|
||||
def _parse_xlsx(path: Path) -> dict:
|
||||
"""提取 Excel .xlsx 文件中的文本。"""
|
||||
try:
|
||||
from openpyxl import load_workbook
|
||||
wb = load_workbook(path, read_only=True, data_only=True)
|
||||
parts = []
|
||||
for name in wb.sheetnames:
|
||||
ws = wb[name]
|
||||
rows = []
|
||||
for row in ws.iter_rows(values_only=True):
|
||||
cells = [str(c) if c is not None else "" for c in row]
|
||||
if any(c for c in cells):
|
||||
rows.append("\t".join(cells))
|
||||
if rows:
|
||||
parts.append(f"[Sheet: {name}]\n" + "\n".join(rows))
|
||||
wb.close()
|
||||
text = "\n\n".join(parts)
|
||||
return {"text": text, "file_type": "xlsx", "method": "openpyxl", "error": None}
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as e:
|
||||
return {"text": "", "file_type": "xlsx", "method": "none",
|
||||
"error": f"XLSX 解析失败: {e}"}
|
||||
return {"text": "", "file_type": "xlsx", "method": "none",
|
||||
"error": "XLSX 解析需要安装 openpyxl"}
|
||||
|
||||
|
||||
def _parse_xls(path: Path) -> dict:
|
||||
"""提取旧版 Excel .xls 文件中的文本。"""
|
||||
try:
|
||||
import xlrd
|
||||
wb = xlrd.open_workbook(path)
|
||||
parts = []
|
||||
for name in wb.sheet_names():
|
||||
ws = wb.sheet_by_name(name)
|
||||
rows = []
|
||||
for rx in range(ws.nrows):
|
||||
cells = [str(ws.cell_value(rx, cx)) if ws.cell_value(rx, cx) != "" else ""
|
||||
for cx in range(ws.ncols)]
|
||||
if any(c for c in cells):
|
||||
rows.append("\t".join(cells))
|
||||
if rows:
|
||||
parts.append(f"[Sheet: {name}]\n" + "\n".join(rows))
|
||||
text = "\n\n".join(parts)
|
||||
return {"text": text, "file_type": "xls", "method": "xlrd", "error": None}
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as e:
|
||||
return {"text": "", "file_type": "xls", "method": "none",
|
||||
"error": f"XLS 解析失败: {e}"}
|
||||
return {"text": "", "file_type": "xls", "method": "none",
|
||||
"error": "XLS 解析需要安装 xlrd"}
|
||||
|
||||
|
||||
def _parse_doc(path: Path) -> dict:
|
||||
"""提取旧版 Word .doc 文件中的文本(尽力而为,二进制格式)。"""
|
||||
try:
|
||||
import olefile
|
||||
ole = olefile.OleFileIO(path)
|
||||
if not ole.exists("WordDocument"):
|
||||
ole.close()
|
||||
return {"text": "", "file_type": "doc", "method": "none",
|
||||
"error": "不是有效的 .doc 文件"}
|
||||
raw = ole.openstream("WordDocument").read()
|
||||
ole.close()
|
||||
# 提取可打印 UTF-16LE 字符段
|
||||
text = ""
|
||||
try:
|
||||
decoded = raw.decode("utf-16-le", errors="ignore")
|
||||
text = "".join(c for c in decoded if c.isprintable() or c in "\n\r\t")
|
||||
except Exception:
|
||||
pass
|
||||
if not text.strip():
|
||||
return {"text": "", "file_type": "doc", "method": "olefile",
|
||||
"error": "无法提取文本(.doc 为二进制格式,建议转换为 .docx)"}
|
||||
return {"text": text.strip(), "file_type": "doc", "method": "olefile", "error": None}
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as e:
|
||||
return {"text": "", "file_type": "doc", "method": "none",
|
||||
"error": f"DOC 解析失败: {e}"}
|
||||
return {"text": "", "file_type": "doc", "method": "none",
|
||||
"error": "DOC 解析需要安装 olefile"}
|
||||
|
||||
|
||||
|
||||
def _parse_text(path: Path) -> dict:
|
||||
"""读取纯文本文件。"""
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
return {"text": text, "file_type": path.suffix, "method": "direct", "error": None}
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
text = path.read_text(encoding="gbk")
|
||||
return {"text": text, "file_type": path.suffix, "method": "direct_gbk", "error": None}
|
||||
except Exception:
|
||||
return {"text": "", "file_type": path.suffix, "method": "none",
|
||||
"error": "无法解码文件"}
|
||||
except Exception:
|
||||
return {"text": "", "file_type": path.suffix, "method": "none",
|
||||
"error": "读取失败"}
|
||||
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
JRXML 元素自动排序 — 按 JasperReports XSD <xs:sequence> 要求重排子元素。
|
||||
|
||||
XSD 要求 jasperReport 子元素严格按以下顺序:
|
||||
property, propertyExpression, import, template, reportFont,
|
||||
style, subDataset, scriptlet, parameter, queryString, field,
|
||||
sortField, variable, filterExpression, group, background, title,
|
||||
pageHeader, columnHeader, detail, columnFooter, pageFooter,
|
||||
lastPageFooter, summary, noData
|
||||
|
||||
以及 band 内部的 reportElement 必须在其他元素之前。
|
||||
"""
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Optional
|
||||
|
||||
# JasperReports XSD sequence 顺序(索引越小越靠前)
|
||||
JASPERREPORT_ORDER = {
|
||||
"property": 0,
|
||||
"propertyExpression": 1,
|
||||
"import": 2,
|
||||
"template": 3,
|
||||
"reportFont": 4,
|
||||
"style": 5,
|
||||
"subDataset": 6,
|
||||
"scriptlet": 7,
|
||||
"parameter": 8,
|
||||
"queryString": 9,
|
||||
"field": 10,
|
||||
"sortField": 11,
|
||||
"variable": 12,
|
||||
"filterExpression": 13,
|
||||
"group": 14,
|
||||
"background": 15,
|
||||
"title": 16,
|
||||
"pageHeader": 17,
|
||||
"columnHeader": 18,
|
||||
"detail": 19,
|
||||
"columnFooter": 20,
|
||||
"pageFooter": 21,
|
||||
"lastPageFooter": 22,
|
||||
"summary": 23,
|
||||
"noData": 24,
|
||||
}
|
||||
|
||||
# 带命名空间的标签映射(去掉 ns 前缀后匹配)
|
||||
NS = "http://jasperreports.sourceforge.net/jasperreports"
|
||||
|
||||
|
||||
def _tag_local(tag: str) -> str:
|
||||
"""提取标签本地名(去掉命名空间前缀)。"""
|
||||
return tag.split("}")[-1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _sort_key(elem: ET.Element) -> int:
|
||||
"""排序键:按 JASPERREPORT_ORDER 中的顺序,未知元素放最后。"""
|
||||
local = _tag_local(elem.tag)
|
||||
return JASPERREPORT_ORDER.get(local, 999)
|
||||
|
||||
|
||||
def reorder_jrxml_elements(xml_string: str) -> str:
|
||||
"""重排 JRXML 字符串中的子元素顺序,使其符合 XSD sequence 要求。
|
||||
|
||||
处理范围:
|
||||
- jasperReport 的直接子元素
|
||||
- band 的直接子元素(reportElement 在前)
|
||||
|
||||
返回重排后的 XML 字符串。如果解析失败,返回原始字符串。
|
||||
"""
|
||||
try:
|
||||
root = ET.fromstring(xml_string)
|
||||
except ET.ParseError:
|
||||
return xml_string # 无法解析,返回原始
|
||||
|
||||
_reorder_children(root)
|
||||
_reorder_bands(root)
|
||||
|
||||
# 序列化回字符串
|
||||
result = ET.tostring(root, encoding="unicode")
|
||||
|
||||
# 恢复 XML 声明、CDATA、命名空间
|
||||
result = _restore_formatting(xml_string, result)
|
||||
return result
|
||||
|
||||
|
||||
def _reorder_children(parent: ET.Element):
|
||||
"""递归重排所有子元素。"""
|
||||
children = list(parent)
|
||||
if not children:
|
||||
return
|
||||
|
||||
# 按 XSD 顺序排序
|
||||
children.sort(key=_sort_key)
|
||||
|
||||
# 重建子元素列表
|
||||
for i, child in enumerate(children):
|
||||
# ET 不支持直接 reorder,用 remove + insert
|
||||
pass
|
||||
|
||||
# 实际上 ElementTree 不支持直接重排,需要重建
|
||||
# 我们用更可靠的方式:收集所有子元素,清空,再按顺序添加
|
||||
sorted_children = sorted(list(parent), key=_sort_key)
|
||||
|
||||
# 移除所有子元素
|
||||
for child in list(parent):
|
||||
parent.remove(child)
|
||||
|
||||
# 按排序后的顺序重新添加(保持 tail 文本在最后)
|
||||
tail_text = ""
|
||||
for child in sorted_children:
|
||||
tail_text = child.tail or ""
|
||||
child.tail = ""
|
||||
parent.append(child)
|
||||
|
||||
# 恢复最后一个元素的 tail
|
||||
if sorted_children and tail_text:
|
||||
sorted_children[-1].tail = tail_text
|
||||
|
||||
# 递归处理子元素
|
||||
for child in parent:
|
||||
_reorder_children(child)
|
||||
|
||||
|
||||
def _reorder_bands(root: ET.Element):
|
||||
"""确保 band 内部 reportElement 在其他元素之前。"""
|
||||
for elem in root.iter():
|
||||
if _tag_local(elem.tag) == "band":
|
||||
_ensure_reportelement_first(elem)
|
||||
|
||||
|
||||
def _ensure_reportelement_first(band: ET.Element):
|
||||
"""在 band 内部,确保 reportElement 元素排在最前面。"""
|
||||
children = list(band)
|
||||
report_elements = [c for c in children if _tag_local(c.tag) == "reportElement"]
|
||||
other_elements = [c for c in children if _tag_local(c.tag) != "reportElement"]
|
||||
|
||||
if not report_elements:
|
||||
return
|
||||
|
||||
# 移除所有
|
||||
for c in list(band):
|
||||
band.remove(c)
|
||||
|
||||
# 先添加 reportElement
|
||||
tail = ""
|
||||
for r in report_elements:
|
||||
r.tail = ""
|
||||
band.append(r)
|
||||
# 再添加其他
|
||||
for o in other_elements:
|
||||
o.tail = ""
|
||||
band.append(o)
|
||||
# 恢复 tail
|
||||
last = band[-1] if list(band) else None
|
||||
if last and children:
|
||||
last.tail = children[-1].tail or ""
|
||||
|
||||
|
||||
def _restore_formatting(original: str, reordered: str) -> str:
|
||||
"""恢复 XML 声明和 CDATA 段。"""
|
||||
# 保留原始声明
|
||||
decl = ""
|
||||
if original.strip().startswith("<?xml"):
|
||||
m = re.match(r'<\?xml[^?]*\?>', original)
|
||||
if m:
|
||||
decl = m.group()
|
||||
if decl and not reordered.strip().startswith("<?xml"):
|
||||
reordered = decl + "\n" + reordered
|
||||
|
||||
# 恢复 CDATA(ET 会把 CDATA 转成普通文本)
|
||||
# 从原始 XML 提取所有 CDATA 块
|
||||
cdata_pattern = re.compile(r'<!\[CDATA\[(.*?)\]\]>', re.DOTALL)
|
||||
cdata_blocks = cdata_pattern.findall(original)
|
||||
|
||||
if cdata_blocks:
|
||||
# 在重排后的 XML 中,对应位置的文本用 CDATA 包裹
|
||||
def _restore_cdata(match):
|
||||
nonlocal cdata_blocks
|
||||
text = match.group(1)
|
||||
for cdata in cdata_blocks:
|
||||
if cdata.strip() == text.strip():
|
||||
return f"<![CDATA[{cdata}]]>"
|
||||
return match.group(0)
|
||||
|
||||
# 替换已转义的文本为 CDATA
|
||||
reordered = re.sub(
|
||||
r'(<queryString[^>]*>)\s*(.*?)\s*(</queryString>)',
|
||||
lambda m: m.group(1) + f"\n <![CDATA[{m.group(2).strip()}]]>\n " + m.group(3),
|
||||
reordered,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
|
||||
return reordered
|
||||
|
||||
|
||||
def normalize_jrxml(jrxml_text: str) -> str:
|
||||
"""规范化 JRXML:排序元素 + 恢复格式。"""
|
||||
if not jrxml_text or not jrxml_text.strip():
|
||||
return jrxml_text
|
||||
result = reorder_jrxml_elements(jrxml_text)
|
||||
return result
|
||||
@@ -0,0 +1,227 @@
|
||||
"""多租户知识库管理模块。
|
||||
|
||||
用户 + 知识库 CRUD,持久化到 kb_data/ 目录。
|
||||
每个 KB 拥有独立的 JSON 元数据文件和文件存储目录。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
import tempfile
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from backend.logger import get_logger
|
||||
|
||||
load_dotenv()
|
||||
|
||||
_kb_log = get_logger("kb_manager")
|
||||
|
||||
KB_DATA_DIR = Path(os.getenv("KB_DATA_DIR", "./kb_data"))
|
||||
_USERS_FILE = KB_DATA_DIR / "users.json"
|
||||
|
||||
_VALID_ID_RE = re.compile(r'^[a-fA-F0-9]{12,}$')
|
||||
|
||||
|
||||
def _validate_id(id_str: str, label: str = "id") -> None:
|
||||
if not _VALID_ID_RE.match(id_str):
|
||||
raise ValueError(f"Invalid {label}: {id_str!r}")
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _ensure_dir(path: Path) -> None:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _read_json(fp: Path) -> dict:
|
||||
with open(fp, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _write_json_atomic(fp: Path, data: dict) -> None:
|
||||
_ensure_dir(fp.parent)
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False,
|
||||
dir=fp.parent, encoding="utf-8",
|
||||
)
|
||||
try:
|
||||
json.dump(data, tmp, ensure_ascii=False, indent=2)
|
||||
tmp.flush()
|
||||
os.fsync(tmp.fileno())
|
||||
tmp.close()
|
||||
os.replace(tmp.name, str(fp))
|
||||
except Exception:
|
||||
tmp.close()
|
||||
Path(tmp.name).unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
# ── User CRUD ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _load_users() -> list[dict]:
|
||||
_ensure_dir(KB_DATA_DIR)
|
||||
if _USERS_FILE.exists():
|
||||
return _read_json(_USERS_FILE)
|
||||
return []
|
||||
|
||||
|
||||
def _save_users(users: list[dict]) -> None:
|
||||
_write_json_atomic(_USERS_FILE, users)
|
||||
|
||||
|
||||
def create_user(name: str, user_id: Optional[str] = None) -> dict:
|
||||
uid = user_id or uuid.uuid4().hex
|
||||
users = _load_users()
|
||||
if any(u["user_id"] == uid for u in users):
|
||||
raise ValueError(f"User {uid} already exists")
|
||||
user = {"user_id": uid, "name": name, "created_at": _now_iso()}
|
||||
users.append(user)
|
||||
_save_users(users)
|
||||
_ensure_dir(KB_DATA_DIR / uid)
|
||||
_write_json_atomic(KB_DATA_DIR / uid / "profile.json", user)
|
||||
_kb_log.info("创建用户", extra={"user_id": uid, "user_name": name})
|
||||
return user
|
||||
|
||||
|
||||
def list_users() -> list[dict]:
|
||||
return _load_users()
|
||||
|
||||
|
||||
def get_user(user_id: str) -> Optional[dict]:
|
||||
_validate_id(user_id, "user_id")
|
||||
for u in _load_users():
|
||||
if u["user_id"] == user_id:
|
||||
return u
|
||||
return None
|
||||
|
||||
|
||||
def delete_user(user_id: str) -> bool:
|
||||
_validate_id(user_id, "user_id")
|
||||
users = _load_users()
|
||||
filtered = [u for u in users if u["user_id"] != user_id]
|
||||
if len(filtered) == len(users):
|
||||
return False
|
||||
_save_users(filtered)
|
||||
user_dir = KB_DATA_DIR / user_id
|
||||
if user_dir.exists():
|
||||
shutil.rmtree(user_dir)
|
||||
_kb_log.info("删除用户", extra={"user_id": user_id})
|
||||
return True
|
||||
|
||||
|
||||
# ── KB CRUD ────────────────────────────────────────────────────────────────
|
||||
|
||||
def _kb_dir(kb_id: str) -> Optional[Path]:
|
||||
_validate_id(kb_id, "kb_id")
|
||||
for user_dir in KB_DATA_DIR.iterdir():
|
||||
if user_dir.is_dir() and not user_dir.name.startswith("."):
|
||||
candidate = user_dir / kb_id
|
||||
if candidate.is_dir():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_user_dir(user_id: str) -> Path:
|
||||
_validate_id(user_id, "user_id")
|
||||
d = KB_DATA_DIR / user_id
|
||||
_ensure_dir(d)
|
||||
return d
|
||||
|
||||
|
||||
def create_kb(user_id: str, name: str, description: str = "",
|
||||
kb_id: Optional[str] = None) -> dict:
|
||||
user_dir = _ensure_user_dir(user_id)
|
||||
kid = kb_id or uuid.uuid4().hex
|
||||
kb_dir = user_dir / kid
|
||||
_ensure_dir(kb_dir)
|
||||
_ensure_dir(kb_dir / "raw")
|
||||
|
||||
now = _now_iso()
|
||||
meta = {
|
||||
"kb_id": kid, "user_id": user_id, "name": name,
|
||||
"description": description, "created_at": now, "updated_at": now,
|
||||
"fields": [], "templates": [], "file_count": 0,
|
||||
"chunk_count": 0, "parse_status": "empty",
|
||||
}
|
||||
_write_json_atomic(kb_dir / "meta.json", meta)
|
||||
_kb_log.info("创建知识库", extra={"kb_id": kid, "user_id": user_id, "kb_name": name})
|
||||
return meta
|
||||
|
||||
|
||||
def list_kbs(user_id: str) -> list[dict]:
|
||||
user_dir = _ensure_user_dir(user_id)
|
||||
kbs = []
|
||||
for kb_dir in sorted(user_dir.iterdir(), key=os.path.getmtime, reverse=True):
|
||||
if kb_dir.is_dir() and not kb_dir.name.startswith("."):
|
||||
meta_path = kb_dir / "meta.json"
|
||||
if meta_path.exists():
|
||||
meta = _read_json(meta_path)
|
||||
kbs.append({
|
||||
"kb_id": meta.get("kb_id", kb_dir.name),
|
||||
"name": meta.get("name", kb_dir.name),
|
||||
"description": meta.get("description", ""),
|
||||
"created_at": meta.get("created_at", ""),
|
||||
"updated_at": meta.get("updated_at", ""),
|
||||
"field_count": len(meta.get("fields", [])),
|
||||
"template_count": len(meta.get("templates", [])),
|
||||
"file_count": meta.get("file_count", 0),
|
||||
"chunk_count": meta.get("chunk_count", 0),
|
||||
"parse_status": meta.get("parse_status", "empty"),
|
||||
})
|
||||
return kbs
|
||||
|
||||
|
||||
def get_kb(kb_id: str) -> Optional[dict]:
|
||||
_validate_id(kb_id, "kb_id")
|
||||
kb_dir = _kb_dir(kb_id)
|
||||
if kb_dir is None:
|
||||
return None
|
||||
meta_path = kb_dir / "meta.json"
|
||||
return _read_json(meta_path) if meta_path.exists() else None
|
||||
|
||||
|
||||
def update_kb_meta(kb_id: str, updates: dict) -> Optional[dict]:
|
||||
kb_dir = _kb_dir(kb_id)
|
||||
if kb_dir is None:
|
||||
return None
|
||||
meta_path = kb_dir / "meta.json"
|
||||
meta = _read_json(meta_path)
|
||||
meta.update(updates)
|
||||
meta["updated_at"] = _now_iso()
|
||||
_write_json_atomic(meta_path, meta)
|
||||
return meta
|
||||
|
||||
|
||||
def delete_kb(kb_id: str) -> bool:
|
||||
kb_dir = _kb_dir(kb_id)
|
||||
if kb_dir is None:
|
||||
return False
|
||||
shutil.rmtree(kb_dir)
|
||||
_kb_log.info("删除知识库", extra={"kb_id": kb_id})
|
||||
return True
|
||||
|
||||
|
||||
def get_kb_raw_dir(kb_id: str) -> Optional[Path]:
|
||||
kb_dir = _kb_dir(kb_id)
|
||||
return kb_dir / "raw" if kb_dir else None
|
||||
|
||||
|
||||
def get_kb_chunks_path(kb_id: str) -> Optional[Path]:
|
||||
kb_dir = _kb_dir(kb_id)
|
||||
return kb_dir / "chunks.json" if kb_dir else None
|
||||
|
||||
|
||||
def get_kb_chroma_path(kb_id: str) -> Optional[Path]:
|
||||
kb_dir = _kb_dir(kb_id)
|
||||
if kb_dir is None:
|
||||
return None
|
||||
chroma_dir = kb_dir / "chroma"
|
||||
_ensure_dir(chroma_dir)
|
||||
return chroma_dir
|
||||
@@ -0,0 +1,336 @@
|
||||
"""KB 解析管道 — 文件提取→字段解析→chunk 切割→向量嵌入。
|
||||
|
||||
调用者: api_server.py (upload endpoint), scripts/init_default_kb.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
import tarfile
|
||||
import tempfile
|
||||
import defusedxml.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from backend.logger import get_logger
|
||||
from backend.file_parser import parse_file
|
||||
|
||||
load_dotenv()
|
||||
|
||||
_kb_parse_log = get_logger("kb_parser")
|
||||
|
||||
|
||||
def _find_tag(elem, tag):
|
||||
for el in elem.iter():
|
||||
local = el.tag.split("}")[-1] if "}" in el.tag else el.tag
|
||||
if local == tag:
|
||||
return el
|
||||
return None
|
||||
|
||||
|
||||
def _find_all_tags(elem, tag):
|
||||
results = []
|
||||
for el in elem.iter():
|
||||
local = el.tag.split("}")[-1] if "}" in el.tag else el.tag
|
||||
if local == tag:
|
||||
results.append(el)
|
||||
return results
|
||||
|
||||
|
||||
def parse_jrxml_fields(jrxml_path: str) -> dict:
|
||||
"""解析 JRXML 文件,提取参数和字段定义。"""
|
||||
try:
|
||||
tree = ET.parse(jrxml_path)
|
||||
root = tree.getroot()
|
||||
except ET.ParseError as e:
|
||||
return {"error": f"JRXML 解析失败: {e}", "parameters": [], "fields": [],
|
||||
"report_name": ""}
|
||||
|
||||
report_name = root.attrib.get("name", "")
|
||||
page_width = root.attrib.get("pageWidth", "")
|
||||
page_height = root.attrib.get("pageHeight", "")
|
||||
|
||||
parameters = []
|
||||
for p in _find_all_tags(root, "parameter"):
|
||||
params = {"name": p.attrib.get("name", ""),
|
||||
"type": p.attrib.get("class", "java.lang.String"),
|
||||
"description": ""}
|
||||
desc = _find_tag(p, "parameterDescription")
|
||||
if desc is not None and desc.text:
|
||||
params["description"] = desc.text.strip()
|
||||
parameters.append(params)
|
||||
|
||||
fields = []
|
||||
for f in _find_all_tags(root, "field"):
|
||||
fields.append({"name": f.attrib.get("name", ""),
|
||||
"type": f.attrib.get("class", "java.lang.String"),
|
||||
"description": ""})
|
||||
|
||||
query_text = ""
|
||||
query = _find_tag(root, "queryString")
|
||||
if query is not None and query.text:
|
||||
query_text = query.text.strip()
|
||||
|
||||
return {"report_name": report_name, "page_width": page_width,
|
||||
"page_height": page_height, "parameters": parameters,
|
||||
"fields": fields, "query": query_text, "error": None}
|
||||
|
||||
|
||||
def _extract_archive(file_path: str, dest_dir: str) -> list[str]:
|
||||
extracted = []
|
||||
resolved_dest = os.path.realpath(dest_dir)
|
||||
|
||||
if zipfile.is_zipfile(file_path):
|
||||
with zipfile.ZipFile(file_path, "r") as zf:
|
||||
for member in zf.namelist():
|
||||
member_path = os.path.realpath(os.path.join(dest_dir, member))
|
||||
if not member_path.startswith(resolved_dest + os.sep):
|
||||
continue
|
||||
zf.extract(member, dest_dir)
|
||||
if not member.endswith("/"):
|
||||
extracted.append(member_path)
|
||||
elif tarfile.is_tarfile(file_path):
|
||||
with tarfile.open(file_path, "r:*") as tf:
|
||||
for member in tf.getmembers():
|
||||
member_path = os.path.realpath(os.path.join(dest_dir, member.name))
|
||||
if not member_path.startswith(resolved_dest + os.sep):
|
||||
continue
|
||||
tf.extract(member, dest_dir)
|
||||
if not member.name.endswith("/"):
|
||||
extracted.append(member_path)
|
||||
return extracted
|
||||
|
||||
|
||||
def process_file_for_kb(kb_id: str, file_path: str,
|
||||
source_name: str = "") -> dict:
|
||||
from backend.kb_manager import get_kb_raw_dir
|
||||
raw_dir = get_kb_raw_dir(kb_id)
|
||||
if raw_dir is None:
|
||||
return {"error": "KB 不存在"}
|
||||
|
||||
fname = source_name or os.path.basename(file_path)
|
||||
dest = raw_dir / fname
|
||||
shutil.copy2(file_path, dest)
|
||||
|
||||
suffix = Path(fname).suffix.lower()
|
||||
|
||||
if suffix == ".jrxml":
|
||||
jrxml_info = parse_jrxml_fields(file_path)
|
||||
text = f"[JRXML 模板: {jrxml_info['report_name']}]\n"
|
||||
text += f"页面: {jrxml_info['page_width']}x{jrxml_info['page_height']}\n"
|
||||
text += "参数:\n" + "\n".join(
|
||||
f" {p['name']} ({p['type']})" for p in jrxml_info["parameters"])
|
||||
text += "\n字段:\n" + "\n".join(
|
||||
f" {f['name']} ({f['type']})" for f in jrxml_info["fields"])
|
||||
if jrxml_info["query"]:
|
||||
text += f"\n查询:\n{jrxml_info['query']}"
|
||||
try:
|
||||
raw_xml = Path(file_path).read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
raw_xml = ""
|
||||
return {"filename": fname, "type": "jrxml", "text": text,
|
||||
"raw_xml": raw_xml, "jrxml_info": jrxml_info, "error": None}
|
||||
|
||||
if suffix in (".zip", ".tar", ".gz", ".tgz"):
|
||||
tmpdir = tempfile.mkdtemp(prefix="kb_extract_")
|
||||
try:
|
||||
extracted = _extract_archive(file_path, tmpdir)
|
||||
sub_results = []
|
||||
for ep in extracted:
|
||||
sub = process_file_for_kb(
|
||||
kb_id, ep, source_name=os.path.basename(ep))
|
||||
sub_results.append(sub)
|
||||
return {"filename": fname, "type": "archive", "text": "",
|
||||
"archive_contents": sub_results, "error": None}
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
parse_result = parse_file(str(dest))
|
||||
return {"filename": fname, "type": suffix.lstrip("."),
|
||||
"text": parse_result.get("text", ""),
|
||||
"error": parse_result.get("error")}
|
||||
|
||||
|
||||
def chunk_file_results(results: list[dict], kb_name: str = "") -> list[dict]:
|
||||
chunks = []
|
||||
chunk_idx = 0
|
||||
|
||||
for r in results:
|
||||
if r.get("type") == "archive":
|
||||
for sub in r.get("archive_contents", []):
|
||||
chunks.extend(chunk_file_results([sub], kb_name))
|
||||
continue
|
||||
|
||||
fname = r.get("filename", "")
|
||||
ftype = r.get("type", "")
|
||||
text = r.get("text", "")
|
||||
if not text.strip():
|
||||
continue
|
||||
|
||||
if ftype == "jrxml" and r.get("raw_xml"):
|
||||
jinfo = r.get("jrxml_info", {})
|
||||
report_name = jinfo.get("report_name", "")
|
||||
chunks.append({
|
||||
"id": f"chunk_{chunk_idx}",
|
||||
"content": f"[JRXML 模板: {report_name}]\n{r['text']}\n\n"
|
||||
f"<xml>\n{r['raw_xml']}\n</xml>",
|
||||
"metadata": {"chunk_type": "jrxml_template",
|
||||
"source_file": fname,
|
||||
"report_name": report_name,
|
||||
"kb_name": kb_name,
|
||||
"param_count": len(jinfo.get("parameters", [])),
|
||||
"field_count": len(jinfo.get("fields", []))},
|
||||
})
|
||||
chunk_idx += 1
|
||||
continue
|
||||
|
||||
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
|
||||
for para in paragraphs:
|
||||
if len(para) < 10:
|
||||
continue
|
||||
chunk_type = "md_section" if ftype in ("md", "") else f"{ftype}_text"
|
||||
chunks.append({
|
||||
"id": f"chunk_{chunk_idx}",
|
||||
"content": para,
|
||||
"metadata": {"chunk_type": chunk_type,
|
||||
"source_file": fname, "kb_name": kb_name},
|
||||
})
|
||||
chunk_idx += 1
|
||||
return chunks
|
||||
|
||||
|
||||
def extract_fields_with_llm(text: str, llm=None) -> list[dict]:
|
||||
if llm is None:
|
||||
return _extract_fields_from_table(text)
|
||||
prompt = (
|
||||
"请分析以下接口文档内容,提取所有字段定义。\n"
|
||||
"对每个字段,输出: 字段名, 含义, 类型, 是否必需。\n"
|
||||
"以 JSON 数组格式输出,每个元素为 {\"name\": \"...\", "
|
||||
"\"description\": \"...\", \"type\": \"...\", \"required\": false}。\n\n"
|
||||
f"{text}"
|
||||
)
|
||||
try:
|
||||
response = llm.invoke(prompt)
|
||||
content = response.content if hasattr(response, "content") else str(response)
|
||||
start = content.find("[")
|
||||
end = content.rfind("]") + 1
|
||||
if start >= 0 and end > start:
|
||||
return json.loads(content[start:end])
|
||||
except Exception as e:
|
||||
_kb_parse_log.warning("LLM 字段提取失败,使用表格回退: %s", e)
|
||||
return _extract_fields_from_table(text)
|
||||
|
||||
|
||||
def _extract_fields_from_table(text: str) -> list[dict]:
|
||||
fields = []
|
||||
lines = text.split("\n")
|
||||
header_found = False
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line.startswith("|"):
|
||||
continue
|
||||
cells = [c.strip() for c in line.split("|")[1:-1]]
|
||||
if not cells:
|
||||
continue
|
||||
if not header_found:
|
||||
if any(h in str(c) for c in cells
|
||||
for h in ["字段", "名称", "含义", "说明", "类型"]):
|
||||
header_found = True
|
||||
continue
|
||||
if all(c.replace("-", "").replace(":", "").replace(" ", "") == ""
|
||||
for c in cells):
|
||||
continue
|
||||
if len(cells) >= 2:
|
||||
name = cells[0].replace("**", "").replace("L ", "").replace("\\", "").strip()
|
||||
if not name or name in ("", "---"):
|
||||
continue
|
||||
field = {"name": name, "description": "", "type": "java.lang.String",
|
||||
"required": False}
|
||||
if len(cells) >= 2 and cells[1]:
|
||||
field["description"] = cells[1].replace("<br/>", " ").strip()
|
||||
if len(cells) >= 3 and cells[2]:
|
||||
field["required"] = cells[2].strip() in ("是", "Y", "y", "yes", "Yes", "必填")
|
||||
if len(cells) >= 4 and cells[3]:
|
||||
field["type"] = cells[3].strip()
|
||||
fields.append(field)
|
||||
return fields
|
||||
|
||||
|
||||
def build_kb_from_files(kb_id: str, file_paths: list[str],
|
||||
llm=None) -> dict:
|
||||
from backend.kb_manager import update_kb_meta, get_kb_chunks_path
|
||||
from backend.kb_searcher import get_kb_searcher
|
||||
|
||||
all_results = []
|
||||
errors = []
|
||||
for fp in file_paths:
|
||||
try:
|
||||
r = process_file_for_kb(kb_id, fp)
|
||||
all_results.append(r)
|
||||
if r.get("error"):
|
||||
errors.append({"file": os.path.basename(fp), "error": r["error"]})
|
||||
except Exception as e:
|
||||
errors.append({"file": os.path.basename(fp), "error": str(e)})
|
||||
|
||||
chunks = chunk_file_results(all_results)
|
||||
|
||||
chunks_path = get_kb_chunks_path(kb_id)
|
||||
if chunks_path:
|
||||
chunks_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(chunks_path, "w", encoding="utf-8") as f:
|
||||
json.dump(chunks, f, ensure_ascii=False, indent=2)
|
||||
|
||||
searcher = get_kb_searcher(kb_id)
|
||||
if searcher and chunks:
|
||||
try:
|
||||
searcher.add_chunks(chunks)
|
||||
except Exception as e:
|
||||
errors.append({"file": "embedding", "error": str(e)})
|
||||
|
||||
all_fields = []
|
||||
template_names = []
|
||||
for r in all_results:
|
||||
_collect_from_result(r, all_fields, template_names)
|
||||
|
||||
for r in all_results:
|
||||
if r.get("type") in ("archive", "jrxml"):
|
||||
continue
|
||||
text = r.get("text", "")
|
||||
if text.strip():
|
||||
for ef in extract_fields_with_llm(text, llm):
|
||||
if not any(f["name"] == ef["name"] for f in all_fields):
|
||||
all_fields.append(ef)
|
||||
|
||||
update_kb_meta(kb_id, {
|
||||
"fields": all_fields, "templates": template_names,
|
||||
"file_count": len(file_paths), "chunk_count": len(chunks),
|
||||
"parse_status": "ready" if not errors else "partial",
|
||||
})
|
||||
|
||||
_kb_parse_log.info("KB 构建完成", extra={
|
||||
"kb_id": kb_id, "fields": len(all_fields),
|
||||
"templates": len(template_names), "chunks": len(chunks),
|
||||
})
|
||||
|
||||
return {"status": "ready" if not errors else "partial",
|
||||
"field_count": len(all_fields), "chunk_count": len(chunks),
|
||||
"template_count": len(template_names), "errors": errors}
|
||||
|
||||
|
||||
def _collect_from_result(r: dict, all_fields: list, template_names: list) -> None:
|
||||
jinfo = r.get("jrxml_info")
|
||||
if jinfo and jinfo.get("report_name"):
|
||||
template_names.append({"name": jinfo["report_name"],
|
||||
"file": r.get("filename", "")})
|
||||
for p in jinfo.get("parameters", []):
|
||||
field = {"name": p["name"], "description": p.get("description", ""),
|
||||
"type": p.get("type", "java.lang.String"), "required": False}
|
||||
if not any(f["name"] == field["name"] for f in all_fields):
|
||||
all_fields.append(field)
|
||||
for f in jinfo.get("fields", []):
|
||||
field = {"name": f["name"], "description": f.get("description", ""),
|
||||
"type": f.get("type", "java.lang.String"), "required": False}
|
||||
if not any(fi["name"] == field["name"] for fi in all_fields):
|
||||
all_fields.append(field)
|
||||
@@ -0,0 +1,170 @@
|
||||
"""KB 隔离的 ChromaDB 语义搜索适配器。
|
||||
|
||||
每个知识库拥有独立的 ChromaDB collection。
|
||||
调用者: backend/rag_adapter.py, agent/nodes.py, api_server.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _resolve(path: str) -> Path:
|
||||
p = Path(path)
|
||||
return p if p.is_absolute() else _PROJECT_ROOT / p
|
||||
|
||||
|
||||
class KBChromaSearcher:
|
||||
"""连接指定 KB 的 ChromaDB,提供语义搜索。"""
|
||||
|
||||
def __init__(self, chroma_path: str, collection_name: str = "kb_chunks",
|
||||
model_name: Optional[str] = None, use_gpu: Optional[bool] = None,
|
||||
use_fp16: Optional[bool] = None):
|
||||
self.chroma_path = str(_resolve(chroma_path))
|
||||
self.collection_name = collection_name
|
||||
model_path = model_name or os.getenv(
|
||||
"RAG_EMBED_MODEL", "./rag/models/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
resolved = _resolve(model_path)
|
||||
self.model_name = str(resolved) if resolved.exists() else model_path
|
||||
self.use_gpu = (use_gpu if use_gpu is not None
|
||||
else os.getenv("RAG_USE_GPU", "true").lower() in ("true", "1"))
|
||||
self.use_fp16 = (use_fp16 if use_fp16 is not None
|
||||
else os.getenv("RAG_USE_FP16", "true").lower() in ("true", "1"))
|
||||
self._model = None
|
||||
self._client = None
|
||||
self._collection = None
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
if self._model is None:
|
||||
import torch
|
||||
from sentence_transformers import SentenceTransformer
|
||||
device = "cuda" if (self.use_gpu and torch.cuda.is_available()) else "cpu"
|
||||
logger.info("加载嵌入模型: %s (device=%s)", self.model_name, device)
|
||||
model = SentenceTransformer(self.model_name, device=device)
|
||||
if device == "cuda" and self.use_fp16:
|
||||
model = model.half()
|
||||
self._model = model
|
||||
return self._model
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if self._client is None:
|
||||
import chromadb
|
||||
self._client = chromadb.PersistentClient(path=self.chroma_path)
|
||||
return self._client
|
||||
|
||||
@property
|
||||
def collection(self):
|
||||
if self._collection is None:
|
||||
try:
|
||||
self._collection = self.client.get_collection(self.collection_name)
|
||||
except Exception:
|
||||
self._collection = self.client.create_collection(
|
||||
self.collection_name, metadata={"hnsw:space": "cosine"})
|
||||
return self._collection
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
try:
|
||||
self.client.get_collection(self.collection_name)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def search(self, query: str, k: int = 5, threshold: Optional[float] = None) -> list[dict]:
|
||||
if not self.is_ready():
|
||||
return []
|
||||
query_embedding = self.model.encode(
|
||||
query, normalize_embeddings=True, show_progress_bar=False)
|
||||
results = self.collection.query(
|
||||
query_embeddings=[query_embedding.tolist()],
|
||||
n_results=k, include=["documents", "metadatas", "distances"])
|
||||
output = []
|
||||
if not results["ids"] or not results["ids"][0]:
|
||||
return output
|
||||
for i, doc_id in enumerate(results["ids"][0]):
|
||||
dist = results["distances"][0][i]
|
||||
if threshold is not None and dist > threshold:
|
||||
continue
|
||||
output.append({
|
||||
"id": doc_id,
|
||||
"content": results["documents"][0][i],
|
||||
"metadata": results["metadatas"][0][i] or {},
|
||||
"distance": dist,
|
||||
})
|
||||
return output
|
||||
|
||||
def search_templates(self, query: str, k: int = 3) -> list[dict]:
|
||||
results = self.search(query, k=k * 2)
|
||||
templates = []
|
||||
for r in results:
|
||||
meta = r.get("metadata", {})
|
||||
chunk_type = meta.get("chunk_type", "")
|
||||
if "jrxml" in chunk_type.lower() or meta.get("report_name"):
|
||||
templates.append(r)
|
||||
if len(templates) >= k:
|
||||
break
|
||||
return templates
|
||||
|
||||
def search_as_context(self, query: str, k: int = 5) -> str:
|
||||
results = self.search(query, k=k)
|
||||
if not results:
|
||||
return ""
|
||||
parts = []
|
||||
for r in results:
|
||||
meta = r.get("metadata", {})
|
||||
header = f"[类型:{meta.get('chunk_type', 'N/A')}]"
|
||||
if meta.get("report_name"):
|
||||
header += f" [报表:{meta['report_name']}]"
|
||||
parts.append(f"{header}\n{r['content']}")
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
def add_chunks(self, chunks: list[dict]) -> None:
|
||||
if not chunks:
|
||||
return
|
||||
ids = [c["id"] for c in chunks]
|
||||
docs = [c["content"] for c in chunks]
|
||||
metas = [c.get("metadata", {}) for c in chunks]
|
||||
embeddings = self.model.encode(
|
||||
docs, normalize_embeddings=True, show_progress_bar=True)
|
||||
self.collection.upsert(
|
||||
ids=ids, documents=docs, metadatas=metas,
|
||||
embeddings=embeddings.tolist())
|
||||
|
||||
|
||||
_searchers: dict = {}
|
||||
|
||||
|
||||
def get_kb_searcher(kb_id: str) -> Optional[KBChromaSearcher]:
|
||||
from backend.kb_manager import get_kb_chroma_path
|
||||
if kb_id in _searchers:
|
||||
return _searchers[kb_id]
|
||||
chroma_path = get_kb_chroma_path(kb_id)
|
||||
if chroma_path is None:
|
||||
return None
|
||||
searcher = KBChromaSearcher(str(chroma_path))
|
||||
_searchers[kb_id] = searcher
|
||||
return searcher
|
||||
|
||||
|
||||
def search_kb(kb_id: str, query: str, k: int = 5) -> str:
|
||||
searcher = get_kb_searcher(kb_id)
|
||||
if searcher is None:
|
||||
return ""
|
||||
return searcher.search_as_context(query, k=k)
|
||||
|
||||
|
||||
def search_templates_in_kb(kb_id: str, query: str, k: int = 3) -> list[dict]:
|
||||
searcher = get_kb_searcher(kb_id)
|
||||
if searcher is None:
|
||||
return []
|
||||
return searcher.search_templates(query, k=k)
|
||||
@@ -0,0 +1,672 @@
|
||||
"""A4 图片模板布局分析器。
|
||||
|
||||
检测上传图片并逐行识别每个元素的:
|
||||
- 位置 (x, y, w, h)
|
||||
- 字体大小(基于 OCR 边界框高度估算)
|
||||
- 文本内容
|
||||
|
||||
支持三种模式:
|
||||
- 完整 A4 模板:比例匹配 + OCR 元素 ≥2 → 全量布局描述
|
||||
- 行片段(非 A4 但有元素):视为 A4 中的某几行 → 部分布局描述
|
||||
- 修改匹配:将图片中的行与现有 JRXML 做匹配,定位修改位置
|
||||
|
||||
用法:
|
||||
from backend.layout_analyzer import analyze_layout, match_rows_to_jrxml
|
||||
result = analyze_layout("row_snippet.png")
|
||||
# result["template_type"] = "partial_rows"
|
||||
match = match_rows_to_jrxml(result, current_jrxml)
|
||||
# match["matched_rows"] = [{"row_index": 0, "jrxml_section": "detail_band", ...}]
|
||||
"""
|
||||
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import PIL.Image
|
||||
|
||||
# A4 标准尺寸 (mm): 210 × 297, 比例 ≈ 0.707
|
||||
A4_RATIO = 210 / 297
|
||||
A4_RATIO_EXACT_MIN, A4_RATIO_EXACT_MAX = 0.686, 0.728
|
||||
A4_RATIO_CLOSE_MIN, A4_RATIO_CLOSE_MAX = 0.650, 0.764
|
||||
|
||||
|
||||
def analyze_layout(
|
||||
file_path: str,
|
||||
row_tolerance_ratio: float = 0.02,
|
||||
) -> dict:
|
||||
"""分析图片/PDF 的报表模板布局。
|
||||
|
||||
返回:
|
||||
{
|
||||
"is_a4_template": bool, # 完整 A4 模板
|
||||
"is_partial": bool, # 行片段(非 A4 但有文字元素)
|
||||
"template_type": str, # "full_a4" | "partial_rows" | "unknown"
|
||||
"image_size": (w, h),
|
||||
"aspect_ratio": float,
|
||||
"a4_confidence": str,
|
||||
"rows": [{y_center, elements: [{x, y, w, h, font_size, text}, ...]}, ...],
|
||||
"description": str,
|
||||
"total_rows": int,
|
||||
"total_elements": int,
|
||||
}
|
||||
"""
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
return _empty_result("文件不存在")
|
||||
|
||||
img = _load_image(path)
|
||||
if img is None:
|
||||
return _empty_result("无法加载图片")
|
||||
|
||||
w, h = img.size
|
||||
ratio = min(w, h) / max(w, h)
|
||||
|
||||
# A4 比例判定
|
||||
if A4_RATIO_EXACT_MIN <= ratio <= A4_RATIO_EXACT_MAX:
|
||||
a4_confidence = "exact"
|
||||
elif A4_RATIO_CLOSE_MIN <= ratio <= A4_RATIO_CLOSE_MAX:
|
||||
a4_confidence = "close"
|
||||
else:
|
||||
a4_confidence = "not_a4"
|
||||
|
||||
# OCR 提取
|
||||
elements = _ocr_elements(img, file_path)
|
||||
|
||||
if not elements:
|
||||
return {
|
||||
"is_a4_template": False,
|
||||
"is_partial": False,
|
||||
"template_type": "unknown",
|
||||
"image_size": (w, h),
|
||||
"aspect_ratio": round(ratio, 3),
|
||||
"a4_confidence": a4_confidence,
|
||||
"rows": [],
|
||||
"description": _build_description([], w, h, a4_confidence, "unknown"),
|
||||
"total_rows": 0,
|
||||
"total_elements": 0,
|
||||
}
|
||||
|
||||
# 行分组
|
||||
rows = _group_into_rows(elements, h, row_tolerance_ratio)
|
||||
|
||||
total = sum(len(r["elements"]) for r in rows)
|
||||
|
||||
# 模板类型判定
|
||||
is_full_a4 = a4_confidence != "not_a4" and total >= 2
|
||||
is_partial = not is_full_a4 and total >= 1 # 非 A4 但有文字 → 行片段
|
||||
|
||||
if is_full_a4:
|
||||
template_type = "full_a4"
|
||||
elif is_partial:
|
||||
template_type = "partial_rows"
|
||||
else:
|
||||
template_type = "unknown"
|
||||
|
||||
description = _build_description(rows, w, h, a4_confidence, template_type)
|
||||
|
||||
return {
|
||||
"is_a4_template": is_full_a4,
|
||||
"is_partial": is_partial,
|
||||
"template_type": template_type,
|
||||
"image_size": (w, h),
|
||||
"aspect_ratio": round(ratio, 3),
|
||||
"a4_confidence": a4_confidence,
|
||||
"rows": rows,
|
||||
"description": description,
|
||||
"total_rows": len(rows),
|
||||
"total_elements": total,
|
||||
}
|
||||
|
||||
|
||||
def extract_layout_schema(layout_result: dict) -> dict:
|
||||
"""将 analyze_layout() 的完整 OCR 行数据压缩为高层布局 schema。
|
||||
|
||||
列检测:跨所有行对元素 X 坐标进行聚类。
|
||||
区域分类:启发式识别标题/表头/数据/表尾行。
|
||||
输出紧凑的 schema_text,供 LLM 阶段一骨架生成使用。
|
||||
"""
|
||||
rows = layout_result.get("rows", [])
|
||||
if not rows:
|
||||
return _empty_schema()
|
||||
|
||||
img_w, img_h = layout_result.get("image_size", (595, 842))
|
||||
if img_w <= 0:
|
||||
img_w = 595
|
||||
|
||||
all_elements = []
|
||||
for row in rows:
|
||||
all_elements.extend(row.get("elements", []))
|
||||
if not all_elements:
|
||||
return _empty_schema()
|
||||
|
||||
x_centers = sorted((e["x"] + e["w"] / 2) for e in all_elements)
|
||||
avg_width = sum(e["w"] for e in all_elements) / len(all_elements)
|
||||
cluster_threshold = avg_width * 0.5
|
||||
|
||||
clusters = []
|
||||
current_cluster = [x_centers[0]]
|
||||
for xc in x_centers[1:]:
|
||||
if xc - current_cluster[-1] < cluster_threshold:
|
||||
current_cluster.append(xc)
|
||||
else:
|
||||
clusters.append(current_cluster)
|
||||
current_cluster = [xc]
|
||||
if current_cluster:
|
||||
clusters.append(current_cluster)
|
||||
|
||||
columns = []
|
||||
for ci, cluster in enumerate(clusters):
|
||||
cx_min = min(cluster)
|
||||
cx_max = max(cluster)
|
||||
col_elements = [
|
||||
e for e in all_elements
|
||||
if cx_min - cluster_threshold <= (e["x"] + e["w"] / 2) <= cx_max + cluster_threshold
|
||||
]
|
||||
avg_w = sum(e["w"] for e in col_elements) / len(col_elements) if col_elements else 0
|
||||
x_start = min(e["x"] for e in col_elements)
|
||||
|
||||
col_elements_by_y = sorted(col_elements, key=lambda e: e["y"])
|
||||
header_text = col_elements_by_y[0]["text"] if col_elements_by_y else f"列{ci+1}"
|
||||
|
||||
columns.append({
|
||||
"index": ci,
|
||||
"header_text": header_text,
|
||||
"avg_width": round(avg_w, 1),
|
||||
"x_start": round(x_start, 1),
|
||||
})
|
||||
|
||||
columns.sort(key=lambda c: c["x_start"])
|
||||
|
||||
row_element_counts = [len(r.get("elements", [])) for r in rows]
|
||||
median_count = sorted(row_element_counts)[len(row_element_counts) // 2] if row_element_counts else 0
|
||||
total_rows = len(rows)
|
||||
|
||||
regions = []
|
||||
current_region = None
|
||||
|
||||
for ri in range(total_rows):
|
||||
count = row_element_counts[ri]
|
||||
if ri == 0 and count < median_count * 0.6 and total_rows > 2:
|
||||
rtype = "title"
|
||||
elif ri == 0 and total_rows <= 2:
|
||||
rtype = "header"
|
||||
elif ri == 1 and total_rows > 2:
|
||||
rtype = "header" if median_count > 0 else "data"
|
||||
elif ri >= total_rows - 2 and count < median_count * 0.7 and total_rows > 3:
|
||||
rtype = "footer"
|
||||
else:
|
||||
rtype = "data"
|
||||
|
||||
if current_region and current_region["type"] == rtype:
|
||||
current_region["row_indices"].append(ri)
|
||||
current_region["element_count"] += count
|
||||
else:
|
||||
if current_region:
|
||||
regions.append(current_region)
|
||||
current_region = {"type": rtype, "row_indices": [ri], "element_count": count}
|
||||
|
||||
if current_region:
|
||||
regions.append(current_region)
|
||||
|
||||
# schema_text
|
||||
width_ratios = [c["avg_width"] / img_w for c in columns]
|
||||
width_labels = []
|
||||
for r in width_ratios:
|
||||
if r < 0.08:
|
||||
width_labels.append("窄")
|
||||
elif r > 0.20:
|
||||
width_labels.append("宽")
|
||||
else:
|
||||
width_labels.append("中")
|
||||
|
||||
col_descs = []
|
||||
for ci, col in enumerate(columns):
|
||||
wl = width_labels[ci] if ci < len(width_labels) else "中"
|
||||
col_descs.append(f"{col['header_text']}({wl})")
|
||||
|
||||
_rn = {"title": "标题", "header": "表头", "data": "数据", "footer": "表尾"}
|
||||
region_parts = []
|
||||
for r in regions:
|
||||
label = _rn.get(r["type"], r["type"])
|
||||
region_parts.append(f"{label}({len(r['row_indices'])}行)")
|
||||
region_summary = " → ".join(region_parts)
|
||||
|
||||
schema_text = (
|
||||
f"报表布局: {len(columns)}列 x {total_rows}行, A4纵向\n"
|
||||
f"列定义: {', '.join(col_descs)}\n"
|
||||
f"区域: {region_summary}"
|
||||
)
|
||||
|
||||
return {
|
||||
"columns": columns,
|
||||
"regions": regions,
|
||||
"total_rows": total_rows,
|
||||
"total_columns": len(columns),
|
||||
"a4_dimensions": {"width": 595, "height": 842},
|
||||
"schema_text": schema_text,
|
||||
}
|
||||
|
||||
|
||||
def _empty_schema() -> dict:
|
||||
return {
|
||||
"columns": [],
|
||||
"regions": [],
|
||||
"total_rows": 0,
|
||||
"total_columns": 0,
|
||||
"a4_dimensions": {"width": 595, "height": 842},
|
||||
"schema_text": "无法解析报表布局",
|
||||
}
|
||||
|
||||
|
||||
def match_rows_to_jrxml(
|
||||
layout_result: dict,
|
||||
current_jrxml: str,
|
||||
) -> dict:
|
||||
"""将图片中的行与现有 JRXML 中的 section/band 做匹配。
|
||||
|
||||
匹配策略:
|
||||
1. 从图片 OCR 文本中提取关键词
|
||||
2. 在 JRXML 中搜索这些关键词出现在哪个 band
|
||||
3. 返回匹配结果,可用于定位修改位置
|
||||
|
||||
返回:
|
||||
{
|
||||
"matched": bool,
|
||||
"matched_rows": [{row_index, row_y_center, jrxml_section, confidence}],
|
||||
"unmatched_rows": [...],
|
||||
"description": str, # 人类可读的匹配结果
|
||||
}
|
||||
"""
|
||||
rows = layout_result.get("rows", [])
|
||||
if not rows or not current_jrxml.strip():
|
||||
return {"matched": False, "matched_rows": [], "unmatched_rows": rows,
|
||||
"description": "无行数据或 JRXML 为空"}
|
||||
|
||||
# 解析 JRXML 结构
|
||||
jrxml_sections = _parse_jrxml_sections(current_jrxml)
|
||||
|
||||
matched_rows = []
|
||||
unmatched_rows = []
|
||||
|
||||
for ri, row in enumerate(rows):
|
||||
ocr_texts = [e["text"] for e in row["elements"]]
|
||||
best_section = None
|
||||
best_score = 0
|
||||
|
||||
for section in jrxml_sections:
|
||||
score = _text_similarity(ocr_texts, section["text_content"])
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_section = section
|
||||
|
||||
if best_score > 0.3 and best_section: # 最低匹配阈值
|
||||
matched_rows.append({
|
||||
"row_index": ri,
|
||||
"row_y_center": row["y_center"],
|
||||
"jrxml_section": best_section["name"],
|
||||
"jrxml_section_type": best_section["type"],
|
||||
"confidence": round(best_score, 2),
|
||||
"matched_text": best_section["text_content"][:200],
|
||||
})
|
||||
else:
|
||||
unmatched_rows.append({
|
||||
"row_index": ri,
|
||||
"row_y_center": row["y_center"],
|
||||
"ocr_texts": ocr_texts,
|
||||
})
|
||||
|
||||
# 生成描述
|
||||
desc_parts = []
|
||||
if matched_rows:
|
||||
desc_parts.append(f"图片中 {len(matched_rows)} 行匹配到当前 JRXML:")
|
||||
for m in matched_rows:
|
||||
desc_parts.append(
|
||||
f" - 图片第 {m['row_index']+1} 行 → JRXML「{m['jrxml_section']}」"
|
||||
f"({m['jrxml_section_type']},置信度 {m['confidence']})"
|
||||
)
|
||||
if unmatched_rows:
|
||||
desc_parts.append(f"图片中 {len(unmatched_rows)} 行未匹配到 JRXML 现有区域:")
|
||||
for u in unmatched_rows:
|
||||
texts = ", ".join(u["ocr_texts"][:3])
|
||||
desc_parts.append(f" - 图片第 {u['row_index']+1} 行:{texts}")
|
||||
|
||||
return {
|
||||
"matched": len(matched_rows) > 0,
|
||||
"matched_rows": matched_rows,
|
||||
"unmatched_rows": unmatched_rows,
|
||||
"description": "\n".join(desc_parts),
|
||||
}
|
||||
|
||||
|
||||
def analyze_and_inject(file_path: str, base_prompt: str,
|
||||
current_jrxml: str = "") -> str:
|
||||
"""分析布局并增强 prompt。
|
||||
|
||||
- 完整 A4 模板 → 全量布局描述
|
||||
- 行片段 + 有 JRXML → 行匹配 + 修改指引
|
||||
- 行片段 + 无 JRXML → 行片段描述(视为 A4 模板的一部分)
|
||||
"""
|
||||
result = analyze_layout(file_path)
|
||||
tt = result.get("template_type", "unknown")
|
||||
|
||||
if tt == "unknown":
|
||||
return base_prompt
|
||||
|
||||
if tt == "full_a4":
|
||||
return f"[图片模板分析 — 完整 A4 报表]\n{result['description']}\n\n---\n原始需求:\n{base_prompt}"
|
||||
|
||||
if tt == "partial_rows":
|
||||
if current_jrxml.strip():
|
||||
match = match_rows_to_jrxml(result, current_jrxml)
|
||||
if match["matched"]:
|
||||
return (
|
||||
f"[图片模板分析 — 行片段修改]\n"
|
||||
f"图片包含 {result['total_rows']} 行,视为 A4 模板的一部分。\n"
|
||||
f"{match['description']}\n\n"
|
||||
f"{result['description']}\n\n"
|
||||
f"---\n请根据以上匹配结果,修改 JRXML 中对应区域的布局:\n{base_prompt}"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"[图片模板分析 — 行片段(未匹配到现有区域)]\n"
|
||||
f"图片包含 {result['total_rows']} 行。\n"
|
||||
f"{result['description']}\n\n"
|
||||
f"---\n请根据以上行结构,在 JRXML 中找到合适位置进行修改:\n{base_prompt}"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"[图片模板分析 — 行片段(无现有报表,按 A4 模板处理)]\n"
|
||||
f"图片包含 {result['total_rows']} 行,请按 A4 报表模板的需求输出整张报表。\n"
|
||||
f"{result['description']}\n\n"
|
||||
f"---\n原始需求:\n{base_prompt}"
|
||||
)
|
||||
|
||||
return base_prompt
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JRXML 结构解析
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _parse_jrxml_sections(jrxml: str) -> list[dict]:
|
||||
"""解析 JRXML 中的 section/band 结构。
|
||||
|
||||
直接搜索所有 band 元素,通过上下文字符串推断其所属 section。
|
||||
"""
|
||||
sections = []
|
||||
try:
|
||||
root = ET.fromstring(jrxml)
|
||||
section_tags = {
|
||||
"title", "pageHeader", "columnHeader", "detail",
|
||||
"columnFooter", "pageFooter", "summary", "background",
|
||||
"noData", "groupHeader", "groupFooter",
|
||||
}
|
||||
|
||||
for section_elem in root.iter():
|
||||
stag = _tag(section_elem)
|
||||
if stag not in section_tags:
|
||||
continue
|
||||
|
||||
for child in section_elem:
|
||||
if _tag(child) == "band":
|
||||
name = child.get("name", "")
|
||||
section_name = f"{stag}[{name}]" if name else stag
|
||||
text_content = ET.tostring(child, encoding="unicode")
|
||||
sections.append({
|
||||
"name": section_name,
|
||||
"type": stag,
|
||||
"text_content": text_content,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: 如果 structured parsing 失败,直接把整个 JRXML 按 band 分割
|
||||
if not sections:
|
||||
sections = _parse_jrxml_regex(jrxml)
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def _tag(elem) -> str:
|
||||
"""去除命名空间前缀的标签名。"""
|
||||
return elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
|
||||
|
||||
|
||||
def _parse_jrxml_regex(jrxml: str) -> list[dict]:
|
||||
"""正则回退方案:直接在文本中搜索 band 块。"""
|
||||
sections = []
|
||||
band_pattern = re.compile(
|
||||
r'<(title|pageHeader|columnHeader|detail|columnFooter|pageFooter|summary|background|noData|groupHeader|groupFooter)>\s*'
|
||||
r'(<band[^>]*>.*?</band>)\s*'
|
||||
r'</\1>',
|
||||
re.DOTALL,
|
||||
)
|
||||
for m in band_pattern.finditer(jrxml):
|
||||
stag = m.group(1)
|
||||
band_xml = m.group(0)
|
||||
sections.append({
|
||||
"name": stag,
|
||||
"type": stag,
|
||||
"text_content": band_xml,
|
||||
})
|
||||
return sections
|
||||
|
||||
|
||||
def _text_similarity(ocr_texts: list[str], jrxml_text: str) -> float:
|
||||
"""计算 OCR 文本与 JRXML 文本的相似度(简单的词匹配)。"""
|
||||
if not ocr_texts or not jrxml_text:
|
||||
return 0.0
|
||||
|
||||
jrxml_lower = jrxml_text.lower()
|
||||
score = 0.0
|
||||
for text in ocr_texts:
|
||||
# 精确匹配
|
||||
if text.lower() in jrxml_lower:
|
||||
score += 1.0
|
||||
else:
|
||||
# 部分词匹配
|
||||
words = re.findall(r"\w+", text)
|
||||
matched = sum(1 for w in words if w.lower() in jrxml_lower)
|
||||
if words:
|
||||
score += matched / len(words) * 0.5
|
||||
|
||||
return min(score / len(ocr_texts), 1.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 内部实现(不变)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _load_image(path: Path) -> Optional[PIL.Image.Image]:
|
||||
suffix = path.suffix.lower()
|
||||
|
||||
if suffix in (".png", ".jpg", ".jpeg", ".bmp", ".webp"):
|
||||
try:
|
||||
return PIL.Image.open(path).convert("RGB")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if suffix == ".pdf":
|
||||
try:
|
||||
import pdfplumber
|
||||
with pdfplumber.open(path) as pdf:
|
||||
if pdf.pages:
|
||||
pil_img = pdf.pages[0].to_image(resolution=150)
|
||||
return pil_img.original.convert("RGB")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
import fitz
|
||||
doc = fitz.open(path)
|
||||
pix = doc[0].get_pixmap(dpi=150)
|
||||
img = PIL.Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
||||
doc.close()
|
||||
return img
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _ocr_elements(img: PIL.Image.Image, file_path: str) -> list[dict]:
|
||||
"""OCR 提取图片中的文字元素(位置+内容)。优先 EasyOCR,回退 PaddleOCR。"""
|
||||
|
||||
# 优先 PaddleOCR(精确识别)
|
||||
try:
|
||||
from paddleocr import PaddleOCR
|
||||
import numpy as np
|
||||
|
||||
ocr = PaddleOCR(lang="ch")
|
||||
result = ocr.ocr(np.array(img))
|
||||
|
||||
elements = []
|
||||
if result and result[0]:
|
||||
for line in result[0]:
|
||||
if len(line) < 2:
|
||||
continue
|
||||
box = line[0]
|
||||
text_info = line[1]
|
||||
text = text_info[0] if isinstance(text_info, (list, tuple)) else str(text_info)
|
||||
if not text.strip():
|
||||
continue
|
||||
|
||||
xs = [p[0] for p in box]
|
||||
ys = [p[1] for p in box]
|
||||
x_min, x_max = min(xs), max(xs)
|
||||
y_min, y_max = min(ys), max(ys)
|
||||
|
||||
elements.append({
|
||||
"x": round(x_min, 1),
|
||||
"y": round(y_min, 1),
|
||||
"w": round(x_max - x_min, 1),
|
||||
"h": round(y_max - y_min, 1),
|
||||
"font_size": round(y_max - y_min, 1),
|
||||
"text": text.strip(),
|
||||
})
|
||||
|
||||
elements.sort(key=lambda e: (e["y"], e["x"]))
|
||||
return elements
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 回退 EasyOCR
|
||||
try:
|
||||
import easyocr
|
||||
import numpy as np
|
||||
|
||||
reader = easyocr.Reader(["ch_sim", "en"], gpu=False, verbose=False)
|
||||
result = reader.readtext(np.array(img))
|
||||
|
||||
elements = []
|
||||
for (bbox, text, confidence) in result:
|
||||
if not text.strip():
|
||||
continue
|
||||
xs = [p[0] for p in bbox]
|
||||
ys = [p[1] for p in bbox]
|
||||
x_min, x_max = min(xs), max(xs)
|
||||
y_min, y_max = min(ys), max(ys)
|
||||
|
||||
elements.append({
|
||||
"x": round(x_min, 1),
|
||||
"y": round(y_min, 1),
|
||||
"w": round(x_max - x_min, 1),
|
||||
"h": round(y_max - y_min, 1),
|
||||
"font_size": round(y_max - y_min, 1),
|
||||
"text": text.strip(),
|
||||
})
|
||||
|
||||
elements.sort(key=lambda e: (e["y"], e["x"]))
|
||||
return elements
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def _group_into_rows(elements: list[dict], img_height: int,
|
||||
tolerance_ratio: float = 0.02) -> list[dict]:
|
||||
if not elements:
|
||||
return []
|
||||
|
||||
tolerance = img_height * tolerance_ratio
|
||||
rows = []
|
||||
current_row = [elements[0]]
|
||||
|
||||
for elem in elements[1:]:
|
||||
prev_cy = current_row[0]["y"] + current_row[0]["h"] / 2
|
||||
curr_cy = elem["y"] + elem["h"] / 2
|
||||
|
||||
if abs(curr_cy - prev_cy) < tolerance:
|
||||
current_row.append(elem)
|
||||
else:
|
||||
rows.append(_build_row(current_row))
|
||||
current_row = [elem]
|
||||
|
||||
if current_row:
|
||||
rows.append(_build_row(current_row))
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def _build_row(elements: list[dict]) -> dict:
|
||||
elements.sort(key=lambda e: e["x"])
|
||||
ys = [e["y"] for e in elements]
|
||||
return {"y_center": round(sum(ys) / len(ys), 1), "elements": elements}
|
||||
|
||||
|
||||
def _build_description(rows: list[dict], img_w: int, img_h: int,
|
||||
a4_confidence: str, template_type: str) -> str:
|
||||
if not rows:
|
||||
if template_type == "partial_rows":
|
||||
return f"图片 {img_w}x{img_h}(非 A4 比例),未检测到文字元素。"
|
||||
return f"图片共 {img_w}x{img_h} 像素,未检测到文字元素。"
|
||||
|
||||
lines = []
|
||||
if template_type == "full_a4":
|
||||
lines.append(f"图片为完整 A4 报表模板,共 {len(rows)} 行,像素区域 {img_w}x{img_h}:")
|
||||
elif template_type == "partial_rows":
|
||||
lines.append(f"图片为报表模板行片段(非完整 A4),包含 {len(rows)} 行,"
|
||||
f"像素区域 {img_w}x{img_h},请按 A4 模板处理:")
|
||||
else:
|
||||
lines.append(f"图片共 {img_w}x{img_h} 像素,包含 {len(rows)} 行文字:")
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
elems = row["elements"]
|
||||
lines.append(f"\n第 {i+1} 行有 {len(elems)} 个元素:")
|
||||
for j, e in enumerate(elems):
|
||||
letter = chr(ord("a") + j)
|
||||
lines.append(
|
||||
f" 元素 {letter}:位置(x={e['x']}, y={e['y']}),"
|
||||
f"长 {e['w']}px,高 {e['h']}px,"
|
||||
f"字体 {e['font_size']}px,"
|
||||
f"内容「{e['text']}」"
|
||||
)
|
||||
|
||||
if template_type == "full_a4":
|
||||
lines.append(f"\n请根据以上布局生成对应的 JRXML 报表模板。")
|
||||
elif template_type == "partial_rows":
|
||||
lines.append(f"\n请将以上 {len(rows)} 行作为 A4 模板的一部分,"
|
||||
f"生成或修改对应的 JRXML 报表区域。")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _empty_result(error: str = "") -> dict:
|
||||
return {
|
||||
"is_a4_template": False,
|
||||
"is_partial": False,
|
||||
"template_type": "unknown",
|
||||
"image_size": (0, 0),
|
||||
"aspect_ratio": 0,
|
||||
"a4_confidence": "not_a4",
|
||||
"rows": [],
|
||||
"description": error,
|
||||
"total_rows": 0,
|
||||
"total_elements": 0,
|
||||
}
|
||||
+221
-18
@@ -1,62 +1,265 @@
|
||||
"""大语言模型工厂:支持 OpenAI 兼容的云端 API、Anthropic 兼容 API 和本地 Ollama。"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
from backend.logger import get_logger
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
_llm_log = get_logger("llm")
|
||||
|
||||
|
||||
def get_llm():
|
||||
class _BaseLLM:
|
||||
"""LLM 统一接口基类 — 所有后端都提供 invoke() 和 stream()。"""
|
||||
|
||||
def invoke(self, prompt: str) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
def stream(self, prompt: str):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _LLMLoggingWrapper(_BaseLLM):
|
||||
"""包装任何 LLM 后端,自动记录输入/输出到 llm.log。"""
|
||||
|
||||
def __init__(self, inner: _BaseLLM, model: str, backend: str, caller: str = ""):
|
||||
self._inner = inner
|
||||
self._model = model
|
||||
self._backend = backend
|
||||
self._caller = caller
|
||||
|
||||
def invoke(self, prompt: str) -> Any:
|
||||
t0 = time.time()
|
||||
prompt_len = len(prompt)
|
||||
_llm_log.debug(
|
||||
"LLM invoke 请求",
|
||||
extra={
|
||||
"direction": "request",
|
||||
"model": self._model,
|
||||
"backend": self._backend,
|
||||
"caller": self._caller,
|
||||
"prompt_length": prompt_len,
|
||||
"prompt_preview": prompt[:500],
|
||||
},
|
||||
)
|
||||
try:
|
||||
result = self._inner.invoke(prompt)
|
||||
elapsed = round((time.time() - t0) * 1000)
|
||||
content = getattr(result, "content", str(result))
|
||||
resp_len = len(content)
|
||||
resp_preview = content[:500]
|
||||
_llm_log.info(
|
||||
"LLM invoke 完成",
|
||||
extra={
|
||||
"direction": "response",
|
||||
"model": self._model,
|
||||
"backend": self._backend,
|
||||
"caller": self._caller,
|
||||
"duration_ms": elapsed,
|
||||
"response_length": resp_len,
|
||||
"response_preview": resp_preview,
|
||||
},
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
elapsed = round((time.time() - t0) * 1000)
|
||||
_llm_log.error(
|
||||
"LLM invoke 异常",
|
||||
extra={
|
||||
"direction": "error",
|
||||
"model": self._model,
|
||||
"backend": self._backend,
|
||||
"caller": self._caller,
|
||||
"duration_ms": elapsed,
|
||||
"error": str(e),
|
||||
"prompt_preview": prompt[:500],
|
||||
},
|
||||
)
|
||||
raise
|
||||
|
||||
def stream(self, prompt: str):
|
||||
t0 = time.time()
|
||||
prompt_len = len(prompt)
|
||||
prompt_preview = prompt[:500]
|
||||
_llm_log.debug(
|
||||
"LLM stream 请求",
|
||||
extra={
|
||||
"direction": "request",
|
||||
"model": self._model,
|
||||
"backend": self._backend,
|
||||
"caller": self._caller,
|
||||
"prompt_length": prompt_len,
|
||||
"prompt_preview": prompt[:500],
|
||||
},
|
||||
)
|
||||
full = []
|
||||
try:
|
||||
for chunk in self._inner.stream(prompt):
|
||||
full.append(chunk)
|
||||
yield chunk
|
||||
elapsed = round((time.time() - t0) * 1000)
|
||||
resp_text = "".join(full)
|
||||
resp_len = len(resp_text)
|
||||
resp_preview = resp_text[:500]
|
||||
stop_reason = getattr(self._inner, '_last_stop_reason', None)
|
||||
self._last_stop_reason = stop_reason
|
||||
if stop_reason == "max_tokens":
|
||||
_llm_log.warning(
|
||||
"LLM stream 截断 (max_tokens),输出可能不完整",
|
||||
extra={
|
||||
"direction": "response",
|
||||
"model": self._model,
|
||||
"backend": self._backend,
|
||||
"caller": self._caller,
|
||||
"duration_ms": elapsed,
|
||||
"response_length": resp_len,
|
||||
"stop_reason": stop_reason,
|
||||
},
|
||||
)
|
||||
else:
|
||||
_llm_log.info(
|
||||
"LLM stream 完成",
|
||||
extra={
|
||||
"direction": "response",
|
||||
"model": self._model,
|
||||
"backend": self._backend,
|
||||
"caller": self._caller,
|
||||
"duration_ms": elapsed,
|
||||
"response_length": resp_len,
|
||||
"response_preview": resp_preview,
|
||||
"stop_reason": stop_reason,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
elapsed = round((time.time() - t0) * 1000)
|
||||
_llm_log.error(
|
||||
"LLM stream 异常",
|
||||
extra={
|
||||
"direction": "error",
|
||||
"model": self._model,
|
||||
"backend": self._backend,
|
||||
"caller": self._caller,
|
||||
"duration_ms": elapsed,
|
||||
"error": str(e),
|
||||
"prompt_preview": prompt[:500],
|
||||
},
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
DEFAULT_MAX_TOKENS = int(os.getenv("LLM_MAX_TOKENS", "8192"))
|
||||
|
||||
|
||||
def _build_raw_llm(caller: str = "", max_tokens: int | None = None) -> tuple[_BaseLLM, str, str]:
|
||||
"""构造原始 LLM 实例,返回 (实例, model名, backend名)。
|
||||
|
||||
max_tokens: 覆盖默认输出 token 数。None 使用 LLM_MAX_TOKENS 环境变量或 8192。
|
||||
"""
|
||||
backend = os.getenv("LLM_BACKEND", "cloud")
|
||||
if backend == "local":
|
||||
from langchain_ollama import ChatOllama
|
||||
|
||||
model = os.getenv("LOCAL_LLM_MODEL", "qwen2.5-coder:7b")
|
||||
return ChatOllama(model=model, temperature=0.1)
|
||||
raw = ChatOllama(model=model, temperature=0.1)
|
||||
|
||||
class OllamaWrapper(_BaseLLM):
|
||||
def invoke(self, prompt):
|
||||
return raw.invoke(prompt)
|
||||
|
||||
def stream(self, prompt):
|
||||
for chunk in raw.stream(prompt):
|
||||
yield chunk.content
|
||||
|
||||
return OllamaWrapper(), model, f"local/{model}"
|
||||
|
||||
provider = os.getenv("LLM_PROVIDER", "openai")
|
||||
if provider == "anthropic":
|
||||
from anthropic import Anthropic
|
||||
|
||||
api_key = os.getenv("OPENAI_API_KEY", "")
|
||||
base_url = os.getenv("OPENAI_BASE_URL", "https://api.minimaxi.com/anthropic")
|
||||
model = os.getenv("LLM_MODEL", "minimax-2.7")
|
||||
api_key = os.getenv("ANTHROPIC_API_KEY") or os.getenv("OPENAI_API_KEY", "")
|
||||
base_url = os.getenv("ANTHROPIC_BASE_URL") or os.getenv("OPENAI_BASE_URL", "https://api.minimaxi.com/anthropic")
|
||||
model = os.getenv("LLM_MODEL", "MiniMax-M2.7")
|
||||
temperature = 0.1
|
||||
max_tokens = 4096
|
||||
_default_max_tokens = max_tokens if max_tokens is not None else DEFAULT_MAX_TOKENS
|
||||
|
||||
os.environ["NO_PROXY"] = "*"
|
||||
client = Anthropic(api_key=api_key, base_url=base_url, timeout=120)
|
||||
|
||||
client = Anthropic(base_url=base_url, timeout=120)
|
||||
class MiniMaxLLM(_BaseLLM):
|
||||
def __init__(self):
|
||||
self._last_stop_reason = None
|
||||
self._max_tokens = _default_max_tokens
|
||||
|
||||
class MiniMaxLLM:
|
||||
def invoke(self, prompt: str) -> Any:
|
||||
resp = client.messages.create(
|
||||
model=model,
|
||||
max_tokens=max_tokens,
|
||||
max_tokens=self._max_tokens,
|
||||
temperature=temperature,
|
||||
messages=[{"role": "user", "content": [{"type": "text", "text": prompt}]}],
|
||||
)
|
||||
for block in resp.content:
|
||||
if block.type == "text":
|
||||
block_type = getattr(block, "type", "")
|
||||
if block_type == "text":
|
||||
return type("Response", (), {"content": block.text})()
|
||||
return type("Response", (), {"content": ""})()
|
||||
|
||||
def get_num_tokens(self, text: str) -> int:
|
||||
return client.count_tokens(text)
|
||||
def stream(self, prompt: str):
|
||||
self._last_stop_reason = None
|
||||
with client.messages.stream(
|
||||
model=model,
|
||||
max_tokens=self._max_tokens,
|
||||
temperature=temperature,
|
||||
messages=[{"role": "user", "content": [{"type": "text", "text": prompt}]}],
|
||||
) as s:
|
||||
for text in s.text_stream:
|
||||
yield text
|
||||
try:
|
||||
final_msg = s.get_final_message()
|
||||
self._last_stop_reason = getattr(final_msg, 'stop_reason', None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return MiniMaxLLM()
|
||||
def get_num_tokens(self, text: str) -> int:
|
||||
resp = client.messages.count_tokens(
|
||||
model=model,
|
||||
messages=[{"role": "user", "content": [{"type": "text", "text": text}]}],
|
||||
)
|
||||
return resp.input_tokens
|
||||
|
||||
return MiniMaxLLM(), model, f"cloud/anthropic/{model}"
|
||||
else:
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
return ChatOpenAI(
|
||||
model=os.getenv("LLM_MODEL", "gpt-4o"),
|
||||
model = os.getenv("LLM_MODEL", "gpt-4o")
|
||||
raw = ChatOpenAI(
|
||||
model=model,
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
class OpenAIWrapper(_BaseLLM):
|
||||
def invoke(self, prompt):
|
||||
return raw.invoke(prompt)
|
||||
|
||||
def stream(self, prompt):
|
||||
for chunk in raw.stream(prompt):
|
||||
yield chunk.content
|
||||
|
||||
return OpenAIWrapper(), model, f"cloud/openai/{model}"
|
||||
|
||||
|
||||
def get_llm(caller: str = "", max_tokens: int | None = None) -> _BaseLLM:
|
||||
"""返回带日志的 LLM 实例。caller 用于标识调用来源(如 generate、classify_intent)。
|
||||
|
||||
max_tokens: 覆盖默认输出 token 数。用于骨架生成等需要大量输出的节点。
|
||||
"""
|
||||
inner, model, backend = _build_raw_llm(caller, max_tokens=max_tokens)
|
||||
return _LLMLoggingWrapper(inner, model=model, backend=backend, caller=caller)
|
||||
|
||||
|
||||
def get_llm_for_correction():
|
||||
return get_llm()
|
||||
return get_llm(caller="correction")
|
||||
@@ -0,0 +1,167 @@
|
||||
"""集中日志模块。
|
||||
|
||||
提供:
|
||||
- 结构化 JSON 日志(每行一条记录)
|
||||
- 请求级 trace_id(通过 contextvars 自动传播)
|
||||
- 独立的 LLM 调用日志文件
|
||||
- 日志轮转(按大小 10MB,保留 5 个备份)
|
||||
|
||||
用法:
|
||||
from backend.logger import get_logger, set_trace_id
|
||||
|
||||
# 业务日志
|
||||
log = get_logger("agent")
|
||||
log.info("节点开始执行", extra={"node": "classify_intent", "session_id": "xxx"})
|
||||
|
||||
# LLM 日志
|
||||
llm_log = get_logger("llm")
|
||||
llm_log.info("LLM 请求", extra={"prompt": "...", "model": "gpt-4o"})
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from contextvars import ContextVar
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG_DIR = Path(os.getenv("LOG_DIR", "./logs"))
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG")
|
||||
LLM_LOG_FILE = "llm.log"
|
||||
APP_LOG_FILE = "app.log"
|
||||
|
||||
CHINA_TZ = timezone(timedelta(hours=8))
|
||||
|
||||
_trace_id_var: ContextVar[str] = ContextVar("trace_id", default="")
|
||||
|
||||
|
||||
def generate_trace_id() -> str:
|
||||
return uuid.uuid4().hex[:16]
|
||||
|
||||
|
||||
def get_trace_id() -> str:
|
||||
tid = _trace_id_var.get()
|
||||
if not tid:
|
||||
tid = generate_trace_id()
|
||||
_trace_id_var.set(tid)
|
||||
return tid
|
||||
|
||||
|
||||
def set_trace_id(trace_id: str):
|
||||
_trace_id_var.set(trace_id)
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
"""将日志记录格式化为单行 JSON,便于后续分析。
|
||||
|
||||
LogRecord 标准属性的键(不放入 extra)。
|
||||
通过 logging.Logger.debug(msg, extra={...}) 传入的键会自动设为
|
||||
LogRecord 属性,由本格式化器收集到 extra 字段中。
|
||||
"""
|
||||
|
||||
_STANDARD_ATTRS: set[str] = frozenset({
|
||||
"args", "asctime", "created", "exc_info", "exc_text", "filename",
|
||||
"funcName", "levelname", "levelno", "lineno", "module", "msecs",
|
||||
"message", "msg", "name", "pathname", "process", "processName",
|
||||
"relativeCreated", "stack_info", "thread", "threadName",
|
||||
"extra_fields", "taskName",
|
||||
})
|
||||
|
||||
def _collect_extra(self, record: logging.LogRecord) -> dict:
|
||||
"""从 LogRecord 上收集非标准属性 → 合并为 extra dict。"""
|
||||
extra = dict(getattr(record, "extra_fields", {}))
|
||||
for key, val in record.__dict__.items():
|
||||
if key not in self._STANDARD_ATTRS and not key.startswith("_"):
|
||||
extra[key] = val
|
||||
return extra
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
log_entry = {
|
||||
"timestamp": datetime.now(CHINA_TZ).isoformat(),
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"trace_id": get_trace_id(),
|
||||
"message": record.getMessage(),
|
||||
"module": record.module,
|
||||
"function": record.funcName,
|
||||
"line": record.lineno,
|
||||
}
|
||||
|
||||
extra = self._collect_extra(record)
|
||||
if extra:
|
||||
log_entry["extra"] = extra
|
||||
|
||||
if record.exc_info and record.exc_info[0]:
|
||||
import traceback
|
||||
log_entry["exception"] = traceback.format_exception(
|
||||
record.exc_info[0], record.exc_info[1], record.exc_info[2]
|
||||
)
|
||||
|
||||
return json.dumps(log_entry, ensure_ascii=False)
|
||||
|
||||
|
||||
def _create_handler(filename: str, level: int) -> RotatingFileHandler:
|
||||
handler = RotatingFileHandler(
|
||||
filename=str(LOG_DIR / filename),
|
||||
maxBytes=10 * 1024 * 1024,
|
||||
backupCount=5,
|
||||
encoding="utf-8",
|
||||
)
|
||||
handler.setLevel(level)
|
||||
handler.setFormatter(JsonFormatter())
|
||||
return handler
|
||||
|
||||
|
||||
def _get_level() -> int:
|
||||
return getattr(logging, LOG_LEVEL.upper(), logging.DEBUG)
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""获取指定名称的 logger,自动配置了 JSON 格式化 + 文件轮转。
|
||||
|
||||
name="llm" → 输出到 logs/llm.log(仅 LLM 调用相关)
|
||||
其他 name → 输出到 logs/app.log
|
||||
"""
|
||||
logger = logging.getLogger(f"jrxml.{name}")
|
||||
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
level = _get_level()
|
||||
logger.setLevel(level)
|
||||
logger.propagate = False
|
||||
|
||||
if name == "llm":
|
||||
logger.addHandler(_create_handler(LLM_LOG_FILE, level))
|
||||
else:
|
||||
logger.addHandler(_create_handler(APP_LOG_FILE, level))
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
class _ExtraAdapter(logging.LoggerAdapter):
|
||||
"""支持通过 adapter.extra 合并 extra 字段的适配器。"""
|
||||
|
||||
def process(self, msg, kwargs):
|
||||
extra = kwargs.pop("extra", {})
|
||||
merged = {**self.extra, **extra} if self.extra or extra else None
|
||||
if merged:
|
||||
kwargs["extra"] = {"extra_fields": merged}
|
||||
return msg, kwargs
|
||||
|
||||
|
||||
def get_trace_logger(name: str) -> _ExtraAdapter:
|
||||
"""返回一个自动附带 trace_id 的 logger 适配器。
|
||||
|
||||
用法:
|
||||
log = get_trace_logger("agent")
|
||||
log.info("节点完成", extra={"node": "generate"})
|
||||
"""
|
||||
logger = get_logger(name)
|
||||
return _ExtraAdapter(logger, {"trace_id": get_trace_id()})
|
||||
@@ -0,0 +1,911 @@
|
||||
"""OCR 单据字段精确提取器。
|
||||
|
||||
两阶段提取流程:
|
||||
阶段1 - 文档分析: 复用 file_parser.parse_file() 和 layout_analyzer.analyze_layout()
|
||||
获取每个文本元素的精确坐标和内容
|
||||
阶段2 - 字段提取: 给定目标字段列表,通过四种策略(精确KV匹配、模糊KV匹配、
|
||||
正则模式匹配、表格结构匹配)提取字段值、位置和置信度
|
||||
|
||||
用法:
|
||||
from backend.ocr_extractor import OcrExtractor
|
||||
|
||||
extractor = OcrExtractor()
|
||||
result = extractor.extract("invoice.png", ["发票代码", "发票号码", "合计金额"])
|
||||
for field in result:
|
||||
print(f"{field['field_name']}: {field['field_value']} (置信度: {field['confidence']})")
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
OCR_USE_GPU = os.getenv("OCR_USE_GPU", "false").lower() in ("true", "1", "yes")
|
||||
OCR_CONFIDENCE_THRESHOLD = float(os.getenv("OCR_CONFIDENCE_THRESHOLD", "0.5"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class OcrTextElement:
|
||||
"""OCR 文本元素,包含精确坐标和内容。"""
|
||||
|
||||
text: str
|
||||
x_min: float
|
||||
y_min: float
|
||||
x_max: float
|
||||
y_max: float
|
||||
confidence: float = 1.0
|
||||
|
||||
@property
|
||||
def center_x(self) -> float:
|
||||
return (self.x_min + self.x_max) / 2
|
||||
|
||||
@property
|
||||
def center_y(self) -> float:
|
||||
return (self.y_min + self.y_max) / 2
|
||||
|
||||
@property
|
||||
def width(self) -> float:
|
||||
return self.x_max - self.x_min
|
||||
|
||||
@property
|
||||
def height(self) -> float:
|
||||
return self.y_max - self.y_min
|
||||
|
||||
@property
|
||||
def bbox(self) -> list[float]:
|
||||
return [self.x_min, self.y_min, self.x_max, self.y_max]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExtractedField:
|
||||
"""提取的字段结果。"""
|
||||
|
||||
field_name: str
|
||||
field_value: str
|
||||
bbox: list[float]
|
||||
confidence: float
|
||||
extraction_method: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExtractionResult:
|
||||
"""单次提取的完整结果。"""
|
||||
|
||||
file_path: str
|
||||
image_size: tuple[int, int]
|
||||
fields: list[ExtractedField] = field(default_factory=list)
|
||||
all_elements: list[OcrTextElement] = field(default_factory=list)
|
||||
errors: list[str] = field(default_factory=list)
|
||||
ocr_available: bool = False
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"file_path": self.file_path,
|
||||
"image_size": self.image_size,
|
||||
"ocr_available": self.ocr_available,
|
||||
"fields": [
|
||||
{
|
||||
"field_name": f.field_name,
|
||||
"field_value": f.field_value,
|
||||
"bbox": f.bbox,
|
||||
"confidence": f.confidence,
|
||||
"extraction_method": f.extraction_method,
|
||||
}
|
||||
for f in self.fields
|
||||
],
|
||||
"all_elements": [
|
||||
{
|
||||
"text": e.text,
|
||||
"bbox": e.bbox,
|
||||
"confidence": e.confidence,
|
||||
}
|
||||
for e in self.all_elements
|
||||
],
|
||||
"total_elements": len(self.all_elements),
|
||||
"errors": self.errors,
|
||||
}
|
||||
|
||||
|
||||
class OcrExtractor:
|
||||
"""OCR 单据字段精确提取器。
|
||||
|
||||
两阶段流水线:
|
||||
阶段1: 对上传图片进行 OCR + 版面分析,产出带坐标的文本元素列表
|
||||
阶段2: 根据目标字段列表,按优先级逐一尝试四种提取策略
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
use_gpu: bool = False,
|
||||
confidence_threshold: float = 0.5,
|
||||
):
|
||||
"""初始化提取器。
|
||||
|
||||
Args:
|
||||
use_gpu: 是否使用 GPU 加速 OCR(需要相应驱动)
|
||||
confidence_threshold: OCR 文本置信度最低阈值,低于此值的元素被忽略
|
||||
"""
|
||||
self.use_gpu = use_gpu if use_gpu else OCR_USE_GPU
|
||||
self.confidence_threshold = (
|
||||
confidence_threshold
|
||||
if confidence_threshold != 0.5
|
||||
else OCR_CONFIDENCE_THRESHOLD
|
||||
)
|
||||
|
||||
# ========================================================================
|
||||
# 公共接口
|
||||
# ========================================================================
|
||||
|
||||
def extract(
|
||||
self,
|
||||
file_path: str,
|
||||
target_fields: Optional[list[str]] = None,
|
||||
) -> dict:
|
||||
"""执行两阶段 OCR 字段提取。
|
||||
|
||||
Args:
|
||||
file_path: 图片文件路径(支持 png/jpg/jpeg/bmp/webp)
|
||||
target_fields: 需要提取的字段名称列表。为空或 None 时自动发现文档中所有键值对。
|
||||
|
||||
Returns:
|
||||
提取结果字典,格式见 ExtractionResult.to_dict()
|
||||
"""
|
||||
result = ExtractionResult(file_path=file_path, image_size=(0, 0))
|
||||
|
||||
if not Path(file_path).exists():
|
||||
result.errors.append(f"文件不存在: {file_path}")
|
||||
return result.to_dict()
|
||||
|
||||
elements, image_size = self._analyze_document(file_path)
|
||||
result.image_size = image_size
|
||||
result.all_elements = elements
|
||||
|
||||
if not elements:
|
||||
result.ocr_available = self._check_ocr_availability()
|
||||
if not result.ocr_available:
|
||||
result.errors.append(
|
||||
"OCR 引擎不可用,请安装 easyocr (pip install easyocr) 或 paddleocr"
|
||||
)
|
||||
else:
|
||||
result.errors.append("图片未检测到文字元素")
|
||||
return result.to_dict()
|
||||
|
||||
result.ocr_available = True
|
||||
|
||||
if target_fields:
|
||||
# 有预设字段名:按名单查找
|
||||
for field_name in target_fields:
|
||||
extracted = self._extract_field(field_name, elements)
|
||||
if extracted:
|
||||
result.fields.append(extracted)
|
||||
else:
|
||||
result.fields.append(
|
||||
ExtractedField(
|
||||
field_name=field_name,
|
||||
field_value="",
|
||||
bbox=[],
|
||||
confidence=0.0,
|
||||
extraction_method="none",
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 无预设字段名:自动发现文档中所有键值对
|
||||
discovered = self._discover_fields(elements)
|
||||
for field in discovered:
|
||||
extracted = self._extract_field(field, elements)
|
||||
if extracted:
|
||||
result.fields.append(extracted)
|
||||
else:
|
||||
result.fields.append(
|
||||
ExtractedField(
|
||||
field_name=field,
|
||||
field_value="",
|
||||
bbox=[],
|
||||
confidence=0.0,
|
||||
extraction_method="none",
|
||||
)
|
||||
)
|
||||
|
||||
return result.to_dict()
|
||||
|
||||
def extract_from_layout_result(
|
||||
self,
|
||||
layout_result: dict,
|
||||
target_fields: list[str],
|
||||
) -> dict:
|
||||
"""直接从 layout_analyzer.analyze_layout() 的结果中提取字段。
|
||||
|
||||
当已有版面分析结果时,跳过阶段1的重复 OCR,直接进入阶段2。
|
||||
|
||||
Args:
|
||||
layout_result: analyze_layout() 的返回值
|
||||
target_fields: 需要提取的字段名称列表
|
||||
|
||||
Returns:
|
||||
提取结果字典
|
||||
"""
|
||||
rows = layout_result.get("rows", [])
|
||||
if not rows:
|
||||
return ExtractionResult(
|
||||
file_path="(from layout)",
|
||||
image_size=layout_result.get("image_size", (0, 0)),
|
||||
errors=["版面分析结果中没有文本行"],
|
||||
).to_dict()
|
||||
|
||||
elements = []
|
||||
for row in rows:
|
||||
for elem_data in row.get("elements", []):
|
||||
elements.append(
|
||||
OcrTextElement(
|
||||
text=elem_data.get("text", ""),
|
||||
x_min=elem_data.get("x", 0),
|
||||
y_min=elem_data.get("y", 0),
|
||||
x_max=elem_data.get("x", 0) + elem_data.get("w", 0),
|
||||
y_max=elem_data.get("y", 0) + elem_data.get("h", 0),
|
||||
)
|
||||
)
|
||||
|
||||
result = ExtractionResult(
|
||||
file_path="(from layout)",
|
||||
image_size=layout_result.get("image_size", (0, 0)),
|
||||
all_elements=elements,
|
||||
ocr_available=True,
|
||||
)
|
||||
|
||||
for field_name in target_fields:
|
||||
extracted = self._extract_field(field_name, elements)
|
||||
if extracted:
|
||||
result.fields.append(extracted)
|
||||
else:
|
||||
result.fields.append(
|
||||
ExtractedField(
|
||||
field_name=field_name,
|
||||
field_value="",
|
||||
bbox=[],
|
||||
confidence=0.0,
|
||||
extraction_method="none",
|
||||
)
|
||||
)
|
||||
|
||||
return result.to_dict()
|
||||
|
||||
# ========================================================================
|
||||
# 阶段1: 文档分析
|
||||
# ========================================================================
|
||||
|
||||
def _analyze_document(self, file_path: str) -> tuple[list[OcrTextElement], tuple[int, int]]:
|
||||
"""阶段1: OCR + 版面分析,产出带坐标的文本元素列表。"""
|
||||
from backend.layout_analyzer import _load_image, _ocr_elements
|
||||
|
||||
img = _load_image(Path(file_path))
|
||||
if img is None:
|
||||
return [], (0, 0)
|
||||
|
||||
image_size = img.size
|
||||
raw_elements = self._ocr_elements_enhanced(img, file_path)
|
||||
|
||||
elements = []
|
||||
for e_data in raw_elements:
|
||||
if e_data.get("confidence", 1.0) < self.confidence_threshold:
|
||||
continue
|
||||
elements.append(
|
||||
OcrTextElement(
|
||||
text=e_data.get("text", ""),
|
||||
x_min=e_data.get("x", 0),
|
||||
y_min=e_data.get("y", 0),
|
||||
x_max=e_data.get("x", 0) + e_data.get("w", 0),
|
||||
y_max=e_data.get("y", 0) + e_data.get("h", 0),
|
||||
confidence=e_data.get("confidence", 1.0),
|
||||
)
|
||||
)
|
||||
|
||||
elements.sort(key=lambda e: (e.y_min, e.x_min))
|
||||
return elements, image_size
|
||||
|
||||
def _ocr_elements_enhanced(self, img, file_path: str) -> list[dict]:
|
||||
"""增强版 OCR,返回带置信度的元素列表。"""
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
paddleocr_result = self._try_paddleocr(img, file_path)
|
||||
if paddleocr_result:
|
||||
return paddleocr_result
|
||||
|
||||
easyocr_result = self._try_easyocr(np.array(img))
|
||||
if easyocr_result:
|
||||
return easyocr_result
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return []
|
||||
|
||||
def _try_easyocr(self, np_img) -> Optional[list[dict]]:
|
||||
try:
|
||||
import easyocr
|
||||
|
||||
reader = easyocr.Reader(
|
||||
["ch_sim", "en"],
|
||||
gpu=self.use_gpu,
|
||||
verbose=False,
|
||||
)
|
||||
raw_result = reader.readtext(np_img)
|
||||
|
||||
elements = []
|
||||
for bbox, text, confidence in raw_result:
|
||||
if not text.strip():
|
||||
continue
|
||||
xs = [p[0] for p in bbox]
|
||||
ys = [p[1] for p in bbox]
|
||||
x_min, x_max = min(xs), max(xs)
|
||||
y_min, y_max = min(ys), max(ys)
|
||||
|
||||
elements.append({
|
||||
"x": round(x_min, 1),
|
||||
"y": round(y_min, 1),
|
||||
"w": round(x_max - x_min, 1),
|
||||
"h": round(y_max - y_min, 1),
|
||||
"text": text.strip(),
|
||||
"confidence": round(confidence, 4),
|
||||
})
|
||||
|
||||
elements.sort(key=lambda e: (e["y"], e["x"]))
|
||||
return elements
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _try_paddleocr(self, img, file_path: str) -> Optional[list[dict]]:
|
||||
try:
|
||||
from paddleocr import PaddleOCR
|
||||
import numpy as np
|
||||
|
||||
ocr = PaddleOCR(lang="ch")
|
||||
raw_result = ocr.ocr(np.array(img))
|
||||
|
||||
elements = []
|
||||
if raw_result and raw_result[0]:
|
||||
for line in raw_result[0]:
|
||||
if len(line) < 2:
|
||||
continue
|
||||
box = line[0]
|
||||
text_info = line[1]
|
||||
|
||||
if isinstance(text_info, (list, tuple)):
|
||||
text = text_info[0]
|
||||
confidence = text_info[1] if len(text_info) > 1 else 1.0
|
||||
else:
|
||||
text = str(text_info)
|
||||
confidence = 1.0
|
||||
|
||||
if not text.strip():
|
||||
continue
|
||||
|
||||
xs = [p[0] for p in box]
|
||||
ys = [p[1] for p in box]
|
||||
x_min, x_max = min(xs), max(xs)
|
||||
y_min, y_max = min(ys), max(ys)
|
||||
|
||||
elements.append({
|
||||
"x": round(x_min, 1),
|
||||
"y": round(y_min, 1),
|
||||
"w": round(x_max - x_min, 1),
|
||||
"h": round(y_max - y_min, 1),
|
||||
"text": text.strip(),
|
||||
"confidence": round(float(confidence), 4),
|
||||
})
|
||||
|
||||
elements.sort(key=lambda e: (e["y"], e["x"]))
|
||||
return elements
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _check_ocr_availability(self) -> bool:
|
||||
try:
|
||||
import easyocr
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
import paddleocr
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
return False
|
||||
|
||||
# ========================================================================
|
||||
# 阶段2: 字段精确提取
|
||||
# ========================================================================
|
||||
|
||||
def _discover_fields(self, elements: list[OcrTextElement]) -> list[str]:
|
||||
"""自动发现文档中的字段名(无需预设字段列表)。
|
||||
|
||||
策略:
|
||||
1. 单元素内"标签: 值"模式 — 从中提取标签
|
||||
2. 同行相邻键值对 — 短文本(标签) + 长文本(值)
|
||||
3. 表头行 — 首行/第二行的文本作为列字段名
|
||||
"""
|
||||
separators = [":", ":", "=", "—"]
|
||||
discovered: set[str] = set()
|
||||
elements_sorted = sorted(elements, key=lambda e: (e.y_min, e.x_min))
|
||||
|
||||
# 策略 1: 单元素内嵌键值对
|
||||
for elem in elements:
|
||||
text = elem.text
|
||||
for sep in separators:
|
||||
if sep in text:
|
||||
parts = text.split(sep, 1)
|
||||
label = parts[0].strip()
|
||||
value = parts[1].strip()
|
||||
if label and value and len(label) <= 20 and label != value:
|
||||
discovered.add(label)
|
||||
|
||||
# 策略 2: 同行相邻键值对(标签在左,值在右)
|
||||
# 按行分组
|
||||
rows: dict[int, list[OcrTextElement]] = {}
|
||||
for elem in elements_sorted:
|
||||
row_key = int(elem.y_min)
|
||||
for existing_key in list(rows.keys()):
|
||||
if abs(int(elem.y_min) - existing_key) < 10:
|
||||
row_key = existing_key
|
||||
break
|
||||
if row_key not in rows:
|
||||
rows[row_key] = []
|
||||
rows[row_key].append(elem)
|
||||
|
||||
for row_elems in rows.values():
|
||||
row_elems.sort(key=lambda e: e.x_min)
|
||||
for i in range(len(row_elems) - 1):
|
||||
left = row_elems[i]
|
||||
right = row_elems[i + 1]
|
||||
# 左边是短文本(可能标签),右边是相邻的正常文本(可能值)
|
||||
if (len(left.text) <= 15 and len(right.text) > 0
|
||||
and abs(right.x_min - left.x_max) < left.width * 3):
|
||||
# 左边不含仅数字/金额模式(这些更可能是值)
|
||||
if not re.match(r'^[\d,.]+\s*%?$', left.text.strip()):
|
||||
discovered.add(left.text.strip())
|
||||
|
||||
# 策略 3: 表头行 — 取前两行中较短的元素作为字段名候选
|
||||
sorted_row_keys = sorted(rows.keys())
|
||||
header_rows = sorted_row_keys[:min(3, len(sorted_row_keys))]
|
||||
for row_key in header_rows:
|
||||
for elem in rows.get(row_key, []):
|
||||
text = elem.text.strip()
|
||||
if text and len(text) <= 20 and not re.match(r'^[\d,.]+\s*%?$', text):
|
||||
discovered.add(text)
|
||||
|
||||
# 去重合并:移除值文本中误识别为标签的条目
|
||||
# 排除纯数字、日期、金额等明显是值的文本
|
||||
value_patterns = [
|
||||
r'^\d{1,2}[月/-]\d{1,2}[日/-]?\d{0,4}$',
|
||||
r'^[\d,]+\.?\d*\s*%?$',
|
||||
r'^[¥¥]\s*[\d,]+\.?\d*$',
|
||||
r'^\d{3,}$',
|
||||
]
|
||||
filtered = set()
|
||||
for name in discovered:
|
||||
is_value = False
|
||||
for pat in value_patterns:
|
||||
if re.match(pat, name):
|
||||
is_value = True
|
||||
break
|
||||
if not is_value:
|
||||
filtered.add(name)
|
||||
|
||||
return sorted(filtered)
|
||||
|
||||
def _extract_field(
|
||||
self,
|
||||
field_name: str,
|
||||
elements: list[OcrTextElement],
|
||||
) -> Optional[ExtractedField]:
|
||||
"""按优先级尝试四种策略提取单个字段。
|
||||
|
||||
策略优先级:
|
||||
1. 精确键值对匹配
|
||||
2. 模糊键值对匹配
|
||||
3. 正则模式匹配
|
||||
4. 表格结构匹配
|
||||
"""
|
||||
strategies = [
|
||||
("exact_match", self._exact_kv_match),
|
||||
("kv_pair", self._fuzzy_kv_match),
|
||||
("regex", self._regex_match),
|
||||
("table_match", self._table_match),
|
||||
]
|
||||
|
||||
for method_name, strategy_fn in strategies:
|
||||
result = strategy_fn(field_name, elements)
|
||||
if result and result.field_value:
|
||||
result.extraction_method = method_name
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 策略1: 精确键值对匹配
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _exact_kv_match(
|
||||
self,
|
||||
field_name: str,
|
||||
elements: list[OcrTextElement],
|
||||
) -> Optional[ExtractedField]:
|
||||
"""精确键值对匹配: 识别"字段名: 值"或"字段名:值"模式。
|
||||
|
||||
在同一文本元素中查找 "字段名" 后紧跟分隔符 + "值" 的模式。
|
||||
如 OCR 识别出 "发票代码: 12345678" 这一整个元素。
|
||||
"""
|
||||
separators = [":", ":", "=", "-", "—", ":", "\t", "|"]
|
||||
field_name_clean = field_name.strip()
|
||||
|
||||
for elem in elements:
|
||||
text = elem.text
|
||||
if field_name_clean not in text:
|
||||
continue
|
||||
|
||||
for sep in separators:
|
||||
pattern = re.escape(field_name_clean) + r"\s*" + re.escape(sep) + r"\s*(.+)"
|
||||
m = re.search(pattern, text)
|
||||
if m:
|
||||
value = m.group(1).strip()
|
||||
if value:
|
||||
return ExtractedField(
|
||||
field_name=field_name,
|
||||
field_value=value,
|
||||
bbox=elem.bbox,
|
||||
confidence=0.95,
|
||||
extraction_method="",
|
||||
)
|
||||
|
||||
simple_pattern = re.escape(field_name_clean) + r"\s+(.+)"
|
||||
m = re.search(simple_pattern, text)
|
||||
if m:
|
||||
value = m.group(1).strip()
|
||||
if value and value != field_name_clean:
|
||||
return ExtractedField(
|
||||
field_name=field_name,
|
||||
field_value=value,
|
||||
bbox=elem.bbox,
|
||||
confidence=0.85,
|
||||
extraction_method="",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 策略2: 模糊键值对匹配
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _fuzzy_kv_match(
|
||||
self,
|
||||
field_name: str,
|
||||
elements: list[OcrTextElement],
|
||||
) -> Optional[ExtractedField]:
|
||||
"""模糊键值对匹配: 字段名和值分布在相邻的文本元素中。
|
||||
|
||||
找到含字段名的元素后,在同一行或相邻元素中查找值。
|
||||
"""
|
||||
field_name_clean = field_name.strip()
|
||||
field_elem = None
|
||||
|
||||
for elem in elements:
|
||||
if field_name_clean in elem.text:
|
||||
field_elem = elem
|
||||
break
|
||||
|
||||
if field_elem is None:
|
||||
matching = []
|
||||
for elem in elements:
|
||||
sim = self._text_similarity(field_name_clean, elem.text)
|
||||
if sim > 0.6:
|
||||
matching.append((sim, elem))
|
||||
if matching:
|
||||
matching.sort(key=lambda x: x[0], reverse=True)
|
||||
field_elem = matching[0][1]
|
||||
|
||||
if field_elem is None:
|
||||
return None
|
||||
|
||||
candidates = []
|
||||
for elem in elements:
|
||||
if elem is field_elem:
|
||||
continue
|
||||
candidates.append(elem)
|
||||
|
||||
same_row = []
|
||||
for elem in candidates:
|
||||
if abs(elem.center_y - field_elem.center_y) < field_elem.height * 1.5:
|
||||
same_row.append(elem)
|
||||
if same_row:
|
||||
same_row.sort(key=lambda e: e.x_min)
|
||||
for elem in same_row:
|
||||
if elem.x_min > field_elem.x_max:
|
||||
return ExtractedField(
|
||||
field_name=field_name,
|
||||
field_value=elem.text,
|
||||
bbox=elem.bbox,
|
||||
confidence=0.75,
|
||||
extraction_method="",
|
||||
)
|
||||
|
||||
nearest = None
|
||||
nearest_dist = float("inf")
|
||||
for elem in candidates:
|
||||
if elem.y_min > field_elem.y_max:
|
||||
dy = elem.y_min - field_elem.y_max
|
||||
dx = abs(elem.center_x - field_elem.center_x)
|
||||
dist = dy + dx * 0.3
|
||||
if dist < nearest_dist and dy < field_elem.height * 3:
|
||||
nearest_dist = dist
|
||||
nearest = elem
|
||||
|
||||
if nearest:
|
||||
return ExtractedField(
|
||||
field_name=field_name,
|
||||
field_value=nearest.text,
|
||||
bbox=nearest.bbox,
|
||||
confidence=0.6,
|
||||
extraction_method="",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 策略3: 正则模式匹配
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
PREDEFINED_PATTERNS: dict[str, str] = {
|
||||
# 发票字段
|
||||
"发票代码": r"[0-9A-Za-z]{10,12}",
|
||||
"发票号码": r"\d{8}",
|
||||
"合计金额": r"[\d,]+\.?\d*",
|
||||
"金额": r"[\d,]+\.?\d*",
|
||||
"开票日期": r"\d{4}[年/\-]\d{1,2}[月/\-]\d{1,2}日?",
|
||||
"日期": r"\d{4}[年/\-]\d{1,2}[月/\-]\d{1,2}日?",
|
||||
"校验码": r"[0-9A-Fa-f]{5,20}",
|
||||
"总价": r"[\d,]+\.?\d*",
|
||||
"总金额": r"[\d,]+\.?\d*",
|
||||
"价税合计": r"[\d,]+\.?\d*",
|
||||
"数量": r"\d+\.?\d*",
|
||||
"单价": r"[\d,]+\.?\d*",
|
||||
"税率": r"\d+\.?\d*%?",
|
||||
# 车历卡/维修结算单字段
|
||||
"维修单号": r"[A-Za-z0-9\-]{6,20}",
|
||||
"车牌号": r"[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤川青藏琼宁][A-Z][·\-]?[A-Z0-9]{5,6}",
|
||||
"联系电话": r"1[3-9]\d{9}",
|
||||
"VIN码": r"[A-HJ-NPR-Z0-9]{17}",
|
||||
"发动机号": r"[A-Z0-9]{6,12}",
|
||||
# 采购单字段
|
||||
"采购日期": r"\d{4}[年/\-]\d{1,2}[月/\-]\d{1,2}日?",
|
||||
"订单号": r"[A-Z0-9\-]{6,20}",
|
||||
}
|
||||
|
||||
def _regex_match(
|
||||
self,
|
||||
field_name: str,
|
||||
elements: list[OcrTextElement],
|
||||
) -> Optional[ExtractedField]:
|
||||
"""正则模式匹配: 根据字段名选择预定义的正则模式,在所有元素中搜索。"""
|
||||
pattern = self.PREDEFINED_PATTERNS.get(field_name)
|
||||
if not pattern:
|
||||
for key, pat in self.PREDEFINED_PATTERNS.items():
|
||||
if key in field_name or field_name in key:
|
||||
pattern = pat
|
||||
break
|
||||
|
||||
if not pattern:
|
||||
return None
|
||||
|
||||
compiled = re.compile(r"^\s*" + pattern + r"\s*$")
|
||||
for elem in elements:
|
||||
if compiled.match(elem.text):
|
||||
return ExtractedField(
|
||||
field_name=field_name,
|
||||
field_value=elem.text.strip(),
|
||||
bbox=elem.bbox,
|
||||
confidence=0.7,
|
||||
extraction_method="",
|
||||
)
|
||||
|
||||
compiled_partial = re.compile(pattern)
|
||||
for elem in elements:
|
||||
m = compiled_partial.search(elem.text)
|
||||
if m:
|
||||
return ExtractedField(
|
||||
field_name=field_name,
|
||||
field_value=m.group(0),
|
||||
bbox=elem.bbox,
|
||||
confidence=0.6,
|
||||
extraction_method="",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 策略4: 表格结构匹配
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _table_match(
|
||||
self,
|
||||
field_name: str,
|
||||
elements: list[OcrTextElement],
|
||||
) -> Optional[ExtractedField]:
|
||||
"""表格结构匹配: 将元素按行列分组,查找表头-值对应关系。
|
||||
|
||||
识别逻辑:
|
||||
1. 将元素按 Y 坐标分组为"行"
|
||||
2. 查找包含 field_name 的表头行
|
||||
3. 在表头列对应的数据行中取值
|
||||
"""
|
||||
if len(elements) < 3:
|
||||
return None
|
||||
|
||||
rows = self._group_elements_by_rows(elements)
|
||||
if len(rows) < 2:
|
||||
return None
|
||||
|
||||
header_row_idx = -1
|
||||
header_col_idx = -1
|
||||
|
||||
for ri, row in enumerate(rows):
|
||||
for ci, elem in enumerate(row):
|
||||
if field_name in elem.text:
|
||||
header_row_idx = ri
|
||||
header_col_idx = ci
|
||||
break
|
||||
if header_row_idx >= 0:
|
||||
break
|
||||
|
||||
if header_row_idx < 0:
|
||||
for ri, row in enumerate(rows):
|
||||
for ci, elem in enumerate(row):
|
||||
sim = self._text_similarity(field_name, elem.text)
|
||||
if sim > 0.5:
|
||||
header_row_idx = ri
|
||||
header_col_idx = ci
|
||||
break
|
||||
if header_row_idx >= 0:
|
||||
break
|
||||
|
||||
if header_row_idx < 0:
|
||||
return None
|
||||
|
||||
data_rows = rows[header_row_idx + 1:]
|
||||
if not data_rows:
|
||||
data_rows = [rows[header_row_idx]]
|
||||
|
||||
matched_elem = None
|
||||
for row in data_rows:
|
||||
if header_col_idx < len(row):
|
||||
matched_elem = row[header_col_idx]
|
||||
break
|
||||
closest = None
|
||||
min_dist = float("inf")
|
||||
header_x = float("inf")
|
||||
if header_col_idx < len(rows[header_row_idx]):
|
||||
header_x = rows[header_row_idx][header_col_idx].center_x
|
||||
for elem in row:
|
||||
dist = abs(elem.center_x - header_x)
|
||||
if dist < min_dist:
|
||||
min_dist = dist
|
||||
closest = elem
|
||||
if closest:
|
||||
matched_elem = closest
|
||||
break
|
||||
|
||||
if matched_elem and matched_elem.text != field_name:
|
||||
return ExtractedField(
|
||||
field_name=field_name,
|
||||
field_value=matched_elem.text,
|
||||
bbox=matched_elem.bbox,
|
||||
confidence=0.55,
|
||||
extraction_method="",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
# ========================================================================
|
||||
# 工具方法
|
||||
# ========================================================================
|
||||
|
||||
@staticmethod
|
||||
def _group_elements_by_rows(
|
||||
elements: list[OcrTextElement],
|
||||
) -> list[list[OcrTextElement]]:
|
||||
"""将元素按 Y 坐标分组为行(容差为元素平均高度的一半)。"""
|
||||
if not elements:
|
||||
return []
|
||||
|
||||
avg_height = sum(e.height for e in elements) / len(elements)
|
||||
tolerance = max(avg_height * 0.5, 5.0)
|
||||
|
||||
rows = []
|
||||
current_row = [elements[0]]
|
||||
|
||||
for elem in elements[1:]:
|
||||
prev_center_y = current_row[0].center_y
|
||||
if abs(elem.center_y - prev_center_y) < tolerance:
|
||||
current_row.append(elem)
|
||||
else:
|
||||
current_row.sort(key=lambda e: e.x_min)
|
||||
rows.append(current_row)
|
||||
current_row = [elem]
|
||||
|
||||
if current_row:
|
||||
current_row.sort(key=lambda e: e.x_min)
|
||||
rows.append(current_row)
|
||||
|
||||
return rows
|
||||
|
||||
@staticmethod
|
||||
def _text_similarity(text1: str, text2: str) -> float:
|
||||
"""计算两个文本的简单相似度(公共字符比例)。"""
|
||||
if not text1 or not text2:
|
||||
return 0.0
|
||||
|
||||
t1 = text1.lower().strip()
|
||||
t2 = text2.lower().strip()
|
||||
|
||||
if t1 == t2:
|
||||
return 1.0
|
||||
if t1 in t2 or t2 in t1:
|
||||
return 0.8
|
||||
|
||||
chars1 = set(t1)
|
||||
chars2 = set(t2)
|
||||
if not chars1:
|
||||
return 0.0
|
||||
|
||||
intersection = chars1 & chars2
|
||||
return len(intersection) / len(chars1)
|
||||
|
||||
|
||||
def extract_ocr_fields(
|
||||
file_path: str,
|
||||
target_fields: list[str],
|
||||
use_gpu: bool = False,
|
||||
confidence_threshold: float = 0.5,
|
||||
) -> dict:
|
||||
"""便捷函数: 对指定图片执行 OCR 字段提取。
|
||||
|
||||
Args:
|
||||
file_path: 图片文件路径
|
||||
target_fields: 目标字段名列表
|
||||
use_gpu: 是否使用 GPU 加速
|
||||
confidence_threshold: OCR 置信度阈值
|
||||
|
||||
Returns:
|
||||
提取结果字典
|
||||
"""
|
||||
extractor = OcrExtractor(
|
||||
use_gpu=use_gpu,
|
||||
confidence_threshold=confidence_threshold,
|
||||
)
|
||||
return extractor.extract(file_path, target_fields)
|
||||
|
||||
|
||||
def extract_from_layout(
|
||||
layout_result: dict,
|
||||
target_fields: list[str],
|
||||
confidence_threshold: float = 0.5,
|
||||
) -> dict:
|
||||
"""便捷函数: 从已有的版面分析结果中提取字段。
|
||||
|
||||
Args:
|
||||
layout_result: analyze_layout() 的返回值
|
||||
target_fields: 目标字段名列表
|
||||
confidence_threshold: OCR 置信度阈值
|
||||
|
||||
Returns:
|
||||
提取结果字典
|
||||
"""
|
||||
extractor = OcrExtractor(confidence_threshold=confidence_threshold)
|
||||
return extractor.extract_from_layout_result(layout_result, target_fields)
|
||||
@@ -0,0 +1,161 @@
|
||||
"""RAG 适配层 — 查询已由 rag_jrxml 子项目构建好的 ChromaDB 向量知识库。
|
||||
|
||||
rag_jrxml 独立运行产出向量库后,主项目通过此模块进行语义搜索。
|
||||
|
||||
用法:
|
||||
from backend.rag_adapter import search_chunks
|
||||
context = search_chunks("如何添加饼图", k=5)
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _resolve(path: str) -> Path:
|
||||
p = Path(path)
|
||||
if not p.is_absolute():
|
||||
p = _PROJECT_ROOT / p
|
||||
return p
|
||||
|
||||
|
||||
class RAGSearcher:
|
||||
"""连接预构建的 ChromaDB,提供语义搜索。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chroma_path: Optional[str] = None,
|
||||
collection_name: Optional[str] = None,
|
||||
model_name: Optional[str] = None,
|
||||
use_gpu: Optional[bool] = None,
|
||||
use_fp16: Optional[bool] = None,
|
||||
):
|
||||
self.chroma_path = _resolve(chroma_path or os.getenv("RAG_CHROMA_PATH", "./db/chroma"))
|
||||
self.collection_name = collection_name or os.getenv("RAG_COLLECTION_NAME", "jrxml_chunks")
|
||||
model_path = model_name or os.getenv("RAG_EMBED_MODEL", "./rag/models/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
# 如果本地路径存在则使用本地,否则当 Hub 模型名使用
|
||||
resolved = _resolve(model_path)
|
||||
self.model_name = str(resolved) if resolved.exists() else model_path
|
||||
self.use_gpu = use_gpu if use_gpu is not None else os.getenv("RAG_USE_GPU", "true").lower() in ("true", "1")
|
||||
self.use_fp16 = use_fp16 if use_fp16 is not None else os.getenv("RAG_USE_FP16", "true").lower() in ("true", "1")
|
||||
|
||||
self._model = None
|
||||
self._client = None
|
||||
self._collection = None
|
||||
|
||||
# ---- 模型懒加载 ----
|
||||
@property
|
||||
def model(self):
|
||||
if self._model is None:
|
||||
import torch
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
device = "cuda" if (self.use_gpu and torch.cuda.is_available()) else "cpu"
|
||||
logger.info("加载嵌入模型: %s (device=%s)", self.model_name, device)
|
||||
model = SentenceTransformer(self.model_name, device=device)
|
||||
if device == "cuda" and self.use_fp16:
|
||||
model = model.half()
|
||||
self._model = model
|
||||
return self._model
|
||||
|
||||
# ---- ChromaDB 懒连接 ----
|
||||
@property
|
||||
def client(self):
|
||||
if self._client is None:
|
||||
import chromadb
|
||||
self._client = chromadb.PersistentClient(path=str(self.chroma_path))
|
||||
return self._client
|
||||
|
||||
@property
|
||||
def collection(self):
|
||||
if self._collection is None:
|
||||
self._collection = self.client.get_collection(self.collection_name)
|
||||
return self._collection
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
try:
|
||||
self.client.get_collection(self.collection_name)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# ---- 语义搜索 ----
|
||||
def search(self, query: str, k: int = 5, threshold: Optional[float] = None) -> list[dict]:
|
||||
"""搜索相关 JRXML chunks,返回 [{id, content, metadata, distance}, ...]."""
|
||||
if not self.is_ready():
|
||||
logger.warning("ChromaDB 集合 '%s' 不存在,请先在 rag/ 子项目中运行管线", self.collection_name)
|
||||
return []
|
||||
|
||||
query_embedding = self.model.encode(
|
||||
query, normalize_embeddings=True, show_progress_bar=False
|
||||
)
|
||||
|
||||
results = self.collection.query(
|
||||
query_embeddings=[query_embedding.tolist()],
|
||||
n_results=k,
|
||||
include=["documents", "metadatas", "distances"],
|
||||
)
|
||||
|
||||
output = []
|
||||
if not results["ids"] or not results["ids"][0]:
|
||||
return output
|
||||
|
||||
for i, doc_id in enumerate(results["ids"][0]):
|
||||
dist = results["distances"][0][i]
|
||||
if threshold is not None and dist > threshold:
|
||||
continue
|
||||
output.append({
|
||||
"id": doc_id,
|
||||
"content": results["documents"][0][i],
|
||||
"metadata": results["metadatas"][0][i],
|
||||
"distance": dist,
|
||||
})
|
||||
return output
|
||||
|
||||
def search_as_context(self, query: str, k: int = 5) -> str:
|
||||
"""搜索并返回拼接好的上下文字符串,可直接注入 LLM prompt。"""
|
||||
results = self.search(query, k=k)
|
||||
if not results:
|
||||
return ""
|
||||
|
||||
parts = []
|
||||
for r in results:
|
||||
meta = r["metadata"]
|
||||
header = f"[类型:{meta.get('chunk_type', 'N/A')}]"
|
||||
if meta.get("report_name"):
|
||||
header += f" [报表:{meta['report_name']}]"
|
||||
if meta.get("band_name"):
|
||||
header += f" [区域:{meta['band_name']}]"
|
||||
parts.append(f"{header}\n{r['content']}")
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
|
||||
# 全局单例,避免重复加载模型
|
||||
_searcher: Optional[RAGSearcher] = None
|
||||
|
||||
|
||||
def _get_searcher() -> RAGSearcher:
|
||||
global _searcher
|
||||
if _searcher is None:
|
||||
_searcher = RAGSearcher()
|
||||
return _searcher
|
||||
|
||||
|
||||
def search_chunks(query: str, k: int = 5, kb_id: str = "") -> str:
|
||||
"""搜索知识库并返回拼接后的上下文文本。
|
||||
|
||||
若指定 kb_id,使用该 KB 专属 ChromaDB;否则使用全局默认库。
|
||||
"""
|
||||
if kb_id:
|
||||
from backend.kb_searcher import search_kb
|
||||
return search_kb(kb_id, query, k=k)
|
||||
return _get_searcher().search_as_context(query, k=k)
|
||||
+128
-26
@@ -5,15 +5,60 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import uuid
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Optional, Any
|
||||
|
||||
# Per-session-file locks to prevent concurrent writes from corrupting JSON
|
||||
_session_locks: dict[str, threading.Lock] = {}
|
||||
_locks_lock = threading.Lock()
|
||||
|
||||
def _get_lock(session_id: str) -> threading.Lock:
|
||||
with _locks_lock:
|
||||
if session_id not in _session_locks:
|
||||
_session_locks[session_id] = threading.Lock()
|
||||
return _session_locks[session_id]
|
||||
|
||||
|
||||
class _SafeEncoder(json.JSONEncoder):
|
||||
"""处理 numpy / lxml / 等非标准类型的 JSON 序列化"""
|
||||
|
||||
def default(self, o: Any) -> Any:
|
||||
try:
|
||||
# numpy 标量
|
||||
import numpy as np
|
||||
if isinstance(o, np.integer):
|
||||
return int(o)
|
||||
if isinstance(o, np.floating):
|
||||
return float(o)
|
||||
if isinstance(o, np.ndarray):
|
||||
return o.tolist()
|
||||
if isinstance(o, np.bool_):
|
||||
return bool(o)
|
||||
except ImportError:
|
||||
pass
|
||||
# lxml intc / 其他 C 类型
|
||||
try:
|
||||
return int(o)
|
||||
except Exception:
|
||||
pass
|
||||
# bytes
|
||||
if isinstance(o, bytes):
|
||||
return o.decode("utf-8", errors="replace")
|
||||
return super().default(o)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from backend.logger import get_logger
|
||||
|
||||
load_dotenv()
|
||||
|
||||
_session_log = get_logger("session")
|
||||
|
||||
SESSIONS_DIR = Path(os.getenv("SESSIONS_DIR", "./sessions"))
|
||||
|
||||
|
||||
@@ -21,35 +66,64 @@ def _ensure_dir():
|
||||
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
_VALID_SESSION_ID_RE = re.compile(r'^[a-fA-F0-9]{12,}$')
|
||||
|
||||
def validate_session_id(session_id: str) -> bool:
|
||||
"""校验 session_id 仅含合法 hex 字符(防路径穿越)。"""
|
||||
return bool(_VALID_SESSION_ID_RE.match(session_id))
|
||||
|
||||
def _session_path(session_id: str) -> Path:
|
||||
if not validate_session_id(session_id):
|
||||
raise ValueError(f"Invalid session_id: {session_id!r}")
|
||||
return SESSIONS_DIR / f"{session_id}.json"
|
||||
|
||||
|
||||
def generate_session_id() -> str:
|
||||
return uuid.uuid4().hex[:12]
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
def create_session(name: str = "", agent_state: Optional[dict] = None) -> dict:
|
||||
"""创建新会话,返回会话元数据。"""
|
||||
def create_session(name: str = "", agent_state: Optional[dict] = None,
|
||||
session_id: Optional[str] = None) -> dict:
|
||||
"""创建新会话,返回会话元数据。session_id 可选——传入时使用指定 ID。"""
|
||||
_ensure_dir()
|
||||
sid = generate_session_id()
|
||||
sid = session_id or generate_session_id()
|
||||
now = _now_iso()
|
||||
agent_state = agent_state or {}
|
||||
agent_state["session_id"] = sid
|
||||
data = {
|
||||
"session_id": sid,
|
||||
"session_name": name or f"新建报表 {now[:10]}",
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"agent_state": agent_state or {},
|
||||
"kb_id": agent_state.get("kb_id", "") if agent_state else "",
|
||||
"agent_state": agent_state,
|
||||
}
|
||||
with open(_session_path(sid), "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
fp = _session_path(sid)
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False,
|
||||
dir=SESSIONS_DIR, encoding="utf-8",
|
||||
)
|
||||
try:
|
||||
json.dump(data, tmp, ensure_ascii=False, indent=2, cls=_SafeEncoder)
|
||||
tmp.flush()
|
||||
os.fsync(tmp.fileno())
|
||||
tmp.close()
|
||||
os.replace(tmp.name, str(fp))
|
||||
except Exception:
|
||||
tmp.close()
|
||||
Path(tmp.name).unlink(missing_ok=True)
|
||||
raise
|
||||
_session_log.info("创建会话", extra={"session_id": sid, "session_name": data["session_name"]})
|
||||
return data
|
||||
|
||||
|
||||
def load_session(session_id: str) -> Optional[dict]:
|
||||
"""按 ID 加载会话数据。未找到则返回 None。"""
|
||||
_ensure_dir()
|
||||
fp = _session_path(session_id)
|
||||
try:
|
||||
fp = _session_path(session_id)
|
||||
except ValueError:
|
||||
return None
|
||||
if not fp.exists():
|
||||
return None
|
||||
with open(fp, "r", encoding="utf-8") as f:
|
||||
@@ -57,26 +131,50 @@ def load_session(session_id: str) -> Optional[dict]:
|
||||
|
||||
|
||||
def save_session(session_id: str, agent_state: dict, session_name: str = ""):
|
||||
"""将会话状态保存(更新)至磁盘。"""
|
||||
"""线程安全地原子保存会话状态到磁盘。"""
|
||||
_ensure_dir()
|
||||
fp = _session_path(session_id)
|
||||
data = {}
|
||||
if fp.exists():
|
||||
with open(fp, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
lock = _get_lock(session_id)
|
||||
with lock:
|
||||
data = {}
|
||||
if fp.exists():
|
||||
with open(fp, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
data["session_id"] = session_id
|
||||
if session_name:
|
||||
data["session_name"] = session_name
|
||||
if not data.get("session_name"):
|
||||
data["session_name"] = f"报表 {data.get('created_at', _now_iso())[:10]}"
|
||||
data["updated_at"] = _now_iso()
|
||||
if not data.get("created_at"):
|
||||
data["created_at"] = data["updated_at"]
|
||||
data["agent_state"] = agent_state
|
||||
data["session_id"] = session_id
|
||||
if session_name:
|
||||
data["session_name"] = session_name
|
||||
if not data.get("session_name"):
|
||||
data["session_name"] = f"报表 {data.get('created_at', _now_iso())[:10]}"
|
||||
data["updated_at"] = _now_iso()
|
||||
if not data.get("created_at"):
|
||||
data["created_at"] = data["updated_at"]
|
||||
data["agent_state"] = agent_state
|
||||
|
||||
with open(fp, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
# 原子写入:先写临时文件,再 replace,避免崩溃时截断 JSON
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False,
|
||||
dir=SESSIONS_DIR, encoding="utf-8",
|
||||
)
|
||||
try:
|
||||
json.dump(data, tmp, ensure_ascii=False, indent=2, cls=_SafeEncoder)
|
||||
tmp.flush()
|
||||
os.fsync(tmp.fileno())
|
||||
tmp.close()
|
||||
os.replace(tmp.name, str(fp))
|
||||
except Exception:
|
||||
tmp.close()
|
||||
Path(tmp.name).unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
def get_session_state(session_id: str) -> Optional[dict]:
|
||||
"""获取会话的完整 agent_state,用于 REST API。
|
||||
|
||||
返回 dict 包含 session_id, session_name, created_at, updated_at, agent_state。
|
||||
未找到则返回 None。
|
||||
"""
|
||||
return load_session(session_id)
|
||||
|
||||
|
||||
def list_all_sessions() -> list[dict]:
|
||||
@@ -101,9 +199,13 @@ def list_all_sessions() -> list[dict]:
|
||||
def delete_session(session_id: str) -> bool:
|
||||
"""按 ID 删除会话文件。"""
|
||||
_ensure_dir()
|
||||
fp = _session_path(session_id)
|
||||
try:
|
||||
fp = _session_path(session_id)
|
||||
except ValueError:
|
||||
return False
|
||||
if fp.exists():
|
||||
fp.unlink()
|
||||
_session_log.info("删除会话", extra={"session_id": session_id})
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
+30
-5
@@ -4,23 +4,48 @@ import os
|
||||
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
from httpx import ConnectError, HTTPStatusError
|
||||
|
||||
load_dotenv()
|
||||
from backend.logger import get_logger
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
_val_log = get_logger("validation")
|
||||
|
||||
VALIDATION_URL = os.getenv("VALIDATION_SERVICE_URL", "http://localhost:8001/validate")
|
||||
|
||||
|
||||
def validate_jrxml(jrxml_text: str) -> dict:
|
||||
"""将 JRXML 发送到验证服务并返回 {valid: bool, error: str}。"""
|
||||
jrxml_length = len(jrxml_text)
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.post(VALIDATION_URL, json={"jrxml": jrxml_text})
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except httpx.ConnectError:
|
||||
result = resp.json()
|
||||
_val_log.info(
|
||||
"验证完成",
|
||||
extra={
|
||||
"valid": result.get("valid"),
|
||||
"error": result.get("error", ""),
|
||||
"jrxml_length": jrxml_length,
|
||||
},
|
||||
)
|
||||
return result
|
||||
except ConnectError:
|
||||
error_msg = f"无法连接到验证服务 ({VALIDATION_URL})。是否正在运行?"
|
||||
_val_log.error("验证服务连接失败", extra={"error": error_msg, "url": VALIDATION_URL})
|
||||
return {"valid": False, "error": error_msg, "service_unavailable": True}
|
||||
except HTTPStatusError as e:
|
||||
status_code = e.response.status_code
|
||||
error_msg = f"验证服务返回错误 ({status_code}): {str(e)}"
|
||||
_val_log.error("验证请求异常", extra={"error": str(e), "url": VALIDATION_URL, "status_code": status_code})
|
||||
return {
|
||||
"valid": False,
|
||||
"error": f"无法连接到验证服务 ({VALIDATION_URL})。是否正在运行?",
|
||||
"error": error_msg,
|
||||
"service_unavailable": status_code >= 500,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"valid": False, "error": f"验证请求失败: {str(e)}"}
|
||||
error_msg = f"验证请求失败: {str(e)}"
|
||||
_val_log.error("验证请求异常", extra={"error": str(e), "url": VALIDATION_URL})
|
||||
return {"valid": False, "error": error_msg}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# jaspersoft 五轮修正失败问题 — 现象文档
|
||||
|
||||
## 1. 问题概述
|
||||
|
||||
**触发场景**:用户上传一张车历卡(维修结算单)图片,系统通过 OCR 识别并生成 JRXML 报表模板。
|
||||
|
||||
**现象**:5 次自动修正循环全部失败,系统提示"[内容保真度不足] 得分 0.00/1.0",最终无法生成可用 JRXML。
|
||||
|
||||
**测试会话**:`sessions/6d39a91e11c54f02bb70a62d856ea2d4.json`(2026-05-24 15:14)
|
||||
|
||||
---
|
||||
|
||||
## 2. 完整流程追踪
|
||||
|
||||
### 2.1 用户输入
|
||||
|
||||
- 上传文件:`e1113725c20fc4ec39bc9e4ab0caa6b2.jpg`(车历卡,1357×1920 RGB)
|
||||
- 用户输入:空(仅上传文件)
|
||||
|
||||
### 2.2 系统处理流程
|
||||
|
||||
```
|
||||
上传图片
|
||||
→ process_input 节点(OCR 字段提取 + 布局分析)
|
||||
→ layout_analyzer(34 行 × 1 列,A4 纵向)
|
||||
→ ocr_extractor(4 策略提取)
|
||||
→ classify_intent(= initial_generation)
|
||||
→ retrieve
|
||||
→ route_after_retrieve(有 layout_schema,走 generate_skeleton)
|
||||
→ generate_skeleton(生成 ~34k 字符骨架 JRXML)
|
||||
→ refine_layout(Band 级窗口化精调)
|
||||
→ map_fields(程序化字段替换 $F{field_N} → 真实字段名)
|
||||
→ validate(validate 节点)
|
||||
→ XSD 验证:✅ 通过
|
||||
→ OCR 保真度检查:❌ score=0.41 < 0.5 → 降级为 fail
|
||||
→ error_msg = "[内容保真度不足] 得分 0.41/1.0。元素覆盖不足:JRXML 仅有 142 个文本元素,OCR 源有 173 个文本元素(覆盖率 82%)。JRXML 中未声明任何字段,但 OCR 提取了结构化字段数据"
|
||||
→ 5 次 correct_jrxml 修正循环均失败
|
||||
→ 状态:fail(MAX_RETRY=5 耗尽)
|
||||
```
|
||||
|
||||
### 2.3 最终状态
|
||||
|
||||
```
|
||||
status: fail
|
||||
retry_count: 5
|
||||
error_msg: "[内容保真度不足] 得分 0.00/1.0。JRXML 仅有 0 个文本元素,OCR 源有 173 个文本元素(覆盖率 0%)"
|
||||
```
|
||||
|
||||
⚠️ **重要矛盾**:`current_jrxml`(24391 字符,142 个 textField)与 `error_msg`("0 个文本元素",score=0.00)存在矛盾。
|
||||
|
||||
验证服务审计(`validate-service-audit`)指出:"6d39a91e has 0 text elements causing score=0.00"——说明在**触发 fail 的那个时间点**,JRXML 确实只有 0 个文本元素。
|
||||
|
||||
但 session 文件最终保存的 `current_jrxml` 是 24391 字符版本。`jrxml_versions` 最后一条记录的 `jrxml` 才是触发失败的真实版本。
|
||||
|
||||
**结论**:`current_jrxml` 在 5 次修正过程中被逐步侵蚀,最终版本是某个接近空壳的状态。`jrxml_versions[-1]` 中的 `jrxml` 才是真正的失败版本。
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键数据对比
|
||||
|
||||
### 3.1 JRXML 实际内容
|
||||
|
||||
session `6d39a91e` 的 `current_jrxml`:
|
||||
- 长度:24391 字符
|
||||
- `<textField` 标签数:284(含自闭合标签)
|
||||
- 完整 textField 元素:142 个
|
||||
- `<staticText>` 元素:0 个
|
||||
- `<field>` 声明:63 个
|
||||
- pageWidth/pageHeight:595×842(A4)
|
||||
- namespace:无 ns0: 前缀(✅ 已消除)
|
||||
|
||||
### 3.2 OCR 数据
|
||||
|
||||
layout_schema:
|
||||
- total_rows: 34
|
||||
- total_columns: 1
|
||||
- 总文本元素:173 个
|
||||
|
||||
ocr_extraction_result:
|
||||
- total_elements: 173
|
||||
- fields 数组:18 个字段,全部是**发票模板字段**
|
||||
- 不含税金额、价税合计、单价、发票代码、发票号码、合计金额、开票日期、总金额、数量、日期、校验码、税率、税额、规格型号、货物名称、购买方名称、金额、销售方名称
|
||||
- 正确匹配率:3/18(17%)
|
||||
|
||||
### 3.3 评分数据
|
||||
|
||||
`_check_ocr_fidelity` 实际运行结果:
|
||||
|
||||
```python
|
||||
# element_coverage 计算
|
||||
textField_count = 142 # 完整元素(非标签数)
|
||||
staticText_count = 0
|
||||
total_jrxml_elements = 142
|
||||
ocr_text_count = 173
|
||||
element_coverage = min(142/173, 1.0) = 0.82
|
||||
|
||||
# field_coverage 计算
|
||||
jrxml_fields = {"print_date", "repair_number", ..., "vehicle_plate", ...} # 63 个英文字段
|
||||
ocr_field_names = {"发票代码", "发票号码", "合计金额", ...} # 18 个中文字段
|
||||
matched = jrxml_fields ∩ ocr_field_names = ∅
|
||||
field_coverage = 0/18 = 0.0
|
||||
|
||||
# score 计算
|
||||
score = 0.0 * 0.5 + 0.82 * 0.5 = 0.41
|
||||
```
|
||||
|
||||
**评分公式**(`nodes.py:1251`):
|
||||
```python
|
||||
score = round(field_coverage * 0.5 + element_coverage * 0.5, 3)
|
||||
```
|
||||
|
||||
**降级条件**(`nodes.py:1294`):
|
||||
```python
|
||||
if fidelity["score"] < 0.5:
|
||||
state["status"] = "fail"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 已确认的根因
|
||||
|
||||
### 根因 1 — 评分逻辑设计错误(P0)
|
||||
|
||||
**问题**:`field_coverage` 将英文字段名和中文字段名做交集比对,在普通生成场景下永远为 0。
|
||||
|
||||
**原因**:
|
||||
1. LLM 生成英文字段名(`print_date`、`vehicle_plate`)是正确的设计选择
|
||||
2. OCR 提取器硬套发票模板,提取中文字段名(`发票代码`、`合计金额`)
|
||||
3. 两者来自完全不同的命名体系,不可能匹配
|
||||
4. `field_coverage=0` 是**预期行为**,而非错误
|
||||
|
||||
**修复方向**:评分公式改为只依赖 `element_coverage`,`field_coverage` 作为信息提示而非降级条件。
|
||||
|
||||
### 根因 2 — OCR 字段提取器无文档类型区分(P0)
|
||||
|
||||
**问题**:`backend/ocr_extractor.py` 对所有单据使用同一套发票字段模板(14 个字段)。
|
||||
|
||||
**现象**:
|
||||
- 车历卡被当作发票处理
|
||||
- 手机号 `13516727312` 被 6 个字段复用(发票代码/校验码/价税合计/单价/税率/不含税金额)
|
||||
- 字段名错配:发票号码→"服务套餐"、总金额→"钣金"、数量→"零件等级"
|
||||
|
||||
### 根因 3 — namespace 修复指令是条件触发(P1)
|
||||
|
||||
**位置**:`prompts/correction.md` 第 11 行
|
||||
|
||||
**问题**:namespace 修复指令只在错误消息**包含 "namespace" 关键词**时才激活,是条件触发而非无条件指令。
|
||||
|
||||
**现象**:
|
||||
- ecd592 session 所有 5 次修正的实际错误类型是"字段未声明"(`字段 'u53d1_u7968...' 在表达式中使用但未声明`),**不包含 "namespace" 关键词**
|
||||
- 因此 namespace 修复指令从未被激活
|
||||
- ns0: 前缀在前两次修正中持续存在,直到第 3 次才被 LLM 自发消除
|
||||
- 最终 `current_jrxml`(ecd592)仍有 `<ns0:jasperReport>`
|
||||
|
||||
**修复方向**:
|
||||
- `prompts/correction.md`:改为无条件指令(检查 JRXML 是否包含 `ns0:`,而非依赖错误类型)
|
||||
- `prompts/initial_generation.md` + `skeleton_generation.md`:添加删除 ns0: 前缀的无条件指令
|
||||
|
||||
### 根因 4 — 正则 `\w+` 不支持中文(低优先级)
|
||||
|
||||
**位置**:`nodes.py:1211`
|
||||
```python
|
||||
jrxml_fields = set(re.findall(r'<field name="(\w+)"', jrxml))
|
||||
```
|
||||
|
||||
**问题**:`\w` 匹配 `[a-zA-Z0-9_]`,不匹配中文。如果 JRXML 使用中文字段名,正则返回 0 个匹配。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证服务的角色
|
||||
|
||||
**文件**:`validation_service/main.py`
|
||||
|
||||
- XSD 验证:通过(✅)
|
||||
- 结构检查:字段声明一致性、SQL SELECT 存在性、pageWidth/pageHeight
|
||||
- **结论**:XSD 验证通过,`correct_jrxml` 的 ns0: 消除也生效——真正导致 fail 的是 OCR 保真度评分
|
||||
|
||||
---
|
||||
|
||||
## 6. 现象描述的矛盾点(需进一步排查)
|
||||
|
||||
session `6d39a91e` 中存在数字矛盾:
|
||||
|
||||
| 指标 | session 中的值 | 矛盾说明 |
|
||||
|---|---|---|
|
||||
| 最终 `current_jrxml` | 24391 字符,142 个 textField | 这是最后一次修正后保存的最终版本 |
|
||||
| `jrxml_versions[-1].jrxml` | 触发 fail 的真实版本 | 审计团队确认"6d39a91e has 0 text elements causing score=0.00" |
|
||||
| `error_msg` score | "0.00/1.0" | 对应 `jrxml_versions[-1]`,而非 `current_jrxml` |
|
||||
|
||||
**核心矛盾已解决**:`current_jrxml`(24391 字符)是**最终状态**(修正耗尽后最后一次保存的版本),而触发 5 次 fail 降级的是 `jrxml_versions` 中各版本的 JRXML——这些版本在修正循环中被逐步侵蚀,最终版本 `jrxml_versions[-1]` 只有 0 个文本元素(score=0.00)。
|
||||
|
||||
`fidelity-check-audit` 用 Python 分析的是最终保存的 `current_jrxml`(score=0.5),而 `validate-service-audit` 分析的是 `jrxml_versions[-1]`(score=0.00)。**两者分析的不是同一个时间点的 JRXML**。
|
||||
|
||||
---
|
||||
|
||||
## 7. 相关文件清单
|
||||
|
||||
| 文件 | 职责 | 备注 |
|
||||
|---|---|---|
|
||||
| `agent/nodes.py:1171-1257` | `_check_ocr_fidelity` | 评分逻辑(有 bug) |
|
||||
| `agent/nodes.py:1260-1350` | `validate` 节点 | 调用保真度检查 |
|
||||
| `agent/nodes.py:1382-1461` | `correct_jrxml` | 修正循环 |
|
||||
| `backend/ocr_extractor.py` | OCR 字段提取 | 无文档类型区分 |
|
||||
| `prompts/correction.md` | 修正 prompt | namespace 触发受限 |
|
||||
| `validation_service/main.py` | 验证服务 | XSD 通过 |
|
||||
| `sessions/6d39a91e11c54f02bb70a62d856ea2d4.json` | 测试会话 | 主测试数据 |
|
||||
| `sessions/ecd5921838004ab3bc4a1ef6ebd673d1.json` | 历史会话 | namespace 问题参考 |
|
||||
@@ -0,0 +1,586 @@
|
||||
# 对话场景遍历文档
|
||||
|
||||
> 从 `agent/graph.py` 状态图递归遍历生成,覆盖所有用户意图 → 节点路径 → 退出条件。
|
||||
> 最后更新: 2026-05-24
|
||||
|
||||
---
|
||||
|
||||
## 状态图总览
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ 修正循环 (最多 MAX_RETRY=5 次) │
|
||||
│ ┌─────────┐ ┌──────────────┐ ┌────────┐ │
|
||||
│ │ validate │───→│ explain_error│───→│correct │ │
|
||||
│ └────┬─────┘ └──────────────┘ │_jrxml │ │
|
||||
│ │ pass └───┬────┘ │
|
||||
│ ▼ │ │
|
||||
│ ┌─────────┐ retry<5 │
|
||||
│ │finalize │◄────────────────────────────────┘ │
|
||||
│ └─────────┘ retry>=5 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
|
||||
load_session ──→ process_input ──→ manage_context ──→ save_state_snapshot
|
||||
│
|
||||
▼
|
||||
classify_intent
|
||||
│
|
||||
┌────────────┬──────────┬────────┬───────────┼───────────┬──────────┐
|
||||
▼ ▼ ▼ ▼ ▼ ▼ ▼
|
||||
retrieve modify_jrxml save_ handle_ handle_ handle_ (兜底)
|
||||
(新建报表) (修改报表) session consult undo reset
|
||||
│ │ (预览) (咨询) (撤销) (重置)
|
||||
┌────────┴────┐ │ │ │ │ │
|
||||
▼ ▼ │ │ │ │ │
|
||||
generate generate_ │ │ │ │ │
|
||||
(1-shot) skeleton │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ refine_ │ │ │ │ │
|
||||
│ layout │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ map_fields │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
└──────┬──────┘ │ │ │ │ │
|
||||
▼ ▼ ▼ ▼ ▼ ▼
|
||||
save_session ◄─────┴──────────┘ finalize ◄─── finalize ◄── finalize
|
||||
│ ▲
|
||||
│ (预览/导出跳过验证) │
|
||||
├───────────────────────────────────────┘
|
||||
│ (其他意图走验证)
|
||||
▼
|
||||
validate ──→ explain_error ──→ correct_jrxml ──→ validate (循环)
|
||||
│ pass │ retry>=MAX
|
||||
▼ ▼
|
||||
finalize ────────────────────────────────→ finalize
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 节点详细清单
|
||||
|
||||
每个节点标注了 **代码行号** (`agent/nodes.py` 或 `agent/graph.py`)、**前驱节点** (predecessors)、**后继节点** (successors)。
|
||||
|
||||
### 1. load_session — 加载会话
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:77` |
|
||||
| 前驱 | (入口节点, graph entry_point) |
|
||||
| 后继 | `process_input` (固定边 graph.py:198) |
|
||||
| 功能 | 从 `sessions/{session_id}.json` 磁盘加载状态,注入 agent_state。不从磁盘覆盖 `session_id`。 |
|
||||
| LLM | 否 |
|
||||
|
||||
### 2. process_input — 处理用户输入
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:98` |
|
||||
| 前驱 | `load_session` (graph.py:198) |
|
||||
| 后继 | `manage_context` (graph.py:199) |
|
||||
| 功能 | 文件解析(PDF/DOCX/XLSX/图片/文本)→ OCR 字段提取 → 批注检测 → 模板 JRXML 解析。注入 `ocr_extraction_result`、`layout_schema`、`ocr_elements`、`uploaded_template_jrxml`。 |
|
||||
| LLM | 否(OCR 用 PaddleOCR/EasyOCR) |
|
||||
|
||||
### 3. manage_context — 上下文管理
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:143` |
|
||||
| 前驱 | `process_input` (graph.py:199) |
|
||||
| 后继 | `save_state_snapshot` (graph.py:200) |
|
||||
| 功能 | Token 计数 → 对话压缩(超限时 LLM 压缩为摘要)→ `compressed_history`。 |
|
||||
| LLM | 是(压缩时调 LLM) |
|
||||
|
||||
### 4. save_state_snapshot — 状态快照
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:191` |
|
||||
| 前驱 | `manage_context` (graph.py:200) |
|
||||
| 后继 | `classify_intent` (graph.py:201) |
|
||||
| 功能 | 深拷贝当前状态 → 推入 `history_states` 列表。最多保留 5 个快照。撤销时恢复到最新快照。 |
|
||||
| LLM | 否 |
|
||||
|
||||
### 5. classify_intent — 意图分类
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:200` |
|
||||
| 前驱 | `save_state_snapshot` (graph.py:201) |
|
||||
| 后继 | 6 路条件分发 (graph.py:204-215) |
|
||||
| 功能 | LLM 分类用户意图为 8 种之一。prompt: `prompts/intent_classify.md`。 |
|
||||
| LLM | 是 |
|
||||
| 路由函数 | `route_by_intent` (graph.py:67) |
|
||||
|
||||
**分类逻辑与路由目标**:
|
||||
|
||||
| 意图值 | 路由目标 | 说明 |
|
||||
|--------|---------|------|
|
||||
| `initial_generation` | → `retrieve` | 新建报表 |
|
||||
| `modify_report` | → `modify_jrxml` | 修改现有报表 |
|
||||
| `preview_report` | → `save_session` | 预览(跳过生成) |
|
||||
| `export_pdf` | → `save_session` | 导出 PDF(跳过生成) |
|
||||
| `export_jrxml` | → `save_session` | 下载 JRXML(跳过生成) |
|
||||
| `consult_question` | → `handle_consult` | 咨询问答 |
|
||||
| `undo_modification` | → `handle_undo` | 撤销 |
|
||||
| `reset_session` | → `handle_reset` | 重置 |
|
||||
| 未知/兜底 | 有 `current_jrxml` → `modify_jrxml`; 无 → `retrieve` | |
|
||||
|
||||
### 6. retrieve — RAG/知识库检索
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:442` |
|
||||
| 前驱 | `classify_intent` (graph.py:204-215, intent=initial_generation) |
|
||||
| 后继 | 条件分发: `generate_skeleton` 或 `generate` (graph.py:218-224) |
|
||||
| 功能 | ① ErrorKB 检索历史修正案例 → ② KB 模板检索 → ③ KB 字段定义检索。注入 `retrieved_context`、`kb_template_jrxml`、`kb_fields`。 |
|
||||
| LLM | 否(向量搜索 + 字段匹配) |
|
||||
| 路由函数 | `route_after_retrieve` (graph.py:94) |
|
||||
|
||||
**路由逻辑** (`route_after_retrieve`, graph.py:94-99):
|
||||
- `layout_schema.total_rows > 0` → `generate_skeleton` (3 阶段)
|
||||
- 否则 → `generate` (1-shot)
|
||||
|
||||
### 7. generate — 1-shot 生成
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:578` |
|
||||
| 前驱 | `retrieve` (graph.py:218-224, 无 layout_schema 时) |
|
||||
| 后继 | `save_session` (graph.py:227-231) |
|
||||
| 功能 | LLM 一次生成完整 JRXML。注入 OCR 上下文 + 模板上下文。流式输出。截断时续写(最多 3 轮)。 |
|
||||
| LLM | 是 |
|
||||
| Prompt | `prompts/initial_generation.md` |
|
||||
|
||||
### 8. generate_skeleton — 骨架生成(3 阶段-1)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:657` |
|
||||
| 前驱 | `retrieve` (graph.py:218-224, 有 layout_schema 时) |
|
||||
| 后继 | `refine_layout` (固定边 graph.py:233) |
|
||||
| 功能 | 压缩布局 schema → LLM 生成骨架 JRXML。字段用 `$F{field_N}` 占位。流式输出 + 续写。 |
|
||||
| LLM | 是 |
|
||||
| Prompt | `prompts/skeleton_generation.md` |
|
||||
|
||||
### 9. refine_layout — 坐标精调(3 阶段-2)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:879` |
|
||||
| 前驱 | `generate_skeleton` (graph.py:233) |
|
||||
| 后继 | `map_fields` (固定边 graph.py:234) |
|
||||
| 功能 | ① `decompose_jrxml()` 拆解为 header + bands → ② 每个 band 窗口化(>4000 字符切分)→ ③ 逐窗口 LLM 精调坐标 → ④ `reassemble_jrxml()` 重组 → ⑤ `validate_element_count()` 校验(>10% 回退)。header 完全不发给 LLM。 |
|
||||
| LLM | 是(N 次,N = band 窗口数) |
|
||||
| Prompt | `prompts/refine_layout.md` |
|
||||
|
||||
### 10. map_fields — 字段映射(3 阶段-3)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:978` |
|
||||
| 前驱 | `refine_layout` (graph.py:234) |
|
||||
| 后继 | `save_session` (graph.py:235-239) |
|
||||
| 功能 | 纯程序化正则替换 `$F{field_N}` → OCR 真实字段名。`_sanitize_field_name()` 净化非 ASCII 字符。零 LLM 调用。 |
|
||||
| LLM | 否 |
|
||||
|
||||
### 11. modify_jrxml — 修改报表
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:1022` |
|
||||
| 前驱 | `classify_intent` (graph.py:204-215, intent=modify_report) |
|
||||
| 后继 | `save_session` (graph.py:242-246) |
|
||||
| 功能 | 基于现有 JRXML + 用户修改描述 + OCR 上下文 + 模板上下文 → LLM 修改。流式输出 + 续写。空响应守卫。 |
|
||||
| LLM | 是 |
|
||||
| Prompt | `prompts/modification.md` |
|
||||
|
||||
### 12. handle_consult — 咨询解答
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:261` |
|
||||
| 前驱 | `classify_intent` (graph.py:204-215, intent=consult_question) |
|
||||
| 后继 | `finalize` (固定边 graph.py:280) |
|
||||
| 功能 | LLM 回答 JasperReports 相关知识问题。回答写入 `conversation_history`。 |
|
||||
| LLM | 是 |
|
||||
| Prompt | `prompts/consult.md` |
|
||||
|
||||
### 13. handle_undo — 撤销
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:281` |
|
||||
| 前驱 | `classify_intent` (graph.py:204-215, intent=undo_modification) |
|
||||
| 后继 | `save_session` (graph.py:249-253) |
|
||||
| 功能 | 从 `history_states` 弹出最近快照,恢复 `current_jrxml`、`conversation_history`、`status`。无快照时提示"无可撤销状态"。 |
|
||||
| LLM | 否 |
|
||||
|
||||
### 14. handle_reset — 重置
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:309` |
|
||||
| 前驱 | `classify_intent` (graph.py:204-215, intent=reset_session) |
|
||||
| 后继 | `finalize` (固定边 graph.py:281) |
|
||||
| 功能 | 清空所有状态到 `create_initial_state()` 默认值(保留 `session_id`、`session_name`)。 |
|
||||
| LLM | 否 |
|
||||
|
||||
### 15. save_session — 保存会话
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:325` |
|
||||
| 前驱 | `generate`、`map_fields`、`modify_jrxml`、`handle_undo`、`classify_intent`(预览/导出) |
|
||||
| 后继 | 条件分发: `validate` 或 `finalize` (graph.py:256-260) |
|
||||
| 功能 | 原子持久化会话 JSON (`tempfile + os.replace`)。序列化 `agent_state` 到 `sessions/{session_id}.json`。 |
|
||||
| LLM | 否 |
|
||||
| 路由函数 | `route_after_save` (graph.py:118) |
|
||||
|
||||
**路由逻辑** (`route_after_save`, graph.py:118-123):
|
||||
- `intent in (preview_report, export_pdf, export_jrxml)` → `finalize` (跳过验证)
|
||||
- 其他 → `validate`
|
||||
|
||||
### 16. validate — 验证
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:1235` |
|
||||
| 前驱 | `save_session` (graph.py:256-260)、`correct_jrxml` (graph.py:273-277) |
|
||||
| 后继 | 条件分发: `finalize` 或 `explain_error` (graph.py:263-267) |
|
||||
| 功能 | ① 结构检查(字段引用一致性/SQL 存在/pageWidth/pageHeight/name)→ ② XSD 校验(可选)→ ③ 像素对比(有上传图片时 Java 渲染 JRXML→PNG + OpenCV SSIM)。 |
|
||||
| LLM | 否 |
|
||||
| 路由函数 | `route_after_validate` (graph.py:127) |
|
||||
|
||||
**路由逻辑** (`route_after_validate`, graph.py:127-131):
|
||||
- `status == "pass"` → `finalize`
|
||||
- `status == "fail"` → `explain_error`
|
||||
|
||||
### 17. explain_error — 错误解释
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:1310` |
|
||||
| 前驱 | `validate` (graph.py:263-267, status=fail) |
|
||||
| 后继 | `correct_jrxml` (graph.py:268-272) |
|
||||
| 功能 | LLM 将编译错误翻译为自然语言解释。注入 `natural_explanation`。 |
|
||||
| LLM | 是 |
|
||||
| Prompt | `prompts/explain_error.md` |
|
||||
|
||||
### 18. correct_jrxml — 自动修正
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:1355` |
|
||||
| 前驱 | `explain_error` (graph.py:268-272) |
|
||||
| 后继 | 条件分发: `validate` 或 `finalize` (graph.py:273-277) |
|
||||
| 功能 | 基于错误解释 + OCR 上下文 + 模板上下文 → LLM 修正 JRXML。注入 `last_error_case`。去重检测(输入输出相同则 `retry_count+=2`)。 |
|
||||
| LLM | 是 |
|
||||
| Prompt | `prompts/correction.md` |
|
||||
| 路由函数 | `route_after_correct` (graph.py:139) |
|
||||
|
||||
**路由逻辑** (`route_after_correct`, graph.py:139-143):
|
||||
- `retry_count >= MAX_RETRY` (默认5) → `finalize` (放弃修正)
|
||||
- `retry_count < MAX_RETRY` → `validate` (重新验证)
|
||||
|
||||
### 19. finalize — 最终处理
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 代码位置 | `agent/nodes.py:1452` |
|
||||
| 前驱 | `validate`(pass)、`correct_jrxml`(retry>=MAX)、`handle_consult`、`handle_reset`、`save_session`(预览/导出) |
|
||||
| 后继 | `END` (graph.py:284) |
|
||||
| 功能 | 记录 `jrxml_versions` 版本历史。验证通过时设置 `final_jrxml`。失败时记录 `pending_failure_context` 供下次输入自动注入。 |
|
||||
| LLM | 否 |
|
||||
|
||||
---
|
||||
|
||||
## 路由函数索引
|
||||
|
||||
| # | 路由函数 | 代码位置 | 条件 | 分支 |
|
||||
|---|---------|---------|------|------|
|
||||
| R1 | `route_by_intent` | `graph.py:67` | `state.intent` | 6 路: retrieve / modify_jrxml / save_session / handle_consult / handle_undo / handle_reset |
|
||||
| R2 | `route_after_retrieve` | `graph.py:94` | `layout_schema.total_rows > 0` | 2 路: generate_skeleton / generate |
|
||||
| R3 | `route_after_generate` | `graph.py:103` | 无条件 | save_session |
|
||||
| R4 | `route_after_modify` | `graph.py:108` | 无条件 | save_session |
|
||||
| R5 | `route_after_undo` | `graph.py:113` | 无条件 | save_session |
|
||||
| R6 | `route_after_save` | `graph.py:118` | `intent in (preview, export)` | 2 路: finalize / validate |
|
||||
| R7 | `route_after_validate` | `graph.py:127` | `status == "pass"` | 2 路: finalize / explain_error |
|
||||
| R8 | `route_after_explain` | `graph.py:133` | 无条件 | correct_jrxml |
|
||||
| R9 | `route_after_correct` | `graph.py:139` | `retry_count >= MAX_RETRY` | 2 路: finalize / validate |
|
||||
|
||||
---
|
||||
|
||||
## 完整对话场景
|
||||
|
||||
### 场景 1: 新建报表 — 1-shot(无布局 schema)
|
||||
|
||||
**触发**: `intent=initial_generation` + 无图片/无结构化布局
|
||||
|
||||
**用户示例**: "帮我生成一个销售报表"、"生成一个包含客户名和金额的表格"
|
||||
|
||||
```
|
||||
load_session nodes.py:77
|
||||
→ process_input nodes.py:98
|
||||
→ manage_context nodes.py:143
|
||||
→ save_state_snapshot nodes.py:191
|
||||
→ classify_intent nodes.py:200 意图=initial_generation
|
||||
└─ R1: route_by_intent graph.py:67 → retrieve
|
||||
→ retrieve nodes.py:442
|
||||
└─ R2: route_after_retrieve graph.py:94 layout_schema 为空 → generate
|
||||
→ generate nodes.py:578 LLM 1-shot 生成完整 JRXML
|
||||
└─ R3: route_after_generate graph.py:103 → save_session
|
||||
→ save_session nodes.py:325 持久化到磁盘
|
||||
└─ R6: route_after_save graph.py:118 intent=initial_generation → validate
|
||||
→ validate nodes.py:1235 结构检查 + XSD + 像素对比
|
||||
└─ R7: route_after_validate graph.py:127
|
||||
├─ status=pass → finalize nodes.py:1452 → END ✓
|
||||
└─ status=fail → explain_error nodes.py:1310
|
||||
└─ R8 → correct_jrxml nodes.py:1355
|
||||
└─ R9:
|
||||
retry<5 → validate (循环)
|
||||
retry>=5 → finalize → END ✗
|
||||
```
|
||||
|
||||
**LLM 调用**: `classify_intent` + `generate` + 最多 5× (`explain_error` + `correct_jrxml`)
|
||||
**退出好结局**: `final_jrxml` 有值, `status=pass`
|
||||
**退出坏结局**: `pending_failure_context` 有值, `retry_count=5`
|
||||
|
||||
---
|
||||
|
||||
### 场景 2: 新建报表 — 3 阶段分层生成(有布局 schema)
|
||||
|
||||
**触发**: `intent=initial_generation` + 上传图片 + OCR 提取到 `layout_schema.total_rows > 0`
|
||||
|
||||
**用户示例**: 上传销售单图片 → "根据这个模板生成报表"
|
||||
|
||||
```
|
||||
load_session nodes.py:77
|
||||
→ process_input nodes.py:98 OCR提取 + 布局分析
|
||||
→ manage_context nodes.py:143
|
||||
→ save_state_snapshot nodes.py:191
|
||||
→ classify_intent nodes.py:200 意图=initial_generation
|
||||
└─ R1: route_by_intent graph.py:67 → retrieve
|
||||
→ retrieve nodes.py:442 KB检索模板+字段
|
||||
└─ R2: route_after_retrieve graph.py:94 layout_schema.total_rows>0 → generate_skeleton
|
||||
→ generate_skeleton nodes.py:657 阶段1: 骨架JRXML ($F{field_N}占位)
|
||||
→ refine_layout nodes.py:879 阶段2: Band级窗口化坐标精调
|
||||
→ map_fields nodes.py:978 阶段3: 程序化字段映射
|
||||
└─ R3: route_after_generate graph.py:103 → save_session
|
||||
→ save_session nodes.py:325
|
||||
└─ R6: route_after_save graph.py:118 → validate
|
||||
→ validate nodes.py:1235
|
||||
└─ R7 同场景1的验证循环
|
||||
```
|
||||
|
||||
**内容保护**:
|
||||
- `refine_layout`: header (field/param/queryString) 完全不发给 LLM
|
||||
- `refine_layout`: 每窗口 ~4000 字符, LLM 无法重写整个报表
|
||||
- `map_fields`: 纯正则替换, 零 LLM, 100% 确定性
|
||||
- `validate_element_count()`: 每阶段后校验, >10% 变化回退
|
||||
|
||||
**LLM 调用**: `classify_intent` + `generate_skeleton` + N×`refine_layout`(N=band窗口数) + 可能的修正循环
|
||||
|
||||
---
|
||||
|
||||
### 场景 3: 修改已有报表
|
||||
|
||||
**触发**: `intent=modify_report`(已有 `current_jrxml`)
|
||||
|
||||
**用户示例**: "把标题字体改大"、"在底部加合计行"、"删除第三列"
|
||||
|
||||
```
|
||||
load_session → process_input → manage_context → save_state_snapshot
|
||||
→ classify_intent nodes.py:200 意图=modify_report
|
||||
└─ R1: route_by_intent graph.py:67 → modify_jrxml
|
||||
→ modify_jrxml nodes.py:1022 LLM修改现有JRXML
|
||||
└─ R4: route_after_modify graph.py:108 → save_session
|
||||
→ save_session nodes.py:325
|
||||
└─ R6: route_after_save graph.py:118 → validate
|
||||
→ (同场景1的验证循环)
|
||||
```
|
||||
|
||||
**特殊逻辑**: `correct_jrxml` 去重检测: 输入输出相同 → `retry_count += 2`
|
||||
|
||||
---
|
||||
|
||||
### 场景 4: 预览 / 导出(跳过验证)
|
||||
|
||||
**触发**: `intent in (preview_report, export_pdf, export_jrxml)`
|
||||
|
||||
**用户示例**: "预览报表"、"导出 PDF"、"下载 JRXML"
|
||||
|
||||
```
|
||||
load_session → process_input → manage_context → save_state_snapshot
|
||||
→ classify_intent nodes.py:200 意图=preview/export
|
||||
└─ R1: route_by_intent graph.py:67 → save_session
|
||||
→ save_session nodes.py:325
|
||||
└─ R6: route_after_save graph.py:118 intent=preview/export → finalize
|
||||
→ finalize nodes.py:1452 → END ✓
|
||||
```
|
||||
|
||||
**LLM 调用**: 仅 `classify_intent` (1次)
|
||||
**跳过**: generate / modify_jrxml / validate / correct_jrxml
|
||||
|
||||
---
|
||||
|
||||
### 场景 5: 咨询问答
|
||||
|
||||
**触发**: `intent=consult_question`
|
||||
|
||||
**用户示例**: "JasperReports 里 $F 和 $P 有什么区别?"、"怎么设置页脚?"
|
||||
|
||||
```
|
||||
load_session → process_input → manage_context → save_state_snapshot
|
||||
→ classify_intent nodes.py:200 意图=consult_question
|
||||
└─ R1: route_by_intent graph.py:67 → handle_consult
|
||||
→ handle_consult nodes.py:261 LLM回答
|
||||
→ finalize nodes.py:1452 → END ✓
|
||||
```
|
||||
|
||||
**LLM 调用**: `classify_intent` + `handle_consult` (2次)
|
||||
|
||||
---
|
||||
|
||||
### 场景 6: 撤销
|
||||
|
||||
**触发**: `intent=undo_modification`
|
||||
|
||||
**用户示例**: "撤销"、"回退"、"恢复到修改前"
|
||||
|
||||
```
|
||||
load_session → process_input → manage_context → save_state_snapshot
|
||||
→ classify_intent nodes.py:200 意图=undo_modification
|
||||
└─ R1: route_by_intent graph.py:67 → handle_undo
|
||||
→ handle_undo nodes.py:281 恢复history_states快照
|
||||
└─ R5: route_after_undo graph.py:113 → save_session
|
||||
→ save_session nodes.py:325
|
||||
└─ R6 → validate → (验证循环)
|
||||
```
|
||||
|
||||
**LLM 调用**: 仅 `classify_intent` (1次)
|
||||
**特殊**: 无快照时提示"无可撤销状态",不改变当前状态
|
||||
|
||||
---
|
||||
|
||||
### 场景 7: 重置
|
||||
|
||||
**触发**: `intent=reset_session`
|
||||
|
||||
**用户示例**: "重置"、"重新开始"、"清空对话"
|
||||
|
||||
```
|
||||
load_session → process_input → manage_context → save_state_snapshot
|
||||
→ classify_intent nodes.py:200 意图=reset_session
|
||||
└─ R1: route_by_intent graph.py:67 → handle_reset
|
||||
→ handle_reset nodes.py:309 清空到初始状态
|
||||
→ finalize nodes.py:1452 → END ✓
|
||||
```
|
||||
|
||||
**LLM 调用**: 仅 `classify_intent` (1次)
|
||||
|
||||
---
|
||||
|
||||
### 场景 8: 兜底路由(未知意图)
|
||||
|
||||
**触发**: LLM 分类返回非标准意图
|
||||
|
||||
```
|
||||
load_session → ... → classify_intent → [未知意图]
|
||||
└─ R1 fallback (graph.py:87-90):
|
||||
├─ state有current_jrxml → modify_jrxml (走修改路径, →场景3)
|
||||
└─ state无current_jrxml → retrieve (走生成路径, →场景1/2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AgentState 字段速查
|
||||
|
||||
| 字段 | 类型 | 写节点 | 读节点 |
|
||||
|------|------|--------|--------|
|
||||
| `intent` | `str` | classify_intent | R1 route_by_intent, R6 route_after_save |
|
||||
| `current_jrxml` | `str` | generate, generate_skeleton, refine_layout, map_fields, modify_jrxml, correct_jrxml, handle_undo | validate, save_session, finalize |
|
||||
| `user_input` | `str` | process_input | classify_intent, manage_context |
|
||||
| `user_modification_request` | `str` | process_input | modify_jrxml |
|
||||
| `conversation_history` | `list` | process_input, finalize, handle_consult | manage_context, classify_intent, modify_jrxml |
|
||||
| `full_conversation_history` | `list` | process_input | manage_context |
|
||||
| `compressed_history` | `str` | manage_context | modify_jrxml, handle_consult |
|
||||
| `retry_count` | `int` | correct_jrxml, validate | R7 route_after_correct |
|
||||
| `status` | `str` | validate | R7 route_after_validate, finalize |
|
||||
| `error_msg` | `str` | validate | explain_error, finalize |
|
||||
| `natural_explanation` | `str` | explain_error | correct_jrxml |
|
||||
| `final_jrxml` | `str` | finalize | (用户下载) |
|
||||
| `jrxml_versions` | `list` | finalize | (前端展示) |
|
||||
| `last_error_case` | `dict` | correct_jrxml | retrieve |
|
||||
| `pending_failure_context` | `dict` | finalize | process_input (下次) |
|
||||
| `layout_schema` | `dict` | process_input | R2 route_after_retrieve, generate_skeleton |
|
||||
| `ocr_elements` | `list` | process_input | refine_layout, generate_skeleton |
|
||||
| `ocr_extraction_result` | `dict` | process_input | map_fields, modify_jrxml, correct_jrxml |
|
||||
| `history_states` | `list` | save_state_snapshot | handle_undo |
|
||||
| `kb_id` | `str` | process_input | retrieve |
|
||||
| `kb_fields` | `list` | retrieve | generate_skeleton |
|
||||
| `uploaded_template_jrxml` | `str` | process_input | generate, generate_skeleton, modify_jrxml, correct_jrxml |
|
||||
|
||||
---
|
||||
|
||||
## LLM 调用统计
|
||||
|
||||
| 场景 | classify | 生成节点 | 窗口数 | 修正循环 | 总计(最小~最大) |
|
||||
|------|----------|---------|--------|---------|----------------|
|
||||
| 1-shot 生成 | 1 | generate=1 | - | 0~5×2 | 2 ~ 12 |
|
||||
| 3 阶段生成 | 1 | skeleton+refine×N | N | 0~5×2 | 2+N ~ 12+N |
|
||||
| 修改报表 | 1 | modify=1 | - | 0~5×2 | 2 ~ 12 |
|
||||
| 预览/导出 | 1 | - | - | - | 1 |
|
||||
| 咨询 | 1 | consult=1 | - | - | 2 |
|
||||
| 撤销 | 1 | - | - | - | 1 |
|
||||
| 重置 | 1 | - | - | - | 1 |
|
||||
|
||||
> N = band 窗口数。`销售单.jrxml` (73k 字符) 拆解后 N≈17。
|
||||
|
||||
---
|
||||
|
||||
## 修正循环流程
|
||||
|
||||
```
|
||||
validate ──fail──→ explain_error ──→ correct_jrxml
|
||||
▲ │
|
||||
│ retry_count < MAX_RETRY(5) │
|
||||
└──────────────────────────────────────┘
|
||||
│
|
||||
│ retry_count >= 5
|
||||
▼
|
||||
finalize (放弃, 记录pending_failure_context)
|
||||
```
|
||||
|
||||
**修正轮次推进**:
|
||||
1. `validate` 失败 → `status="fail"`, `error_msg` 有值
|
||||
2. `explain_error` → LLM 翻译错误 → `natural_explanation` 有值
|
||||
3. `correct_jrxml` → LLM 修正 → `retry_count += 1`。去重检测:输入输出相同 → `retry_count += 2`
|
||||
4. `route_after_correct` → retry<5 → 回到 `validate`; retry>=5 → `finalize`
|
||||
|
||||
**失败上下文** (`pending_failure_context`): 重试耗尽后记录 `{error_msg, bad_jrxml, retry_count, ts}`,下次用户消息时 `process_input` 自动注入到 prompt。
|
||||
|
||||
---
|
||||
|
||||
## 边定义索引(graph.py 全部边)
|
||||
|
||||
| 类型 | 源节点 | 目标节点 | 位置 |
|
||||
|------|--------|---------|------|
|
||||
| 固定边 | load_session | process_input | line 198 |
|
||||
| 固定边 | process_input | manage_context | line 199 |
|
||||
| 固定边 | manage_context | save_state_snapshot | line 200 |
|
||||
| 固定边 | save_state_snapshot | classify_intent | line 201 |
|
||||
| 条件边 | classify_intent | retrieve / modify_jrxml / save_session / handle_consult / handle_undo / handle_reset | lines 204-215 |
|
||||
| 条件边 | retrieve | generate / generate_skeleton | lines 218-224 |
|
||||
| 条件边 | generate | save_session | lines 227-231 |
|
||||
| 固定边 | generate_skeleton | refine_layout | line 233 |
|
||||
| 固定边 | refine_layout | map_fields | line 234 |
|
||||
| 条件边 | map_fields | save_session | lines 235-239 |
|
||||
| 条件边 | modify_jrxml | save_session | lines 242-246 |
|
||||
| 条件边 | handle_undo | save_session | lines 249-253 |
|
||||
| 条件边 | save_session | validate / finalize | lines 256-260 |
|
||||
| 条件边 | validate | finalize / explain_error | lines 263-267 |
|
||||
| 条件边 | explain_error | correct_jrxml | lines 268-272 |
|
||||
| 条件边 | correct_jrxml | validate / finalize | lines 273-277 |
|
||||
| 固定边 | handle_consult | finalize | line 280 |
|
||||
| 固定边 | handle_reset | finalize | line 281 |
|
||||
| 固定边 | finalize | END | line 284 |
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,54 @@
|
||||
# JRXML Agent 前端
|
||||
|
||||
Vue 3 + TypeScript + Vite + Pinia — JRXML 报表生成代理的 Web UI。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Vue 3** (Composition API + `<script setup>`)
|
||||
- **TypeScript** 6.x
|
||||
- **Vite** 8.x
|
||||
- **Pinia** 3.x (状态管理)
|
||||
- **SSE** (Server-Sent Events) 流式响应
|
||||
|
||||
## 组件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/client.ts SSE 客户端 + fetch 封装
|
||||
├── stores/
|
||||
│ ├── chat.ts Pinia: 消息/流式/节点进度/文件
|
||||
│ ├── session.ts Pinia: 会话 CRUD
|
||||
│ └── kb.ts Pinia: 多租户知识库管理
|
||||
├── components/
|
||||
│ ├── Sidebar.vue 会话列表 + 下载 + 历史版本
|
||||
│ ├── ChatMessages.vue 消息列表渲染
|
||||
│ ├── ProcessSection.vue 处理过程折叠区(<details>/<summary>)
|
||||
│ ├── StreamingMessage.vue 流式消息显示
|
||||
│ ├── NodeProgress.vue 节点进度指示器
|
||||
│ ├── UnifiedInput.vue 统一输入框(文本+文件拖拽/粘贴)
|
||||
│ ├── SummaryCard.vue 结果摘要卡片
|
||||
│ ├── KbSelector.vue KB 下拉选择器
|
||||
│ └── KbManager.vue KB 管理面板(创建/上传/构建/删除)
|
||||
└── utils/format.ts 工具函数
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # 启动开发服务器 (localhost:5173)
|
||||
npm run build # 生产构建
|
||||
npx playwright test # E2E 测试
|
||||
```
|
||||
|
||||
## SSE 事件流
|
||||
|
||||
前端通过 `api.chat()` 发起 POST 请求,后端返回 `text/event-stream`:
|
||||
|
||||
| 事件 | 说明 |
|
||||
|------|------|
|
||||
| `node_start` | 节点开始执行(含 node/label/step_index) |
|
||||
| `node_complete` | 节点执行完成(含 detail) |
|
||||
| `stream_token` | LLM 逐字输出 |
|
||||
| `agent_complete` | 全图执行完成(含 intent/status/jrxml_length/error 等) |
|
||||
| `agent_error` | 执行异常 |
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1439
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@types/node": "^24.12.3",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.0.12",
|
||||
"vue-tsc": "^3.2.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
timeout: 60000,
|
||||
expect: { timeout: 10000 },
|
||||
retries: 0,
|
||||
use: {
|
||||
baseURL: "http://localhost:5173",
|
||||
headless: true,
|
||||
screenshot: "only-on-failure",
|
||||
trace: "retain-on-failure",
|
||||
},
|
||||
webServer: {
|
||||
command: "npm run dev",
|
||||
url: "http://localhost:5173",
|
||||
reuseExistingServer: true,
|
||||
timeout: 30000,
|
||||
},
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,196 @@
|
||||
<script setup lang="ts">
|
||||
import { watch, nextTick, ref } from 'vue'
|
||||
import { useChatStore } from './stores/chat'
|
||||
import { useSessionStore } from './stores/session'
|
||||
import { api } from './api/client'
|
||||
import Sidebar from './components/Sidebar.vue'
|
||||
import ChatMessages from './components/ChatMessages.vue'
|
||||
import ProcessSection from './components/ProcessSection.vue'
|
||||
import SummaryCard from './components/SummaryCard.vue'
|
||||
import UnifiedInput from './components/UnifiedInput.vue'
|
||||
import KbSelector from './components/KbSelector.vue'
|
||||
import KbManager from './components/KbManager.vue'
|
||||
import { useKbStore } from './stores/kb'
|
||||
|
||||
const chat = useChatStore()
|
||||
const session = useSessionStore()
|
||||
const kb = useKbStore()
|
||||
|
||||
function handleKbChange(kbId: string) {
|
||||
if (session.currentId) {
|
||||
kb.bindKbToSession(session.currentId, kbId)
|
||||
}
|
||||
}
|
||||
|
||||
const chatContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
async function scrollToBottom() {
|
||||
await nextTick()
|
||||
if (chatContainer.value) {
|
||||
chatContainer.value.scrollTop = chatContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [chat.messages.length, chat.streamText],
|
||||
() => scrollToBottom(),
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
async function handleSend(text: string, files: File[]) {
|
||||
if (!session.currentId) {
|
||||
const sid = await session.createSession()
|
||||
await session.switchSession(sid)
|
||||
}
|
||||
|
||||
// Upload files first
|
||||
const remoteIds: string[] = []
|
||||
for (const f of files) {
|
||||
try {
|
||||
const info = await api.uploadFile(f, session.currentId)
|
||||
remoteIds.push(info.file_id)
|
||||
} catch (e) {
|
||||
console.error('文件上传失败:', e)
|
||||
chat.setError('文件上传失败')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
chat.addMessage({ role: 'user', content: text || '[附加文件]' })
|
||||
scrollToBottom()
|
||||
|
||||
chat.startStreaming()
|
||||
|
||||
try {
|
||||
await api.chat(session.currentId, text, remoteIds, {
|
||||
onNodeStart(data) {
|
||||
chat.addNode({ node: data.node, label: data.label, step_index: data.step_index })
|
||||
},
|
||||
onNodeComplete(data) {
|
||||
chat.completeNode(data)
|
||||
},
|
||||
onStreamToken(data) {
|
||||
chat.appendStreamToken(data.text)
|
||||
scrollToBottom()
|
||||
},
|
||||
onAgentComplete(data) {
|
||||
chat.finishStreaming({
|
||||
intent: data.intent,
|
||||
status: data.status,
|
||||
jrxml_length: data.jrxml_length,
|
||||
error_msg: data.error_msg,
|
||||
natural_explanation: data.natural_explanation,
|
||||
consult_answer: data.consult_answer,
|
||||
retry_count: data.retry_count,
|
||||
total_duration_ms: data.total_duration_ms,
|
||||
ocr_extraction_result: data.ocr_extraction_result,
|
||||
})
|
||||
|
||||
const streamContent = chat.streamText
|
||||
if (data.status === 'pass') {
|
||||
if (streamContent) {
|
||||
chat.addMessage({ role: 'assistant', content: streamContent, type: 'jrxml' })
|
||||
}
|
||||
chat.addMessage({ role: 'assistant', content: 'JRXML 生成成功!可从侧边栏下载。', type: 'success' })
|
||||
} else if (data.status && data.status !== 'pass') {
|
||||
chat.addMessage({
|
||||
role: 'assistant',
|
||||
content: `经过 ${data.retry_count} 次重试后失败。\n\n错误: ${data.error_msg}${data.natural_explanation ? '\n\n原因: ' + data.natural_explanation : ''}`,
|
||||
type: 'error',
|
||||
})
|
||||
} else if (data.intent === 'consult_question') {
|
||||
// 咨询回答:优先用 streamContent,其次用 consult_answer
|
||||
const answerText = streamContent || data.consult_answer || ''
|
||||
if (answerText) {
|
||||
chat.addMessage({ role: 'assistant', content: answerText, type: 'consult' })
|
||||
} else {
|
||||
chat.addMessage({ role: 'assistant', content: '咨询已完成,但未获取到回答内容。', type: 'error' })
|
||||
}
|
||||
} else {
|
||||
if (streamContent) {
|
||||
chat.addMessage({ role: 'assistant', content: streamContent, type: 'jrxml' })
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh session sidebar data after a short delay
|
||||
setTimeout(() => session.refreshFromApi(), 500)
|
||||
},
|
||||
onAgentError(data) {
|
||||
chat.setError(data.error)
|
||||
chat.addMessage({ role: 'assistant', content: `执行异常: ${data.error}`, type: 'error' })
|
||||
setTimeout(() => session.refreshFromApi(), 500)
|
||||
},
|
||||
})
|
||||
} catch (e: any) {
|
||||
chat.setError(e.message || '网络请求失败')
|
||||
chat.addMessage({ role: 'assistant', content: `请求失败: ${e.message}`, type: 'error' })
|
||||
chat.finishStreaming({ status: '' })
|
||||
} finally {
|
||||
if (chat.streaming) {
|
||||
chat.finishStreaming({ status: '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<Sidebar @quickAction="(text) => handleSend(text, [])" />
|
||||
|
||||
<main class="main-area">
|
||||
<KbSelector @change="handleKbChange" />
|
||||
<div class="chat-container" ref="chatContainer">
|
||||
<ChatMessages />
|
||||
<ProcessSection />
|
||||
<SummaryCard />
|
||||
</div>
|
||||
|
||||
<UnifiedInput
|
||||
:disabled="chat.streaming"
|
||||
@send="handleSend"
|
||||
/>
|
||||
</main>
|
||||
|
||||
<KbManager />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #11111b;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
/** JSON fetch wrapper + SSE streaming helper. */
|
||||
|
||||
const BASE = '/api'
|
||||
|
||||
export interface SessionSummary {
|
||||
session_id: string
|
||||
session_name: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface SessionData extends SessionSummary {
|
||||
agent_state: Record<string, any>
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
file_id: string
|
||||
filename: string
|
||||
content_type: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface AgentCompleteData {
|
||||
reason: string
|
||||
intent: string
|
||||
status: string
|
||||
jrxml_length: number
|
||||
error_msg: string
|
||||
natural_explanation: string
|
||||
consult_answer: string
|
||||
retry_count: number
|
||||
total_duration_ms: number
|
||||
ocr_extraction_result: any
|
||||
}
|
||||
|
||||
export interface SSECallbacks {
|
||||
onNodeStart?: (data: { node: string; label: string; step_index: number }) => void
|
||||
onNodeComplete?: (data: { node: string; label: string; detail: string }) => void
|
||||
onStreamToken?: (data: { text: string; type: string }) => void
|
||||
onAgentComplete?: (data: AgentCompleteData) => void
|
||||
onAgentError?: (data: { error: string; traceback?: string }) => void
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// ── Health ──
|
||||
async health() {
|
||||
const r = await fetch(`${BASE}/health`)
|
||||
return r.json()
|
||||
},
|
||||
|
||||
async config() {
|
||||
const r = await fetch(`${BASE}/config`)
|
||||
return r.json()
|
||||
},
|
||||
|
||||
// ── Sessions ──
|
||||
async createSession(): Promise<SessionSummary> {
|
||||
const r = await fetch(`${BASE}/sessions`, { method: 'POST' })
|
||||
return r.json()
|
||||
},
|
||||
|
||||
async listSessions(): Promise<SessionSummary[]> {
|
||||
const r = await fetch(`${BASE}/sessions`)
|
||||
const data = await r.json()
|
||||
return data.sessions
|
||||
},
|
||||
|
||||
async getSession(sessionId: string): Promise<SessionData> {
|
||||
const r = await fetch(`${BASE}/sessions/${sessionId}`)
|
||||
if (!r.ok) throw new Error('会话不存在')
|
||||
return r.json()
|
||||
},
|
||||
|
||||
async deleteSession(sessionId: string): Promise<void> {
|
||||
await fetch(`${BASE}/sessions/${sessionId}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
// ── Upload ──
|
||||
async uploadFile(file: File, sessionId: string): Promise<FileInfo> {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const r = await fetch(`${BASE}/upload?session_id=${encodeURIComponent(sessionId)}`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
if (!r.ok) throw new Error('上传失败')
|
||||
return r.json()
|
||||
},
|
||||
|
||||
// ── Chat (SSE) ──
|
||||
async chat(
|
||||
sessionId: string,
|
||||
text: string,
|
||||
fileIds: string[],
|
||||
callbacks: SSECallbacks,
|
||||
): Promise<void> {
|
||||
const r = await fetch(`${BASE}/sessions/${sessionId}/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text, file_ids: fileIds }),
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const err = await r.json().catch(() => ({ detail: r.statusText }))
|
||||
throw new Error(err.detail || '请求失败')
|
||||
}
|
||||
|
||||
const reader = r.body!.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
let currentEvent = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
currentEvent = line.slice(7).trim()
|
||||
} else if (line.startsWith('data: ')) {
|
||||
const payload = JSON.parse(line.slice(6))
|
||||
switch (currentEvent) {
|
||||
case 'node_start':
|
||||
callbacks.onNodeStart?.(payload)
|
||||
break
|
||||
case 'node_complete':
|
||||
callbacks.onNodeComplete?.(payload)
|
||||
break
|
||||
case 'stream_token':
|
||||
callbacks.onStreamToken?.(payload)
|
||||
break
|
||||
case 'agent_complete':
|
||||
callbacks.onAgentComplete?.(payload)
|
||||
break
|
||||
case 'agent_error':
|
||||
callbacks.onAgentError?.(payload)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { useChatStore, type Message } from '../stores/chat'
|
||||
import { formatTime } from '../utils/format'
|
||||
|
||||
const chat = useChatStore()
|
||||
|
||||
function renderContent(msg: Message): { text: string; isXml: boolean } {
|
||||
if (msg.type === 'jrxml') {
|
||||
return { text: msg.content, isXml: true }
|
||||
}
|
||||
return { text: msg.content, isXml: false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-messages" ref="scrollRef">
|
||||
<div v-if="chat.messages.length === 0 && !chat.streaming" class="empty-state">
|
||||
<div class="empty-icon">📋</div>
|
||||
<p>开始对话 — 描述您需要的报表</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="msg in chat.messages"
|
||||
:key="msg.id"
|
||||
class="message"
|
||||
:class="`msg-${msg.role}`"
|
||||
>
|
||||
<div class="msg-header">
|
||||
<span class="msg-role">{{ msg.role === 'user' ? '您' : 'AI' }}</span>
|
||||
<span class="msg-time">{{ formatTime(msg.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="msg-body">
|
||||
<template v-if="renderContent(msg).isXml">
|
||||
<pre class="code-block">{{ renderContent(msg).text }}</pre>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="markdown-body" v-text="renderContent(msg).text"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="msg.type === 'error'" class="msg-tag error-tag">错误</div>
|
||||
<div v-if="msg.type === 'success'" class="msg-tag success-tag">成功</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #6c7086;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.msg-user {
|
||||
margin-left: auto;
|
||||
background: #313244;
|
||||
}
|
||||
|
||||
.msg-assistant {
|
||||
margin-right: auto;
|
||||
background: #1e1e2e;
|
||||
border: 1px solid #313244;
|
||||
}
|
||||
|
||||
.msg-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.msg-role {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #cba6f7;
|
||||
}
|
||||
|
||||
.msg-time {
|
||||
font-size: 11px;
|
||||
color: #6c7086;
|
||||
}
|
||||
|
||||
.msg-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #cdd6f4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #11111b;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
color: #a6e3a1;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.msg-tag {
|
||||
display: inline-block;
|
||||
margin-top: 6px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.error-tag {
|
||||
background: #f38ba8;
|
||||
color: #1e1e2e;
|
||||
}
|
||||
|
||||
.success-tag {
|
||||
background: #a6e3a1;
|
||||
color: #1e1e2e;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useKbStore } from '../stores/kb'
|
||||
|
||||
const kb = useKbStore()
|
||||
|
||||
const newKbName = ref('')
|
||||
const newKbDesc = ref('')
|
||||
const creating = ref(false)
|
||||
const uploading = ref<string | null>(null)
|
||||
const building = ref<string | null>(null)
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
onMounted(() => { kb.init() })
|
||||
|
||||
async function handleCreate() {
|
||||
if (!newKbName.value.trim()) return
|
||||
creating.value = true
|
||||
await kb.createKb(newKbName.value.trim(), newKbDesc.value.trim())
|
||||
newKbName.value = ''
|
||||
newKbDesc.value = ''
|
||||
creating.value = false
|
||||
}
|
||||
|
||||
async function handleDelete(kbId: string) {
|
||||
if (confirm('确定删除此知识库?所有文件和数据将被永久删除。')) {
|
||||
await kb.deleteKb(kbId)
|
||||
}
|
||||
}
|
||||
|
||||
function triggerUpload(kbId: string) {
|
||||
uploading.value = kbId
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleFileSelect(e: Event, kbId: string) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (input.files && input.files.length > 0) {
|
||||
for (const f of input.files) {
|
||||
await kb.uploadFileToKb(kbId, f)
|
||||
}
|
||||
await kb.buildKb(kbId)
|
||||
await kb.refreshKbs()
|
||||
}
|
||||
input.value = ''
|
||||
uploading.value = null
|
||||
}
|
||||
|
||||
async function handleBuild(kbId: string) {
|
||||
building.value = kbId
|
||||
await kb.buildKb(kbId)
|
||||
building.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="kb.showManager" class="kb-manager-overlay" @click.self="kb.showManager = false">
|
||||
<div class="kb-manager">
|
||||
<h3>知识库管理</h3>
|
||||
|
||||
<div class="create-form">
|
||||
<input v-model="newKbName" class="kb-input" placeholder="知识库名称" :disabled="creating" />
|
||||
<input v-model="newKbDesc" class="kb-input" placeholder="描述(可选)" :disabled="creating" />
|
||||
<button class="kb-btn primary" :disabled="creating || !newKbName.trim()" @click="handleCreate">
|
||||
{{ creating ? '创建中...' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="kb.loading" class="empty">加载中...</div>
|
||||
<div v-else-if="kb.kbs.length === 0" class="empty">暂无知识库</div>
|
||||
|
||||
<div v-for="k in kb.kbs" :key="k.kb_id" class="kb-card">
|
||||
<div class="kb-card-header">
|
||||
<strong>{{ k.name }}</strong>
|
||||
<span class="kb-status" :class="k.parse_status">
|
||||
{{ k.parse_status === 'ready' ? '就绪' : k.parse_status === 'partial' ? '部分' : '空' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="kb-meta">
|
||||
{{ k.field_count }}字段 · {{ k.template_count }}模板 ·
|
||||
{{ k.file_count }}文件 · {{ k.chunk_count }}块
|
||||
</div>
|
||||
<div class="kb-actions">
|
||||
<button class="kb-btn" @click="triggerUpload(k.kb_id)" :disabled="uploading === k.kb_id">
|
||||
{{ uploading === k.kb_id ? '上传中...' : '上传文件' }}
|
||||
</button>
|
||||
<button class="kb-btn" @click="handleBuild(k.kb_id)" :disabled="building === k.kb_id || k.file_count === 0">
|
||||
{{ building === k.kb_id ? '构建中...' : '构建' }}
|
||||
</button>
|
||||
<button class="kb-btn danger" @click="handleDelete(k.kb_id)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="kb-btn close-btn" @click="kb.showManager = false">关闭</button>
|
||||
|
||||
<input ref="fileInput" type="file" multiple
|
||||
accept=".jrxml,.md,.xlsx,.xls,.docx,.doc,.pdf,.csv,.txt,.json,.zip,.tar,.gz"
|
||||
style="display:none"
|
||||
@change="(e: Event) => { const kbId = uploading; if (kbId) handleFileSelect(e, kbId); }" />
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.kb-manager-overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 100;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.kb-manager {
|
||||
background: #1e1e2e;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
width: 540px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
h3 { margin: 0 0 16px; font-size: 18px; }
|
||||
.create-form { display: flex; gap: 8px; margin-bottom: 16px; }
|
||||
.kb-input {
|
||||
flex: 1;
|
||||
background: #181825;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 6px;
|
||||
color: #cdd6f4;
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
.kb-input:focus { border-color: #cba6f7; }
|
||||
.kb-btn {
|
||||
background: #313244; border: none; border-radius: 6px;
|
||||
color: #cdd6f4; padding: 6px 12px; font-size: 12px;
|
||||
cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.kb-btn:hover:not(:disabled) { background: #45475a; }
|
||||
.kb-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.kb-btn.primary { background: #cba6f7; color: #1e1e2e; }
|
||||
.kb-btn.primary:hover:not(:disabled) { background: #b4befe; }
|
||||
.kb-btn.danger { color: #f38ba8; }
|
||||
.kb-btn.danger:hover:not(:disabled) { background: #f38ba8; color: #1e1e2e; }
|
||||
.empty { text-align: center; color: #6c7086; padding: 24px 0; }
|
||||
.kb-card {
|
||||
background: #181825; border: 1px solid #313244;
|
||||
border-radius: 8px; padding: 12px; margin-bottom: 8px;
|
||||
}
|
||||
.kb-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||||
.kb-status { font-size: 11px; padding: 1px 6px; border-radius: 4px; }
|
||||
.kb-status.ready { background: #a6e3a1; color: #1e1e2e; }
|
||||
.kb-status.partial { background: #fab387; color: #1e1e2e; }
|
||||
.kb-status.empty { background: #45475a; color: #a6adc8; }
|
||||
.kb-meta { font-size: 11px; color: #6c7086; margin-bottom: 8px; }
|
||||
.kb-actions { display: flex; gap: 6px; }
|
||||
.close-btn { display: block; margin: 16px auto 0; }
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useKbStore } from '../stores/kb'
|
||||
|
||||
const kb = useKbStore()
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [kbId: string]
|
||||
}>()
|
||||
|
||||
function handleChange(e: Event) {
|
||||
const kbId = (e.target as HTMLSelectElement).value
|
||||
kb.selectKb(kbId)
|
||||
if (kbId) {
|
||||
kb.fetchKbFields(kbId)
|
||||
}
|
||||
emit('change', kbId)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
kb.init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="kb-selector">
|
||||
<label class="kb-label">知识库</label>
|
||||
<select
|
||||
class="kb-select"
|
||||
:value="kb.currentKbId"
|
||||
@change="handleChange"
|
||||
>
|
||||
<option value="">-- 不使用知识库 --</option>
|
||||
<option
|
||||
v-for="k in kb.kbs"
|
||||
:key="k.kb_id"
|
||||
:value="k.kb_id"
|
||||
>
|
||||
{{ k.name }} ({{ k.field_count }}字段, {{ k.template_count }}模板)
|
||||
</option>
|
||||
</select>
|
||||
<button class="kb-manage-btn" @click="kb.showManager = !kb.showManager" title="管理知识库">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span v-if="kb.currentKbName" class="kb-badge">当前: {{ kb.currentKbName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.kb-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: #181825;
|
||||
border-bottom: 1px solid #313244;
|
||||
}
|
||||
|
||||
.kb-label {
|
||||
font-size: 12px;
|
||||
color: #6c7086;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kb-select {
|
||||
flex: 1;
|
||||
background: #1e1e2e;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 6px;
|
||||
color: #cdd6f4;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.kb-select:focus {
|
||||
border-color: #cba6f7;
|
||||
}
|
||||
|
||||
.kb-manage-btn {
|
||||
background: #313244;
|
||||
border: none;
|
||||
color: #a6adc8;
|
||||
border-radius: 6px;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kb-manage-btn:hover {
|
||||
background: #45475a;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.kb-badge {
|
||||
font-size: 11px;
|
||||
color: #a6e3a1;
|
||||
background: #1e1e2e;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { useChatStore } from '../stores/chat'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const chat = useChatStore()
|
||||
|
||||
const visibleNodes = computed(() =>
|
||||
chat.nodes.filter(n => n.node !== 'load_session'
|
||||
&& n.node !== 'process_input'
|
||||
&& n.node !== 'manage_context'
|
||||
&& n.node !== 'save_state_snapshot'
|
||||
&& n.node !== 'save_session')
|
||||
)
|
||||
|
||||
const currentLabel = computed(() => {
|
||||
const running = visibleNodes.value.find(n => n.status === 'running')
|
||||
return running ? running.label : null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="chat.streaming && visibleNodes.length > 0" class="node-progress">
|
||||
<div class="progress-label">
|
||||
{{ currentLabel || '处理中...' }}
|
||||
</div>
|
||||
<div class="progress-dots">
|
||||
<span
|
||||
v-for="n in visibleNodes"
|
||||
:key="n.node"
|
||||
class="dot"
|
||||
:class="{ done: n.status === 'done', active: n.status === 'running' }"
|
||||
></span>
|
||||
</div>
|
||||
<div class="progress-detail" v-if="visibleNodes[visibleNodes.length - 1]?.detail">
|
||||
{{ visibleNodes[visibleNodes.length - 1].detail }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.node-progress {
|
||||
padding: 8px 16px;
|
||||
margin: 0 24px 8px;
|
||||
background: #181825;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #313244;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #89b4fa;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.progress-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #45475a;
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
background: #f9e2af;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.dot.done {
|
||||
background: #a6e3a1;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.progress-detail {
|
||||
font-size: 11px;
|
||||
color: #6c7086;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<script setup lang="ts">
|
||||
import { useChatStore, type ProcessSection } from '../stores/chat'
|
||||
|
||||
const chat = useChatStore()
|
||||
|
||||
function sectionClass(s: ProcessSection): string {
|
||||
if (s.status === 'running') return 'section-running'
|
||||
if (s.content) return 'section-done'
|
||||
return 'section-internal'
|
||||
}
|
||||
|
||||
function isXmlLike(text: string): boolean {
|
||||
return text.includes('<?xml') || text.includes('<jasperReport')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="chat.sections.length > 0" class="process-sections">
|
||||
<div class="sections-header">
|
||||
<template v-if="chat.streaming">
|
||||
<span class="pulse-dot"></span>
|
||||
处理中 · {{ chat.formatDuration(chat.totalDurationMs) }}
|
||||
</template>
|
||||
<template v-else-if="chat.error">
|
||||
<span class="error-icon">✕</span>
|
||||
执行异常 · {{ chat.formatDuration(chat.totalDurationMs) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="done-icon">✓</span>
|
||||
完成 · {{ chat.formatDuration(chat.totalDurationMs) }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<details
|
||||
v-for="s in chat.sections"
|
||||
:key="s.node"
|
||||
:open="s.expanded"
|
||||
class="process-section"
|
||||
:class="sectionClass(s)"
|
||||
@toggle="(e: Event) => { const d = e.target as HTMLDetailsElement; s.expanded = d.open }"
|
||||
>
|
||||
<summary class="section-summary">
|
||||
<span class="step-badge">{{ s.stepIndex }}</span>
|
||||
<span class="step-label">{{ s.label }}</span>
|
||||
<span v-if="s.status === 'running'" class="step-spinner">...</span>
|
||||
<span v-else class="step-check">OK</span>
|
||||
<span class="step-duration" v-if="s.durationMs > 0">
|
||||
{{ chat.formatDuration(s.durationMs) }}
|
||||
</span>
|
||||
<span class="step-detail-short" v-if="s.status === 'done' && s.detail">
|
||||
{{ s.detail }}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="section-content" v-if="s.content">
|
||||
<pre v-if="isXmlLike(s.content)" class="xml-content">{{ s.content }}</pre>
|
||||
<div v-else class="text-content">{{ s.content }}</div>
|
||||
</div>
|
||||
<div v-else-if="s.status === 'running'" class="section-waiting">
|
||||
等待生成...
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.process-sections {
|
||||
margin: 0 24px 8px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.sections-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #89b4fa;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #89b4fa;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: #f38ba8;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.done-icon {
|
||||
color: #a6e3a1;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.process-section {
|
||||
margin-bottom: 4px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #313244;
|
||||
background: #181825;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.process-section.section-running {
|
||||
border-color: #45475a;
|
||||
background: #1e1e2e;
|
||||
}
|
||||
|
||||
.process-section.section-internal {
|
||||
opacity: 0.65;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.section-summary {
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.section-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.step-badge {
|
||||
background: #313244;
|
||||
color: #6c7086;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-running .step-badge {
|
||||
background: #45475a;
|
||||
color: #cba6f7;
|
||||
}
|
||||
|
||||
.section-done .step-badge {
|
||||
background: #313244;
|
||||
color: #a6e3a1;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
color: #cdd6f4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section-internal .step-label {
|
||||
color: #6c7086;
|
||||
}
|
||||
|
||||
.step-spinner {
|
||||
color: #f9e2af;
|
||||
font-weight: bold;
|
||||
animation: pulse-text 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-text {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.step-check {
|
||||
color: #a6e3a1;
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.step-duration {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
color: #6c7086;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.step-detail-short {
|
||||
font-size: 11px;
|
||||
color: #6c7086;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
|
||||
.xml-content {
|
||||
background: #11111b;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
color: #a6e3a1;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
color: #bac2de;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.section-waiting {
|
||||
padding: 8px 10px 12px;
|
||||
font-size: 12px;
|
||||
color: #6c7086;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,311 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useSessionStore } from '../stores/session'
|
||||
import { useChatStore } from '../stores/chat'
|
||||
|
||||
const session = useSessionStore()
|
||||
const chat = useChatStore()
|
||||
|
||||
const emit = defineEmits<{
|
||||
quickAction: [text: string]
|
||||
}>()
|
||||
|
||||
function handlePreview() {
|
||||
emit('quickAction', '预览报表')
|
||||
}
|
||||
|
||||
function handleUndo() {
|
||||
emit('quickAction', '撤销上一步修改')
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
emit('quickAction', '重新来,清空当前报表')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
session.loadSessions()
|
||||
})
|
||||
|
||||
async function handleNew() {
|
||||
const sid = await session.createSession()
|
||||
await session.switchSession(sid)
|
||||
chat.reset()
|
||||
}
|
||||
|
||||
async function handleSwitch(sid: string) {
|
||||
await session.switchSession(sid)
|
||||
chat.reset()
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!session.currentId) return
|
||||
if (!confirm('确定要删除当前会话吗?')) return
|
||||
await session.deleteCurrent()
|
||||
chat.reset()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>JRXML Agent</h2>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="section-title">
|
||||
<span>会话列表</span>
|
||||
<button class="btn-icon" @click="handleNew" title="新建会话">+</button>
|
||||
</div>
|
||||
|
||||
<div class="session-list">
|
||||
<div
|
||||
v-for="s in session.sortedSessions"
|
||||
:key="s.session_id"
|
||||
class="session-item"
|
||||
:class="{ active: s.session_id === session.currentId }"
|
||||
@click="handleSwitch(s.session_id)"
|
||||
>
|
||||
<span class="session-name">{{ s.session_name }}</span>
|
||||
<span class="session-time">{{ s.updated_at?.slice(0, 10) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="session.currentId"
|
||||
class="btn-delete"
|
||||
@click="handleDelete"
|
||||
>
|
||||
删除当前会话
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section" v-if="session.currentId">
|
||||
<div class="section-title">快捷操作</div>
|
||||
<div class="quick-actions">
|
||||
<button
|
||||
class="btn-action btn-preview"
|
||||
:disabled="!session.hasJrxml"
|
||||
@click="handlePreview"
|
||||
>预览</button>
|
||||
<button
|
||||
class="btn-action btn-undo"
|
||||
:disabled="!session.hasHistory"
|
||||
@click="handleUndo"
|
||||
>撤销</button>
|
||||
<button
|
||||
class="btn-action btn-reset"
|
||||
@click="handleReset"
|
||||
>重置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="section-title">下载</div>
|
||||
<a
|
||||
v-if="session.currentJrxml"
|
||||
:href="`/api/sessions/${session.currentId}/download/latest`"
|
||||
class="btn-download"
|
||||
download
|
||||
>
|
||||
下载最新 JRXML
|
||||
</a>
|
||||
<div v-else class="btn-download disabled">暂无下载文件</div>
|
||||
<div v-if="session.versions.length > 1" class="version-list">
|
||||
<div class="version-list-title">历史版本</div>
|
||||
<a
|
||||
v-for="(v, i) in session.versions"
|
||||
:key="i"
|
||||
:href="`/api/sessions/${session.currentId}/download/${i}`"
|
||||
class="version-item"
|
||||
download
|
||||
>
|
||||
<span class="version-label">{{ v.label || `版本 ${i + 1}` }}</span>
|
||||
<span class="version-time">{{ v.ts?.slice(0, 16)?.replace('T', ' ') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<span>v5.0</span>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
height: 100vh;
|
||||
background: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #313244;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px 16px 12px;
|
||||
border-bottom: 1px solid #313244;
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #cba6f7;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #313244;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: #a6adc8;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: #313244;
|
||||
color: #cdd6f4;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.btn-icon:hover { background: #45475a; }
|
||||
|
||||
.session-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.session-item:hover { background: #313244; }
|
||||
.session-item.active { background: #45475a; }
|
||||
|
||||
.session-name {
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
.session-time {
|
||||
font-size: 11px;
|
||||
color: #6c7086;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
display: block;
|
||||
width: calc(100% - 16px);
|
||||
margin: 8px 8px 0;
|
||||
padding: 6px 0;
|
||||
border: 1px solid #45475a;
|
||||
background: none;
|
||||
color: #f38ba8;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-delete:hover { background: #45475a; }
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
flex: 1;
|
||||
padding: 6px 0;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #cdd6f4;
|
||||
background: none;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-action:hover:not(:disabled) { background: #45475a; }
|
||||
.btn-action:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.btn-preview { border-color: #a6e3a1; color: #a6e3a1; }
|
||||
.btn-undo { border-color: #f9e2af; color: #f9e2af; }
|
||||
.btn-reset { border-color: #f38ba8; color: #f38ba8; }
|
||||
|
||||
.btn-download {
|
||||
display: block;
|
||||
margin: 4px 16px;
|
||||
padding: 8px 0;
|
||||
background: #cba6f7;
|
||||
color: #1e1e2e;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-download:hover { background: #b4befe; }
|
||||
.btn-download.disabled {
|
||||
background: #313244;
|
||||
color: #6c7086;
|
||||
cursor: not-allowed;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.version-list {
|
||||
margin-top: 8px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.version-list-title {
|
||||
font-size: 11px;
|
||||
color: #6c7086;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
font-size: 12px;
|
||||
color: #a6adc8;
|
||||
text-decoration: none;
|
||||
}
|
||||
.version-item:hover { color: #cba6f7; }
|
||||
.version-time { font-size: 11px; color: #6c7086; }
|
||||
|
||||
.sidebar-footer {
|
||||
margin-top: auto;
|
||||
padding: 12px 16px;
|
||||
font-size: 11px;
|
||||
color: #585b70;
|
||||
border-top: 1px solid #313244;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { useChatStore } from '../stores/chat'
|
||||
|
||||
const chat = useChatStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="chat.streaming" class="streaming-message">
|
||||
<div class="stream-header">
|
||||
<span class="stream-label">AI 正在生成...</span>
|
||||
</div>
|
||||
<pre class="stream-code" v-if="chat.streamText">{{ chat.streamText }}</pre>
|
||||
<div v-else class="stream-waiting">
|
||||
<span class="dot-pulse"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.streaming-message {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
background: #1e1e2e;
|
||||
border: 1px solid #45475a;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.stream-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stream-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #f9e2af;
|
||||
}
|
||||
|
||||
.stream-code {
|
||||
background: #11111b;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
color: #a6e3a1;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.stream-waiting {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dot-pulse::after {
|
||||
content: '...';
|
||||
animation: dots 1.5s steps(4, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0%, 20% { content: '.'; }
|
||||
40% { content: '..'; }
|
||||
60%, 100% { content: '...'; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useSessionStore } from '../stores/session'
|
||||
import { useChatStore } from '../stores/chat'
|
||||
|
||||
const session = useSessionStore()
|
||||
const chat = useChatStore()
|
||||
|
||||
const visible = computed(() =>
|
||||
!chat.streaming && chat.summary.status !== ''
|
||||
)
|
||||
|
||||
function downloadLatest() {
|
||||
if (session.currentId) {
|
||||
window.open(`/api/sessions/${session.currentId}/download/latest`, '_blank')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="summary-card">
|
||||
<div v-if="chat.summary.status === 'pass'" class="card card-success">
|
||||
<div class="card-title">JRXML 生成成功</div>
|
||||
<div class="card-text">
|
||||
生成 {{ chat.summary.jrxml_length }} 字符
|
||||
<span v-if="chat.lastDurationMs > 0" class="card-duration">
|
||||
· {{ chat.formatDuration(chat.lastDurationMs) }}
|
||||
</span>
|
||||
</div>
|
||||
<button class="card-btn" @click="downloadLatest">下载 JRXML</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="card card-error">
|
||||
<div class="card-title">
|
||||
经过 {{ chat.summary.retry_count }} 次重试后仍失败
|
||||
<span v-if="chat.lastDurationMs > 0" class="card-duration">
|
||||
· {{ chat.formatDuration(chat.lastDurationMs) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-text">{{ chat.summary.error_msg }}</div>
|
||||
<div v-if="chat.summary.natural_explanation" class="card-reason">
|
||||
{{ chat.summary.natural_explanation }}
|
||||
</div>
|
||||
<div class="card-hint">请继续描述修改需求,系统会自动加载失败上下文。</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.summary-card {
|
||||
margin: 12px 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.card-success {
|
||||
background: #1e1e2e;
|
||||
border-color: #a6e3a1;
|
||||
}
|
||||
|
||||
.card-error {
|
||||
background: #1e1e2e;
|
||||
border-color: #f38ba8;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-success .card-title {
|
||||
color: #a6e3a1;
|
||||
}
|
||||
|
||||
.card-error .card-title {
|
||||
color: #f38ba8;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 13px;
|
||||
color: #cdd6f4;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-duration {
|
||||
color: #6c7086;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card-reason {
|
||||
font-size: 12px;
|
||||
color: #a6adc8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-hint {
|
||||
font-size: 11px;
|
||||
color: #6c7086;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.card-btn {
|
||||
margin-top: 8px;
|
||||
padding: 6px 16px;
|
||||
background: #a6e3a1;
|
||||
color: #1e1e2e;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-btn:hover {
|
||||
background: #94e2d5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,274 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { fileIcon } from '../utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
disabled: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
send: [text: string, files: File[]]
|
||||
}>()
|
||||
|
||||
const text = ref('')
|
||||
const attachedFiles = ref<{ id: string; name: string; file: File }[]>([])
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const canSend = computed(() =>
|
||||
!props.disabled && (text.value.trim() || attachedFiles.value.length > 0)
|
||||
)
|
||||
|
||||
function triggerFileInput() {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
function handleFileSelect(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (input.files) {
|
||||
for (const f of input.files) {
|
||||
attachedFiles.value.push({ id: crypto.randomUUID(), name: f.name, file: f })
|
||||
}
|
||||
}
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
function removeFile(id: string) {
|
||||
attachedFiles.value = attachedFiles.value.filter(f => f.id !== id)
|
||||
}
|
||||
|
||||
function handleSend() {
|
||||
if (!canSend.value) return
|
||||
emit('send', text.value, attachedFiles.value.map(f => f.file))
|
||||
text.value = ''
|
||||
attachedFiles.value = []
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-resize textarea
|
||||
function autoResize() {
|
||||
const el = textareaRef.value
|
||||
if (el) {
|
||||
el.style.height = 'auto'
|
||||
el.style.height = Math.min(el.scrollHeight, 120) + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
// Drag & drop
|
||||
function handleDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
if (e.dataTransfer?.files) {
|
||||
for (const f of e.dataTransfer.files) {
|
||||
attachedFiles.value.push({ id: crypto.randomUUID(), name: f.name, file: f })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragover(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
}
|
||||
|
||||
// Paste support
|
||||
function handlePaste(e: ClipboardEvent) {
|
||||
const items = e.clipboardData?.items
|
||||
if (!items) return
|
||||
for (const item of items) {
|
||||
if (item.kind === 'file') {
|
||||
const f = item.getAsFile()
|
||||
if (f) {
|
||||
e.preventDefault()
|
||||
attachedFiles.value.push({ id: crypto.randomUUID(), name: f.name || 'clipboard.png', file: f })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="unified-input" @drop="handleDrop" @dragover="handleDragover">
|
||||
<!-- File chips -->
|
||||
<div v-if="attachedFiles.length > 0" class="file-chips">
|
||||
<div v-for="f in attachedFiles" :key="f.id" class="chip">
|
||||
<span class="chip-icon">{{ fileIcon(f.name) }}</span>
|
||||
<span class="chip-name">{{ f.name }}</span>
|
||||
<button class="chip-remove" @click="removeFile(f.id)">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input row -->
|
||||
<div class="input-row">
|
||||
<button class="attach-btn" @click="triggerFileInput" title="附加文件">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="text"
|
||||
class="text-input"
|
||||
placeholder="描述您的报表需求... (Enter 发送, Shift+Enter 换行)"
|
||||
:disabled="disabled"
|
||||
@keydown="handleKeydown"
|
||||
@input="autoResize"
|
||||
@paste="handlePaste"
|
||||
rows="1"
|
||||
></textarea>
|
||||
|
||||
<button
|
||||
class="send-btn"
|
||||
:disabled="!canSend"
|
||||
@click="handleSend"
|
||||
title="发送"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*,.pdf,.docx,.doc,.xlsx,.xls,.txt,.csv,.jrxml"
|
||||
style="display:none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.unified-input {
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 12px;
|
||||
background: #1e1e2e;
|
||||
margin: 0 24px 16px;
|
||||
padding: 8px 12px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.unified-input:focus-within {
|
||||
border-color: #cba6f7;
|
||||
}
|
||||
|
||||
.file-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #313244;
|
||||
border-radius: 6px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chip-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chip-name {
|
||||
color: #cdd6f4;
|
||||
max-width: 140px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chip-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6c7086;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chip-remove:hover {
|
||||
color: #f38ba8;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.attach-btn {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
background: #313244;
|
||||
color: #a6adc8;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.attach-btn:hover {
|
||||
background: #45475a;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #cdd6f4;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
resize: none;
|
||||
outline: none;
|
||||
line-height: 1.5;
|
||||
padding: 8px 0;
|
||||
min-height: 36px;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.text-input::placeholder {
|
||||
color: #6c7086;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
background: #cba6f7;
|
||||
color: #1e1e2e;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
background: #b4befe;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
background: #45475a;
|
||||
color: #6c7086;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,219 @@
|
||||
/** Pinia store — chat messages + streaming state with per-section tracking. */
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export interface Message {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
type?: 'text' | 'jrxml' | 'error' | 'success' | 'consult'
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface NodeProgress {
|
||||
node: string
|
||||
label: string
|
||||
detail?: string
|
||||
status: 'running' | 'done'
|
||||
}
|
||||
|
||||
export interface ProcessSection {
|
||||
node: string
|
||||
label: string
|
||||
stepIndex: number
|
||||
detail: string
|
||||
content: string
|
||||
status: 'running' | 'done'
|
||||
expanded: boolean
|
||||
durationMs: number
|
||||
startTime: number
|
||||
}
|
||||
|
||||
export interface AgentSummary {
|
||||
intent: string
|
||||
status: string
|
||||
jrxml_length: number
|
||||
error_msg: string
|
||||
natural_explanation: string
|
||||
retry_count: number
|
||||
}
|
||||
|
||||
export interface UploadedFile {
|
||||
file_id: string
|
||||
filename: string
|
||||
content_type: string
|
||||
size: number
|
||||
preview?: string
|
||||
}
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const messages = ref<Message[]>([])
|
||||
const streaming = ref(false)
|
||||
const lastDurationMs = ref(0)
|
||||
const streamText = ref('')
|
||||
const nodes = ref<NodeProgress[]>([])
|
||||
const sections = ref<ProcessSection[]>([])
|
||||
const error = ref<string>('')
|
||||
const ocrResult = ref<any>(null)
|
||||
const uploadedFiles = ref<UploadedFile[]>([])
|
||||
const summary = ref<AgentSummary>({
|
||||
intent: '', status: '', jrxml_length: 0,
|
||||
error_msg: '', natural_explanation: '', retry_count: 0,
|
||||
})
|
||||
|
||||
const totalDurationMs = computed(() => {
|
||||
if (sections.value.length === 0) return 0
|
||||
const last = sections.value[sections.value.length - 1]
|
||||
return last.status === 'done'
|
||||
? last.startTime + last.durationMs - sections.value[0].startTime
|
||||
: Date.now() - sections.value[0].startTime
|
||||
})
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return `${ms}ms`
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
||||
const m = Math.floor(ms / 60000)
|
||||
const s = Math.round((ms % 60000) / 1000)
|
||||
return `${m}m${s}s`
|
||||
}
|
||||
|
||||
function addMessage(msg: Omit<Message, 'id' | 'timestamp'>) {
|
||||
messages.value.push({
|
||||
...msg,
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
function startStreaming() {
|
||||
streaming.value = true
|
||||
lastDurationMs.value = 0
|
||||
streamText.value = ''
|
||||
nodes.value = []
|
||||
sections.value = []
|
||||
error.value = ''
|
||||
summary.value = {
|
||||
intent: '', status: '', jrxml_length: 0,
|
||||
error_msg: '', natural_explanation: '', retry_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function appendStreamToken(text: string) {
|
||||
streamText.value += text
|
||||
const active = sections.value.find(s => s.status === 'running')
|
||||
if (active) {
|
||||
active.content += text
|
||||
}
|
||||
}
|
||||
|
||||
function addNode(node: { node: string; label: string; step_index?: number }) {
|
||||
nodes.value.push({ node: node.node, label: node.label, status: 'running' })
|
||||
const prev = sections.value.find(s => s.status === 'running')
|
||||
if (prev) {
|
||||
prev.status = 'done'
|
||||
prev.durationMs = Date.now() - prev.startTime
|
||||
prev.expanded = false
|
||||
}
|
||||
sections.value.push({
|
||||
node: node.node,
|
||||
label: node.label,
|
||||
stepIndex: node.step_index || sections.value.length + 1,
|
||||
detail: '',
|
||||
content: '',
|
||||
status: 'running',
|
||||
expanded: true,
|
||||
durationMs: 0,
|
||||
startTime: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
function completeNode(node: { node: string; label: string; detail: string }) {
|
||||
const existing = nodes.value.find(n => n.node === node.node)
|
||||
if (existing) {
|
||||
existing.status = 'done'
|
||||
existing.detail = node.detail
|
||||
}
|
||||
const sec = sections.value.find(s => s.node === node.node && s.status === 'running')
|
||||
if (sec) {
|
||||
sec.detail = node.detail
|
||||
sec.status = 'done'
|
||||
sec.durationMs = Date.now() - sec.startTime
|
||||
}
|
||||
}
|
||||
|
||||
function finishStreaming(data?: {
|
||||
intent?: string; status?: string; jrxml_length?: number
|
||||
error_msg?: string; natural_explanation?: string; consult_answer?: string; retry_count?: number
|
||||
total_duration_ms?: number; ocr_extraction_result?: any
|
||||
}) {
|
||||
streaming.value = false
|
||||
nodes.value.forEach(n => { n.status = 'done' })
|
||||
sections.value.forEach(s => {
|
||||
if (s.status === 'running') {
|
||||
s.status = 'done'
|
||||
s.durationMs = Date.now() - s.startTime
|
||||
}
|
||||
s.expanded = false
|
||||
})
|
||||
if (data) {
|
||||
lastDurationMs.value = data.total_duration_ms || 0
|
||||
summary.value = {
|
||||
intent: data.intent || '',
|
||||
status: data.status || '',
|
||||
jrxml_length: data.jrxml_length || 0,
|
||||
error_msg: data.error_msg || '',
|
||||
natural_explanation: data.natural_explanation || '',
|
||||
consult_answer: data.consult_answer || '',
|
||||
retry_count: data.retry_count || 0,
|
||||
}
|
||||
if (data.ocr_extraction_result) {
|
||||
ocrResult.value = data.ocr_extraction_result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setError(err: string) {
|
||||
error.value = err
|
||||
streaming.value = false
|
||||
sections.value.forEach(s => { s.status = 'done'; s.expanded = false })
|
||||
}
|
||||
|
||||
function toggleSection(node: string) {
|
||||
const sec = sections.value.find(s => s.node === node)
|
||||
if (sec) {
|
||||
sec.expanded = !sec.expanded
|
||||
}
|
||||
}
|
||||
|
||||
function addUploadedFile(file: UploadedFile) {
|
||||
uploadedFiles.value.push(file)
|
||||
}
|
||||
|
||||
function removeUploadedFile(fileId: string) {
|
||||
uploadedFiles.value = uploadedFiles.value.filter(f => f.file_id !== fileId)
|
||||
}
|
||||
|
||||
function reset() {
|
||||
messages.value = []
|
||||
streamText.value = ''
|
||||
nodes.value = []
|
||||
sections.value = []
|
||||
error.value = ''
|
||||
streaming.value = false
|
||||
ocrResult.value = null
|
||||
uploadedFiles.value = []
|
||||
summary.value = {
|
||||
intent: '', status: '', jrxml_length: 0,
|
||||
error_msg: '', natural_explanation: '', retry_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages, streaming, lastDurationMs, streamText, nodes, sections, error, ocrResult,
|
||||
uploadedFiles, summary, totalDurationMs,
|
||||
addMessage, startStreaming, appendStreamToken, addNode, completeNode,
|
||||
finishStreaming, setError, toggleSection, reset, formatDuration,
|
||||
addUploadedFile, removeUploadedFile,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,152 @@
|
||||
/** Pinia store — multi-tenant KB management. */
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface KbSummary {
|
||||
kb_id: string
|
||||
name: string
|
||||
description: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
field_count: number
|
||||
template_count: number
|
||||
file_count: number
|
||||
chunk_count: number
|
||||
parse_status: string
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
user_id: string
|
||||
name: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface KbTemplate {
|
||||
name: string
|
||||
file: string
|
||||
}
|
||||
|
||||
export interface KbField {
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
required: boolean
|
||||
}
|
||||
|
||||
export const useKbStore = defineStore('kb', () => {
|
||||
const users = ref<UserInfo[]>([])
|
||||
const currentUserId = ref('')
|
||||
const kbs = ref<KbSummary[]>([])
|
||||
const currentKbId = ref('')
|
||||
const currentKbName = ref('')
|
||||
const currentKbFields = ref<KbField[]>([])
|
||||
const currentKbTemplates = ref<KbTemplate[]>([])
|
||||
const loading = ref(false)
|
||||
const showManager = ref(false)
|
||||
|
||||
function setKbs(list: KbSummary[]) { kbs.value = list }
|
||||
|
||||
function selectKb(kbId: string) {
|
||||
currentKbId.value = kbId
|
||||
const kb = kbs.value.find(k => k.kb_id === kbId)
|
||||
if (kb) currentKbName.value = kb.name
|
||||
}
|
||||
|
||||
async function refreshUsers() {
|
||||
try {
|
||||
const r = await fetch('/api/users')
|
||||
const data = await r.json()
|
||||
users.value = data.users || []
|
||||
if (users.value.length > 0 && !currentUserId.value) {
|
||||
currentUserId.value = users.value[0].user_id
|
||||
}
|
||||
} catch (e) { console.error('获取用户列表失败:', e) }
|
||||
}
|
||||
|
||||
async function refreshKbs(userId?: string) {
|
||||
const uid = userId || currentUserId.value
|
||||
if (!uid) return
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await fetch(`/api/users/${uid}/kbs`)
|
||||
const data = await r.json()
|
||||
kbs.value = data.kbs || []
|
||||
} catch (e) { console.error('获取知识库列表失败:', e) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function createKb(name: string, description = ''): Promise<KbSummary | null> {
|
||||
if (!currentUserId.value) return null
|
||||
try {
|
||||
const r = await fetch(`/api/users/${currentUserId.value}/kbs`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description }),
|
||||
})
|
||||
if (!r.ok) throw new Error('创建失败')
|
||||
const kb = await r.json()
|
||||
await refreshKbs()
|
||||
return kb
|
||||
} catch (e) { console.error('创建知识库失败:', e); return null }
|
||||
}
|
||||
|
||||
async function deleteKb(kbId: string): Promise<boolean> {
|
||||
try {
|
||||
const r = await fetch(`/api/kbs/${kbId}`, { method: 'DELETE' })
|
||||
if (!r.ok) throw new Error('删除失败')
|
||||
if (currentKbId.value === kbId) { currentKbId.value = ''; currentKbName.value = '' }
|
||||
await refreshKbs()
|
||||
return true
|
||||
} catch (e) { console.error('删除知识库失败:', e); return false }
|
||||
}
|
||||
|
||||
async function uploadFileToKb(kbId: string, file: File): Promise<boolean> {
|
||||
try {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const r = await fetch(`/api/kbs/${kbId}/upload`, { method: 'POST', body: form })
|
||||
return r.ok
|
||||
} catch (e) { console.error('KB文件上传失败:', e); return false }
|
||||
}
|
||||
|
||||
async function buildKb(kbId: string): Promise<boolean> {
|
||||
try {
|
||||
const r = await fetch(`/api/kbs/${kbId}/build`, { method: 'POST' })
|
||||
if (!r.ok) throw new Error('构建失败')
|
||||
await refreshKbs()
|
||||
return true
|
||||
} catch (e) { console.error('KB构建失败:', e); return false }
|
||||
}
|
||||
|
||||
async function fetchKbFields(kbId: string) {
|
||||
try {
|
||||
const r = await fetch(`/api/kbs/${kbId}/fields`)
|
||||
const data = await r.json()
|
||||
currentKbFields.value = data.fields || []
|
||||
currentKbTemplates.value = data.templates || []
|
||||
} catch (e) { console.error('获取KB字段失败:', e) }
|
||||
}
|
||||
|
||||
async function bindKbToSession(sessionId: string, kbId: string) {
|
||||
try {
|
||||
await fetch(`/api/sessions/${sessionId}/kb`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ kb_id: kbId }),
|
||||
})
|
||||
} catch (e) { console.error('绑定KB失败:', e) }
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await refreshUsers()
|
||||
if (currentUserId.value) await refreshKbs()
|
||||
}
|
||||
|
||||
return {
|
||||
users, currentUserId, kbs, currentKbId, currentKbName,
|
||||
currentKbFields, currentKbTemplates, loading, showManager,
|
||||
setKbs, selectKb, refreshUsers, refreshKbs, createKb, deleteKb,
|
||||
uploadFileToKb, buildKb, fetchKbFields, bindKbToSession, init,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
/** Pinia store — session management. */
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { api, type SessionSummary } from '../api/client'
|
||||
|
||||
export const useSessionStore = defineStore('session', () => {
|
||||
const sessions = ref<SessionSummary[]>([])
|
||||
const currentId = ref<string>('')
|
||||
const currentName = ref<string>('')
|
||||
const versions = ref<any[]>([])
|
||||
const historyStates = ref<any[]>([])
|
||||
const currentJrxml = ref<string>('')
|
||||
|
||||
const hasJrxml = computed(() => !!currentJrxml.value)
|
||||
const hasHistory = computed(() => historyStates.value.length > 0)
|
||||
|
||||
const sortedSessions = computed(() =>
|
||||
[...sessions.value].sort((a, b) =>
|
||||
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
||||
)
|
||||
)
|
||||
|
||||
const currentSession = computed(() =>
|
||||
sessions.value.find(s => s.session_id === currentId.value)
|
||||
)
|
||||
|
||||
async function loadSessions() {
|
||||
try {
|
||||
sessions.value = await api.listSessions()
|
||||
} catch (e) {
|
||||
console.error('加载会话列表失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function createSession() {
|
||||
const s = await api.createSession()
|
||||
sessions.value.unshift(s)
|
||||
return s.session_id
|
||||
}
|
||||
|
||||
async function switchSession(sessionId: string) {
|
||||
currentId.value = sessionId
|
||||
try {
|
||||
const data = await api.getSession(sessionId)
|
||||
currentName.value = data.session_name
|
||||
const state = data.agent_state
|
||||
currentJrxml.value = state.current_jrxml || ''
|
||||
versions.value = state.jrxml_versions || []
|
||||
historyStates.value = state.history_states || []
|
||||
} catch (e) {
|
||||
console.error('加载会话失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCurrent() {
|
||||
if (!currentId.value) return
|
||||
await api.deleteSession(currentId.value)
|
||||
sessions.value = sessions.value.filter(s => s.session_id !== currentId.value)
|
||||
currentId.value = ''
|
||||
currentName.value = ''
|
||||
currentJrxml.value = ''
|
||||
versions.value = []
|
||||
}
|
||||
|
||||
async function refreshFromApi() {
|
||||
if (!currentId.value) return
|
||||
try {
|
||||
const data = await api.getSession(currentId.value)
|
||||
const state = data.agent_state
|
||||
currentJrxml.value = state.current_jrxml || ''
|
||||
versions.value = state.jrxml_versions || []
|
||||
historyStates.value = state.history_states || []
|
||||
} catch (e) {
|
||||
console.error('刷新会话状态失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function refreshFromState(agentState: Record<string, any>) {
|
||||
currentJrxml.value = agentState.current_jrxml || currentJrxml.value
|
||||
versions.value = agentState.jrxml_versions || versions.value
|
||||
historyStates.value = agentState.history_states || historyStates.value
|
||||
}
|
||||
|
||||
return {
|
||||
sessions, currentId, currentName, versions, historyStates, currentJrxml,
|
||||
hasJrxml, hasHistory, sortedSessions, currentSession,
|
||||
loadSessions, createSession, switchSession, deleteCurrent, refreshFromState, refreshFromApi,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
/** Date formatting and text utilities. */
|
||||
|
||||
export function formatTime(iso: string): string {
|
||||
if (!iso) return ''
|
||||
const d = new Date(iso)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
export function truncate(str: string, max: number): string {
|
||||
if (!str) return ''
|
||||
return str.length > max ? str.slice(0, max) + '...' : str
|
||||
}
|
||||
|
||||
export function fileIcon(filename: string): string {
|
||||
const ext = filename.split('.').pop()?.toLowerCase() || ''
|
||||
const map: Record<string, string> = {
|
||||
png: '🖼', jpg: '🖼', jpeg: '🖼', bmp: '🖼', webp: '🖼',
|
||||
pdf: '📄', docx: '📝', doc: '📝',
|
||||
xlsx: '📊', xls: '📊',
|
||||
txt: '📃', csv: '📃',
|
||||
}
|
||||
return map[ext] || '📎'
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* E2E tests: key user flows for the JRXML Agent frontend.
|
||||
*
|
||||
* Pre-requisites: npm run dev (reuseExistingServer in playwright.config).
|
||||
* API calls are intercepted by page.route() — no real backend needed.
|
||||
*/
|
||||
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────
|
||||
|
||||
function mockApi(page: any) {
|
||||
page.route("**/api/health", (route: any) =>
|
||||
route.fulfill({ json: { status: "ok", version: "5.0" } })
|
||||
);
|
||||
|
||||
page.route("**/api/sessions", (route: any) => {
|
||||
if (route.request().method() === "POST") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
session_id: "test12345678",
|
||||
session_name: "新建报表 2026-05-22",
|
||||
created_at: "2026-05-22T10:00:00.000Z",
|
||||
updated_at: "2026-05-22T10:00:00.000Z",
|
||||
},
|
||||
});
|
||||
}
|
||||
return route.fulfill({ json: { sessions: [] } });
|
||||
});
|
||||
|
||||
page.route("**/api/sessions/*/chat", (route: any) => {
|
||||
const sseBody = [
|
||||
"event: node_start",
|
||||
'data: {"node":"classify_intent","label":"识别意图","step_index":1}',
|
||||
"",
|
||||
"event: node_complete",
|
||||
'data: {"node":"classify_intent","label":"识别意图","detail":"意图: 新建报表"}',
|
||||
"",
|
||||
"event: agent_complete",
|
||||
'data: {"reason":"done","intent":"initial_generation","status":"pass","jrxml_length":42,"versions":1,"total_duration_ms":1200}',
|
||||
"",
|
||||
"",
|
||||
].join("\n");
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
body: sseBody,
|
||||
});
|
||||
});
|
||||
|
||||
page.route("**/api/upload", (route: any) =>
|
||||
route.fulfill({
|
||||
json: { file_id: "f001122334455", filename: "test.png", size: 1024 },
|
||||
})
|
||||
);
|
||||
|
||||
// Catch-all for GET/DELETE /api/sessions/:id (must fallback for POST to let chat route match)
|
||||
page.route("**/api/sessions/**", (route: any) => {
|
||||
if (route.request().method() === "DELETE") {
|
||||
return route.fulfill({ json: { status: "deleted" } });
|
||||
}
|
||||
if (route.request().method() === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
session_id: "test12345678",
|
||||
session_name: "测试会话",
|
||||
agent_state: { current_jrxml: "<jasperReport/>" },
|
||||
},
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
}
|
||||
|
||||
// ── tests ──────────────────────────────────────────────────────
|
||||
|
||||
test.describe("Page load", () => {
|
||||
test("renders sidebar and input area", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.locator("aside.sidebar")).toBeVisible();
|
||||
await expect(page.locator("h2")).toContainText("JRXML");
|
||||
await expect(page.locator(".unified-input")).toBeVisible();
|
||||
});
|
||||
|
||||
test("sidebar shows session list header and new button", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByText("会话列表")).toBeVisible();
|
||||
await expect(page.locator('button[title="新建会话"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Session management", () => {
|
||||
test("creates new session on button click", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await page.locator('button[title="新建会话"]').click();
|
||||
await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.locator(".session-item.active")).toBeVisible();
|
||||
});
|
||||
|
||||
test("can delete current session", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await page.locator('button[title="新建会话"]').click();
|
||||
await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
page.on("dialog", (dialog) => dialog.accept());
|
||||
await page.locator(".btn-delete").click();
|
||||
|
||||
await expect(page.locator(".session-item")).toHaveCount(0, { timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Chat flow", () => {
|
||||
test("sends text and displays user message + process section", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await page.locator('button[title="新建会话"]').click();
|
||||
await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const textarea = page.locator(".unified-input textarea");
|
||||
await textarea.fill("生成一个员工名册报表");
|
||||
await page.locator(".send-btn").click();
|
||||
|
||||
await expect(
|
||||
page.locator(".chat-messages .message.msg-user").filter({ hasText: "员工名册" })
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await expect(page.locator(".process-section")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("summary card appears after stream complete", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await page.locator('button[title="新建会话"]').click();
|
||||
await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
await page.locator(".unified-input textarea").fill("生成报表");
|
||||
await page.locator(".send-btn").click();
|
||||
|
||||
await expect(page.locator(".summary-card")).toBeVisible({ timeout: 15000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Input UX", () => {
|
||||
test("send button disabled when input empty", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.locator(".send-btn")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("send button enabled when text entered", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await page.locator(".unified-input textarea").fill("Hi");
|
||||
await expect(page.locator(".send-btn")).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
// ── KB (Knowledge Base) API mocks ───────────────────────────────
|
||||
|
||||
function mockKbApi(page: any) {
|
||||
mockApi(page);
|
||||
|
||||
page.route("**/api/users", (route: any) => {
|
||||
if (route.request().method() === "POST") {
|
||||
return route.fulfill({
|
||||
json: { user_id: "u_e2e_test_001", name: "E2E用户", created_at: "2026-05-23T00:00:00Z" },
|
||||
});
|
||||
}
|
||||
return route.fulfill({
|
||||
json: { users: [{ user_id: "u_e2e_test_001", name: "E2E用户", created_at: "2026-05-23T00:00:00Z" }] },
|
||||
});
|
||||
});
|
||||
|
||||
page.route("**/api/users/*/kbs", (route: any) => {
|
||||
if (route.request().method() === "POST") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
kb_id: "kb_e2e_001", user_id: "u_e2e_test_001",
|
||||
name: "E2E测试库", description: "",
|
||||
created_at: "2026-05-23T00:00:00Z", updated_at: "2026-05-23T00:00:00Z",
|
||||
fields: [], templates: [], file_count: 0, chunk_count: 0, parse_status: "empty",
|
||||
},
|
||||
});
|
||||
}
|
||||
return route.fulfill({
|
||||
json: {
|
||||
kbs: [{
|
||||
kb_id: "kb_e2e_001", name: "E2E测试库", description: "",
|
||||
created_at: "2026-05-23T00:00:00Z", updated_at: "2026-05-23T00:00:00Z",
|
||||
field_count: 10, template_count: 3, file_count: 2, chunk_count: 50, parse_status: "ready",
|
||||
}],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
page.route("**/api/kbs/*/status", (route: any) =>
|
||||
route.fulfill({ json: { parse_status: "ready", file_count: 2, chunk_count: 50 } })
|
||||
);
|
||||
page.route("**/api/kbs/*/fields", (route: any) =>
|
||||
route.fulfill({ json: {
|
||||
fields: [{ name: "billNo", description: "工单号", type: "String" }],
|
||||
templates: [{ name: "结算单", file: "结算单.jrxml" }],
|
||||
}})
|
||||
);
|
||||
page.route("**/api/kbs/*", (route: any) => {
|
||||
if (route.request().method() === "DELETE") {
|
||||
return route.fulfill({ json: { status: "deleted" } });
|
||||
}
|
||||
return route.fulfill({
|
||||
json: {
|
||||
kb_id: "kb_e2e_001", user_id: "u_e2e_test_001",
|
||||
name: "E2E测试库", description: "",
|
||||
fields: [], templates: [],
|
||||
file_count: 2, chunk_count: 50, parse_status: "ready",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
page.route("**/api/kbs/*/upload", (route: any) =>
|
||||
route.fulfill({ json: { filename: "test.jrxml", type: "jrxml", error: null } })
|
||||
);
|
||||
|
||||
page.route("**/api/sessions/*/kb", (route: any) => {
|
||||
if (route.request().method() === "PUT") {
|
||||
return route.fulfill({ json: { kb_id: "kb_e2e_001", kb_name: "E2E测试库" } });
|
||||
}
|
||||
return route.fulfill({ json: { kb_id: "kb_e2e_001", kb_name: "E2E测试库" } });
|
||||
});
|
||||
}
|
||||
|
||||
// ── KB feature tests ────────────────────────────────────────────
|
||||
|
||||
test.describe("KB selector", () => {
|
||||
test("KB selector renders in chat interface", async ({ page }) => {
|
||||
await mockKbApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.locator(".kb-selector")).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.locator(".kb-label")).toContainText("知识库");
|
||||
await expect(page.locator(".kb-select")).toBeVisible();
|
||||
await expect(page.locator(".kb-manage-btn")).toBeVisible();
|
||||
});
|
||||
|
||||
test("can select a KB from dropdown", async ({ page }) => {
|
||||
await mockKbApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
const select = page.locator(".kb-select");
|
||||
await expect(select).toBeVisible({ timeout: 5000 });
|
||||
await select.selectOption({ label: "E2E测试库 (10字段, 3模板)" });
|
||||
await expect(page.locator(".kb-badge")).toContainText("E2E测试库");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("KB manager", () => {
|
||||
test("opens KB manager overlay", async ({ page }) => {
|
||||
await mockKbApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await page.locator(".kb-manage-btn").click();
|
||||
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.locator(".kb-manager h3")).toContainText("知识库管理");
|
||||
});
|
||||
|
||||
test("can close KB manager", async ({ page }) => {
|
||||
await mockKbApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await page.locator(".kb-manage-btn").click();
|
||||
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
|
||||
await page.locator(".close-btn").click();
|
||||
await expect(page.locator(".kb-manager")).not.toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test("create form has name input and create button", async ({ page }) => {
|
||||
await mockKbApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await page.locator(".kb-manage-btn").click();
|
||||
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await expect(page.locator('.kb-manager .create-form .kb-input').first()).toBeVisible();
|
||||
await expect(page.locator('.kb-manager .create-form button.primary')).toBeVisible();
|
||||
});
|
||||
|
||||
test("KB cards show name, status, and actions", async ({ page }) => {
|
||||
await mockKbApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
await page.locator(".kb-manage-btn").click();
|
||||
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await expect(page.locator(".kb-card")).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.locator(".kb-card strong")).toContainText("E2E测试库");
|
||||
await expect(page.locator(".kb-status.ready")).toContainText("就绪");
|
||||
await expect(page.locator(".kb-actions button")).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("JRXML upload in chat", () => {
|
||||
test("file input accepts .jrxml extension", async ({ page }) => {
|
||||
await mockKbApi(page);
|
||||
await page.goto("/");
|
||||
|
||||
const input = page.locator('.unified-input input[type="file"]');
|
||||
await expect(input).toHaveAttribute("accept", /\.jrxml/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"types": ["vite/client"],
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "esnext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"kb_id": "49b972ec9e424f04aec34899c978f087",
|
||||
"user_id": "2db10c2ebbf6434aab28035026e196c3",
|
||||
"name": "smoke_kb",
|
||||
"description": "",
|
||||
"created_at": "2026-05-23T12:21:32.409028+00:00",
|
||||
"updated_at": "2026-05-23T12:21:32.409028+00:00",
|
||||
"fields": [],
|
||||
"templates": [],
|
||||
"file_count": 0,
|
||||
"chunk_count": 0,
|
||||
"parse_status": "empty"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"user_id": "2db10c2ebbf6434aab28035026e196c3",
|
||||
"name": "SmokeTest",
|
||||
"created_at": "2026-05-23T12:21:32.399217+00:00"
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+3950
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
+1059
File diff suppressed because it is too large
Load Diff
+2382
File diff suppressed because it is too large
Load Diff
+2381
File diff suppressed because it is too large
Load Diff
+2381
File diff suppressed because it is too large
Load Diff
+2289
File diff suppressed because it is too large
Load Diff
+67
@@ -0,0 +1,67 @@
|
||||
# 保险单接口文档
|
||||
|
||||
# 保险单接口文档
|
||||
|
||||
打印平台模版分类:保险单
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| companyTitle | 打印title | | String |
|
||||
| customerName | 客户姓名 | | String |
|
||||
| carNo | 车牌号 | | String |
|
||||
| vin | vin码 | | String |
|
||||
| carModel | 车型 | | String |
|
||||
| cellPhone | 手机号 | | String |
|
||||
| insureDate | 起保日期 | | String yyyy-MM-dd |
|
||||
| receivable | 应收金额 | | BigDecimal |
|
||||
| preferentialAmount | 优惠金额 | | BigDecimal |
|
||||
| receiveAmount | 实收金额 | | BigDecimal |
|
||||
| oweAmount | 未收金额 | | BigDecimal |
|
||||
| commissionAmountTotal | 手续费 | | BigDecimal |
|
||||
| companyRefundAmount | 保险公司返点 | | BigDecimal |
|
||||
| customerRefundAmount | 客户返点 | | BigDecimal |
|
||||
| insuranceCompanyName | 承保公司 | | String |
|
||||
| memo | 备注 | | String |
|
||||
| employeeName | 服务顾问 | | String |
|
||||
| contacts | 保险公司联系人 | | String |
|
||||
| contactMobile | 保险公司联系人手机号 | | String |
|
||||
| channelName | 来店途径 | | String |
|
||||
| startDate | 开始日期 | | String yyyy-MM-dd |
|
||||
| endDate | 结束日期 | | String yyyy-MM-dd |
|
||||
| renewal | 是否续保 0否/1是 | | Integer |
|
||||
| tsInsuranceDetailList | 保险单明细 | | List<TsInsuranceDetailPrintVo> |
|
||||
| | | | |
|
||||
| <br/><br/> | | | |
|
||||
|
||||
TsInsuranceDetailPrintVo:
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| policyNo | 保单号 | | String |
|
||||
| insuranceType | 保险类型(0交强险/1商业险) | | Integer |
|
||||
| name | 险种名称 | | String |
|
||||
| amount | 保额 | | BigDecimal |
|
||||
| receivable | 应收金额(元) | | BigDecimal |
|
||||
| discount | 折扣 | | BigDecimal |
|
||||
| concessionary | 优惠金额(元) | | BigDecimal |
|
||||
| commissionRate | 手续费率 | | BigDecimal |
|
||||
| commissionAmount | 手续费 | | BigDecimal |
|
||||
| paid | 实收金额(元) | | BigDecimal |
|
||||
| memo | 备注 | | String |
|
||||
| companyRefundAmount | 保险公司返点 | | BigDecimal |
|
||||
| customerRefundAmount | 客户返点 | | BigDecimal |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| <br/><br/> | | | |
|
||||
|
||||
> 更新: 2023-09-20 11:32:29 原文: <https://xcz.yuque.com/ombipo/rpc7ms/fpzmr5qph5mloy1x>
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
# 出/入库单据打印
|
||||
|
||||
# 现状梳理
|
||||
|
||||
| **场景** | | 入口 | 打印效果 | **底层模版** | 接口 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 库存 | 出入库单据-出库单 |  |  |  | REST:/stock/stockInAndOutBill/stockOutPrint?idStock=XXX&isNew=true<br>底层接口:com.f6car.stock.service.impl.print.PrintServiceImpl#getStockOutPrintUrl<br>日志关键字:+"出库单打印参数:" (有 apollo 开关 log.stockInout.print.switch 默认true) |
|
||||
| | 领料出库(工单领料)-出库单 |  |  |
|
||||
| | 出入库单据-入库单 |  |  |  | REST:/stock/stockInAndOutBill/stockInPrint?idStock=XXX&isNew=true<br>底层接口:com.f6car.stock.service.impl.print.PrintServiceImpl#getStockInPrintUrl<br>日志关键字:+"入库单打印参数:" (有 apollo 开关 log.stockInout.print.switch 默认true) |
|
||||
| | 领料出库(工单领料)-退料单 | <br> |  |
|
||||
| | 手工出入库-出库单 |  |  |  | REST:/stock/manual/print?pkId=XXX&billType=0&isNew=true (type=0 表示入库单 type=1 表示出库单)<br>底层接口:com.f6car.stock.service.print.PrintService#getManualStorageStockInPrintUrl<br>日志关键字:+"手工出入库单据打印入参是:" |
|
||||
| | 手工出入库-入库单 |  |  |  |
|
||||
| | 领料详情-打印领料单 |  |  |  | REST:/stock/maintain/print?idSourceBill=XXX&hasPreview=true<br>底层接口:com.f6car.stock.service.impl.print.PrintServiceImpl#getMaintainPrintUrl<br>日志关键字:+"领料单打印参数:" (有 apollo 开关 log.stock.maintain.print.switch 默认false) |
|
||||
|
||||
# 出入库单据-出库单 && 领料出库-出库单打印模版参数说明
|
||||
|
||||
出库单据定制类需求模版分类(newStockOutMaintainCustomPrint)--20250925新增
|
||||
|
||||
打印模版参数
|
||||
|
||||
HashMap<String, Object> resultMap
|
||||
|
||||
| **字段** | **说明** | 备注 |
|
||||
| --- | --- | --- |
|
||||
| title | 门店名称+ "出库单" | |
|
||||
| billNo | 出库单号 | |
|
||||
| sourceBillNo | 来源单号 | |
|
||||
| showSourceBillNo | 显示来源单号 (boolean) | |
|
||||
| billStatus | 单据状态(制单、完成) | |
|
||||
| inOutDate | 出库日期 | |
|
||||
| showInOutDate | 是否显示出库日期(boolean) | |
|
||||
| objectName | 出入库对象 | |
|
||||
| objectNameGD | 出入库对象工单<br/>工单出库单:客户姓名+车牌号整体+车辆VIN码+车辆品牌车系车型全称 (拼接后取前 80 个字符)<br/>非工单出库单:"" | |
|
||||
| creatorName | 制单人 | |
|
||||
| billDate | 制单日期 (yyyy-MM-dd) | |
|
||||
| sumNumber | 材料总数量 | |
|
||||
| sumAmount | 总金额 <br>\--脱敏场景显示 _\*_\*_\*\*_ | |
|
||||
| chineseAmount | 总金额(中文大写)<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| nowDateTime | 打印当前时间 (yyyy-MM-dd HH:mm) | |
|
||||
| idOwnOrg | 门店ID | |
|
||||
| remark | 备注信息 | |
|
||||
| showRemark | 是否显示备注(boolean)<br>\-- 备注不为空时是 true | |
|
||||
| isGdType | 是否是工单类型单据出库(boolean)<br>\-- 工单出库单是 true | |
|
||||
| saName | 服务顾问姓名<br>\-- 工单出库单场景 | |
|
||||
| printCount | 打印次数 | |
|
||||
| showCustomCode | 配置出入库打印参数-是否显示材料编码(boolean) | |
|
||||
| showBusinessLabel | 配置出入库打印参数-是否显示材料业务分类(boolean) | |
|
||||
| showApplyModel | 配置出入库打印参数-是否显示材料适用车型(boolean) | |
|
||||
| showStorageName | 配置出入库打印参数-是否显示出库仓库(boolean) | |
|
||||
| showDefSeat | 配置出入库打印参数-是否显示出库货位(boolean) | |
|
||||
| showChineseAmount | 配置出入库打印参数-是否显示大写金额(boolean) | |
|
||||
| showChineseSubtotal | 配置出入库打印参数-是否显示大写行合计(boolean) | |
|
||||
| columnCount | 显示几列 | |
|
||||
| batchPrintConfig | 配置出入库打印参数-查询批次成本展示设置 0:都不展示,1:总成本(将批次成本合并),2和3:展示批次成本 | |
|
||||
| memo | 车主描述 | \--20250925 新增 |
|
||||
| partInfoDetailMapList | | |
|
||||
| sortNumber | 序号<br>**通用模版追加一行合计行,显示 合计** | |
|
||||
| partShowName | 材料组合名称 | |
|
||||
| partName | 材料名称 | |
|
||||
| partBrand | 材料品牌 | \-- 2025.02.27 新增 |
|
||||
| supplierCode | 零件号 | |
|
||||
| labelName | 业务分类 | |
|
||||
| applyModel | 适用车型 | |
|
||||
| customCode | 材料编码 | |
|
||||
| storageName | 仓库名称 | |
|
||||
| defSeat | 货位 | |
|
||||
| number | 数量<br>**\-- 通用模版追加一行合计行,显示 材料出库总数** | |
|
||||
| unit | 单位 | |
|
||||
| price | 单价<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| subtotal | 金额<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| employeeName | 出库人 | |
|
||||
| salesEmployeeNameList | 材料行销售人员 | \--20250925 新增 |
|
||||
| orderBatchList<br>\--List<Map<String, String>> | 材料行成本相关 | |
|
||||
| orderNo | 批次号<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示 ""<br>**\-- 通用模版追加一行合计,追加行,显示 ""** | |
|
||||
| count | 批次出库几个<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示 ""<br>**\-- 通用模版追加一行合计,追加行,显示总数** | |
|
||||
| price | 批次单位成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 "" | |
|
||||
| priceNoTax | 批次单位除税成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 "" | 2025.08.14 新增 |
|
||||
| totalPrice | 批次总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本 | |
|
||||
| totalPriceNoTax | 批次除税总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价除税总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本 | 2025.08.14 新增 |
|
||||
|
||||
样列:
|
||||
|
||||

|
||||
|
||||
```plaintext
|
||||
{
|
||||
"saName":"xuetting",
|
||||
"objectNameGD":"18551638685【苏ABF358】 LFV2A21K6A3092399 大众 速腾 1.4T 双离合变速器(DSG) 2011 速腾",
|
||||
"creatorName":"cltest",
|
||||
"showStorageName":true,
|
||||
"remark":"",
|
||||
"title":"流程配置ES出库单",
|
||||
"showSourceBillNo":true,
|
||||
"sumAmount":"860.0",
|
||||
"showChineseAmount":true,
|
||||
"billStatus":"完成",
|
||||
"sumNumber":"1.0",
|
||||
"isGdType":true,
|
||||
"printCount":"3",
|
||||
"billNo":"CKD20250116001",
|
||||
"showDefSeat":true,
|
||||
"idOwnOrg":"15870306745529549109",
|
||||
"showApplyModel":true,
|
||||
"memo":"",
|
||||
"partInfoDetailMapList":[
|
||||
{
|
||||
"employeeName":"cltest",
|
||||
"sortNumber":"1",
|
||||
"partShowName":" 3M 燃油宝1号 PN6868 3M (PN6868)",
|
||||
"defSeat":"A",
|
||||
"orderBatchList":[
|
||||
{
|
||||
"orderNo":"20211213000001",
|
||||
"totalPrice":"0.0",
|
||||
"price":"0.0",
|
||||
"count":"1.0"
|
||||
}
|
||||
],
|
||||
"supplierCode":"PN6868",
|
||||
"partName":"3M 燃油宝1号 PN6868",
|
||||
"applyModel":"",
|
||||
"customCode":"CL0000015",
|
||||
"storageName":"主仓库",
|
||||
"number":"1.0",
|
||||
"unit":"瓶",
|
||||
"price":"860.0",
|
||||
"subtotal":"860.0",
|
||||
"labelName":"保养",
|
||||
"salesEmployeeNameList": "B2C一店新员工,B2C一店采购员"
|
||||
|
||||
},
|
||||
{
|
||||
"number":"1.0",
|
||||
"subtotal":"860.0",
|
||||
"sortNumber":"合计",
|
||||
"orderBatchList":[
|
||||
{
|
||||
"orderNo":"",
|
||||
"totalPrice":"0.0",
|
||||
"price":"",
|
||||
"count":"1.0"
|
||||
}
|
||||
]
|
||||
}],
|
||||
"sourceBillNo":"WXD20250103001",
|
||||
"billDate":"2025-01-16",
|
||||
"chineseAmount":"捌佰陆拾元整",
|
||||
"columnCount":"5",
|
||||
"showCustomCode":true,
|
||||
"showBusinessLabel":true,
|
||||
"batchPrintConfig":"3",
|
||||
"nowDateTime":"2025-02-18 10:04",
|
||||
"showInOutDate":true,
|
||||
"showChineseSubtotal":true,
|
||||
"objectName":"18551638685【苏ABF358】大众 速腾",
|
||||
"inOutDate":"2025-01-16",
|
||||
"showRemark":false
|
||||
}
|
||||
```
|
||||
|
||||
# 出入库单据-入库单 && 退料入库-入库单打印模版参数说明
|
||||
|
||||
入库单据定制类需求模版分类(newStockInMaintainCustomPrint)--20250925新增
|
||||
|
||||
打印模版参数
|
||||
|
||||
HashMap<String, Object> resultMap
|
||||
|
||||
| **字段** | **说明** | 备注 |
|
||||
| --- | --- | --- |
|
||||
| title | 门店名称+ "入库单" | |
|
||||
| billNo | 入库单号 | |
|
||||
| sourceBillNo | 来源单号 | |
|
||||
| showSourceBillNo | 显示来源单号 (boolean)<br>默认:true | |
|
||||
| billStatus | 单据状态(制单、完成) | |
|
||||
| inOutDate | 入库日期<br>\-- yyyy-MM-dd | |
|
||||
| showInOutDate | 是否显示入库日期(boolean)<br>\-制单是 false<br>\-完成是 true | |
|
||||
| objectName | 出入库对象 | |
|
||||
| creatorName | 制单人 | |
|
||||
| billDate | 制单日期 (yyyy-MM-dd) | |
|
||||
| sumNumber | 材料总数量 | |
|
||||
| sumAmount | 总金额 <br>\--脱敏场景显示 _\*_\*_\*\*_ | |
|
||||
| noTaxSumAmount | 除税总金额 <br>\--脱敏场景显示 _\*_\*_\*\*_ | |
|
||||
| chineseAmount | 总金额(中文大写)<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| chineseNoTaxSumAmount | 除税总金额(中文大写)<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| nowDateTime | 打印当前时间 (yyyy-MM-dd HH:mm) | |
|
||||
| idOwnOrg | 门店ID | |
|
||||
| remark | 备注信息 | |
|
||||
| showRemark | 是否显示备注(boolean)<br>\-- 备注不为空时是 true | |
|
||||
| isGdType | 是否是工单类型单据出库(boolean)<br>\-- 工单出库单是 true | |
|
||||
| saName | 服务顾问姓名<br>\-- 工单出库单场景 | |
|
||||
| printCount | 打印次数 | |
|
||||
| showCustomCode | 配置出入库打印参数-是否显示材料编码(boolean) | |
|
||||
| showBusinessLabel | 配置出入库打印参数-是否显示材料业务分类(boolean) | |
|
||||
| showApplyModel | 配置出入库打印参数-是否显示材料适用车型(boolean) | |
|
||||
| showStorageName | 配置出入库打印参数-是否显示出库仓库(boolean) | |
|
||||
| showDefSeat | 配置出入库打印参数-是否显示出库货位(boolean) | |
|
||||
| showChineseAmount | 配置出入库打印参数-是否显示大写金额(boolean) | |
|
||||
| showChineseSubtotal | 配置出入库打印参数-是否显示大写行合计(boolean) | |
|
||||
| sumSubtotal | 入库总金额<br>\-- 脱敏场景显示 \*\*\*\*<br>\-- 工单退-才有 | |
|
||||
| chineseSubtotal | 大写入库总金额<br>\-- 脱敏场景显示 \*\*\*\*<br>\-- 工单退-才有 | |
|
||||
| showReturnIn | 显示退料入库一行<br>\-- 工单退-true | |
|
||||
| showSign | 【仓管签字】显示的位置<br>工单退-2;其它场景1 | |
|
||||
| stockInType | 退料入库<br>\-- 工单退-才有 | |
|
||||
| columnCount | 显示几列 | |
|
||||
| memo | 车主描述 | \--20250925 新增 |
|
||||
| partInfoDetailMapList | | |
|
||||
| sortNumber | 序号 | |
|
||||
| partShowName | 材料组合名称 | |
|
||||
| partName | 材料名称 | |
|
||||
| partBrand | 材料品牌 | |
|
||||
| labelName | 业务分类 | |
|
||||
| applyModel | 适用车型 | |
|
||||
| customCode | 材料编码 | |
|
||||
| storageName | 仓库名称 | |
|
||||
| defSeat | 货位 | |
|
||||
| number | 数量<br>**\-- 通用模版追加一行合计行,显示 材料出库总数** | |
|
||||
| unit | 单位 | |
|
||||
| price | 单价<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| noTaxPrice | 除税单价<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| subtotal | 金额<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| noTaxSubtotal | 除税金额<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| employeeName | 入库人 | |
|
||||
| salesEmployeeNameList | 材料行销售人员 | \--20250925 新增 |
|
||||
|
||||
# 手工出入库-出/入库单
|
||||
|
||||
打印模版参数
|
||||
|
||||
HashMap<String, Object> resultMap
|
||||
|
||||
| **字段** | **说明** | **备注** |
|
||||
| --- | --- | --- |
|
||||
| title | 出库单:门店名称+ "出库单"<br>入库单:门店名称+ "入库单" | |
|
||||
| billNo | 出库单号/入库单号 | |
|
||||
| sourceBillNo | 来源单号 空 | |
|
||||
| showSourceBillNo | 显示来源单号 (boolean)false | |
|
||||
| billStatus | 单据状态(制单、完成) | |
|
||||
| inOutDate | 出库日期 (yyyy-MM-dd) | |
|
||||
| showInOutDate | 是否显示出库日期(boolean) | |
|
||||
| objectName | 出入库对象 | |
|
||||
| objectNameGD | 出入库对象 | |
|
||||
| creatorName | 制单人 | |
|
||||
| billDate | 制单日期 (yyyy-MM-dd) | |
|
||||
| sumNumber | 材料总数量 | |
|
||||
| sumAmount | 总金额 <br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| chineseAmount | 总金额(中文大写)<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| nowDateTime | 打印当前时间 (yyyy-MM-dd HH:mm) | |
|
||||
| idOwnOrg | 门店ID | |
|
||||
| remark | 备注信息 | |
|
||||
| showRemark | 是否显示备注(boolean)<br/>-- 备注不为空时是 true | |
|
||||
| printCount | 打印次数 | |
|
||||
| showCustomCode | <br/>配置出入库打印参数-是否显示材料编码(boolean) | |
|
||||
| showBusinessLabel | 配置出入库打印参数-是否显示材料业务分类(boolean) | |
|
||||
| showApplyModel | 配置出入库打印参数-是否显示材料适用车型(boolean) | |
|
||||
| showStorageName | 配置出入库打印参数-是否显示出库仓库(boolean) | |
|
||||
| showDefSeat | 配置出入库打印参数-是否显示出库货位(boolean) | |
|
||||
| showChineseAmount | 配置出入库打印参数-是否显示大写金额(boolean) | |
|
||||
| showChineseSubtotal | 配置出入库打印参数-是否显示大写行合计(boolean) | |
|
||||
| columnCount | 显示几列 | |
|
||||
| batchPrintConfig | 配置出入库打印参数-查询批次成本展示设置 0:都不展示,1:总成本(将批次成本合并),2和3:展示批次成本 | |
|
||||
| partInfoDetailMapList | | |
|
||||
| sortNumber | 序号<br>**\-- 通用模版追加一行合计行,显示 合计** | |
|
||||
| partShowName | 材料组合名称 (材料名称 规格型号 材料品牌 零件号) | |
|
||||
| partName | 材料名称 | |
|
||||
| partBrand | 材料品牌 -- 2025.02.27 新增 | |
|
||||
| supplierCode | 零件号 | |
|
||||
| labelName | 业务分类 | |
|
||||
| applyModel | 适用车型 | |
|
||||
| customCode | 材料编码 | |
|
||||
| storageName | 仓库名称 | |
|
||||
| defSeat | 货位 | |
|
||||
| number | 数量<br>**\-- 通用模版追加一行合计行,显示 材料出库总数** | |
|
||||
| unit | 单位 | |
|
||||
| price | 单价<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| subtotal | 金额<br>\--脱敏场景显示 _\*_\*\*\* | |
|
||||
| employeeName | 出库人 | |
|
||||
| taxRate | 税率 | |
|
||||
| orderBatchList<br>\--List<Map<String, String>> | 材料行成本相关 | |
|
||||
| orderNo | 批次号<br>\- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示 ""<br>**\-- 通用模版追加一行合计,追加行,显示 ""** | |
|
||||
| count | 批次出库几个<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示出库/入库个数<br>**\-- 通用模版追加一行合计,追加行,显示总数** | |
|
||||
| price | 批次单位成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 ""\*\* | |
|
||||
| priceNoTax | 批次除税单位成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 ""\*\* | 2025.08.14 追加 |
|
||||
| totalPrice | 批次总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本\*\*\*\* | |
|
||||
| totalPriceNoTax | 批次除税总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本\*\*\*\* | 2025.08.14 追加 |
|
||||
|
||||
样列:
|
||||
|
||||

|
||||
|
||||
日志关键字:+"手工出入库单据打印入参是"
|
||||
|
||||
```plaintext
|
||||
{
|
||||
"objectNameGD":"",
|
||||
"creatorName":"王◇龙",
|
||||
"showStorageName":true,
|
||||
"remark":"",
|
||||
"title":"ISC总店出库单",
|
||||
"showSourceBillNo":false,
|
||||
"sumAmount":"410.0",
|
||||
"showChineseAmount":true,
|
||||
"billStatus":"完成",
|
||||
"sumNumber":"2.0",
|
||||
"printCount":"5",
|
||||
"billNo":"SGC20240515001",
|
||||
"showDefSeat":true,
|
||||
"idOwnOrg":"4060685614490690260",
|
||||
"showApplyModel":true,
|
||||
"partInfoDetailMapList":[
|
||||
{
|
||||
"employeeName":"XN",
|
||||
"sortNumber":"1",
|
||||
"partShowName":" 材料09080112 123456 米其林 (1240)",
|
||||
"defSeat":"A-14-02",
|
||||
"orderBatchList":[
|
||||
{
|
||||
"orderNo":"20210909000128",
|
||||
"totalPrice":"400.0",
|
||||
"price":"400.0",
|
||||
"count":"1.0"
|
||||
}
|
||||
],
|
||||
"supplierCode":"1240",
|
||||
"partName":"材料09080112",
|
||||
"applyModel":"大众 途安",
|
||||
"customCode":"CL090800112",
|
||||
"storageName":"主仓库",
|
||||
"number":"1.0",
|
||||
"unit":"条",
|
||||
"price":"400.0",
|
||||
"subtotal":"400.0",
|
||||
"labelName":"轮胎"
|
||||
},
|
||||
{
|
||||
"employeeName":"XN",
|
||||
"sortNumber":"2",
|
||||
"partShowName":" fnst=>1 AC德科 (FNST>=1)",
|
||||
"defSeat":"",
|
||||
"orderBatchList":[
|
||||
{
|
||||
"orderNo":"20201104000002",
|
||||
"totalPrice":"4.0",
|
||||
"price":"4.0",
|
||||
"count":"1.0"
|
||||
}
|
||||
],
|
||||
"supplierCode":"FNST>=1",
|
||||
"partName":"fnst=>1",
|
||||
"applyModel":"江淮瑞风S52....",
|
||||
"customCode":"fnst>=1",
|
||||
"storageName":"总2仓",
|
||||
"number":"1.0",
|
||||
"unit":"个",
|
||||
"price":"10.0",
|
||||
"subtotal":"10.0",
|
||||
"labelName":"保养"
|
||||
},
|
||||
{
|
||||
"number":"2.0",
|
||||
"subtotal":"410.0",
|
||||
"sortNumber":"合计",
|
||||
"orderBatchList":[
|
||||
{
|
||||
"orderNo":"",
|
||||
"totalPrice":"404.0",
|
||||
"price":"",
|
||||
"count":"2.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"sourceBillNo":"",
|
||||
"billDate":"2024-05-15",
|
||||
"chineseAmount":"肆佰壹拾元整",
|
||||
"columnCount":"5",
|
||||
"showCustomCode":true,
|
||||
"showBusinessLabel":true,
|
||||
"batchPrintConfig":"3",
|
||||
"nowDateTime":"2025-02-18 09:33",
|
||||
"showInOutDate":true,
|
||||
"showChineseSubtotal":true,
|
||||
"objectName":"",
|
||||
"inOutDate":"2024-05-15",
|
||||
"showRemark":false
|
||||
}
|
||||
```
|
||||
|
||||
# 领料详情-打印领料单
|
||||
|
||||
打印模版参数
|
||||
|
||||
HashMap<String, Object> resultMap
|
||||
|
||||
| **字段** | **说明** | **备注** |
|
||||
| --- | --- | --- |
|
||||
| idOwnOrg | 门店ID | |
|
||||
| title | 门店名称+ "领料单" | |
|
||||
| billNo | 工单号 | |
|
||||
| nowDateTime | 打印当前时间 (yyyy-MM-dd HH:mm) | |
|
||||
| employeeName | 服务顾问 | |
|
||||
| carModel | 车型 | |
|
||||
| carNoWhole | 车牌号 | |
|
||||
| memo | 车主描述 | 2025.08.14 新增 |
|
||||
| printTimes | 打印次数 | |
|
||||
| columnCount | 显示几列 | |
|
||||
| batchPrintConfig | 配置出入库打印参数-查询批次成本展示设置 0:都不展示,1:总成本(将批次成本合并),2和3:展示批次成本 | |
|
||||
| stuffDetailVOList | | |
|
||||
| index | 序号<br>**\-- 通用模版追加一行合计行,显示 合计** | |
|
||||
| partName | 材料组合名称 (材料名称 规格型号 材料品牌 零件号) | |
|
||||
| unit | 单位 | |
|
||||
| defSeatList | 货位 | |
|
||||
| salesEmployeeNameList | 销售人员<br>List<String> | 8.14新增 |
|
||||
| orderBatchList<br>\--List<Map<String, String>> | 材料行成本相关 | |
|
||||
| orderNo | 批次号<br>\- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示 ""<br>**\-- 通用模版追加一行合计,追加行,显示 ""** | |
|
||||
| count | 批次出库几个<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示出库/入库个数<br>**\-- 通用模版追加一行合计,追加行,显示总数** | |
|
||||
| price | 批次单位成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 ""\*\* | |
|
||||
| priceNoTax | 批次除税单位成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 ""\*\* | 2025.08.14 追加 |
|
||||
| totalPrice | 批次总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本\*\*\*\* | |
|
||||
| totalPriceNoTax | 批次除税总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本\*\*\*\* | 2025.08.14 追加 |
|
||||
+682
@@ -0,0 +1,682 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
|
||||
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="3fa22123-efc4-4f3f-a186-6a8f692d17e6">
|
||||
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
|
||||
<subDataset name="List1" uuid="7366c5be-288c-41c7-b295-b8d023ec81ae">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="index" class="java.lang.String"/>
|
||||
<field name="serviceName" class="java.lang.String"/>
|
||||
<field name="cooperationServiceName" class="java.lang.String"/>
|
||||
<field name="cooperationOrgName" class="java.lang.String"/>
|
||||
<field name="auditStatus" class="java.lang.String"/>
|
||||
<field name="cooperationCost" class="java.math.BigDecimal"/>
|
||||
</subDataset>
|
||||
<parameter name="title" class="java.lang.String"/>
|
||||
<parameter name="billNo" class="java.lang.String"/>
|
||||
<parameter name="creatorName" class="java.lang.String"/>
|
||||
<parameter name="printTime" class="java.lang.String"/>
|
||||
<parameter name="naEmployee" class="java.lang.String"/>
|
||||
<parameter name="billDate" class="java.lang.String"/>
|
||||
<parameter name="deliveryTime" class="java.lang.String"/>
|
||||
<parameter name="naCustomer" class="java.lang.String"/>
|
||||
<parameter name="carModel" class="java.lang.String"/>
|
||||
<parameter name="cellPhone" class="java.lang.String"/>
|
||||
<parameter name="carNoWhole" class="java.lang.String"/>
|
||||
<parameter name="vin" class="java.lang.String"/>
|
||||
<parameter name="serviceDetailVOList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="amountAll" class="java.math.BigDecimal"/>
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<detail>
|
||||
<band height="115" splitType="Stretch">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="555" height="40" uuid="7c3a0deb-dcb0-406e-ba9c-9f279e1518b0">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="16"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{title}!=null?$P{title}+"协作项目确认单":""]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="240" y="60" width="120" height="18" uuid="6849ccdb-a3a0-4d32-a556-7a8fd5a19cca">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{creatorName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="420" y="60" width="135" height="18" uuid="b347e5b3-390c-44c3-b746-481aa6dc808e">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{printTime}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="60" y="60" width="120" height="18" uuid="92646184-6d70-4b6e-9f0e-23b4a7b15fb5">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{billNo}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement key="" x="360" y="60" width="50" height="18" isRemoveLineWhenBlank="true" uuid="86735eee-0435-4238-9b93-c08d86ed318f">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[打印时间:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="60" width="60" height="18" uuid="fef0343d-31f4-4b1c-a521-1a1b736aa485">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[工单号:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="180" y="60" width="60" height="18" uuid="627d90fe-c235-45d8-9f4a-92f8d7ec0de7">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[制单人:]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="78" width="555" height="1" uuid="329fa736-2c18-4d9d-8fab-f54846315633">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<staticText>
|
||||
<reportElement x="180" y="96" width="60" height="18" uuid="ef411f00-dff2-400c-9e82-bbc2450553c0">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[车型]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="78" width="60" height="18" uuid="74602da5-9195-47dd-9416-9cb229aeccad">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[服务顾问]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="240" y="96" width="315" height="18" uuid="985f2bdc-723d-477e-9786-263a1d3ce58c">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{carModel}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="240" y="78" width="120" height="18" uuid="49312902-8cda-4fe3-a92f-d6ecbf9cb893">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{billDate}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="180" y="78" width="60" height="18" uuid="65483940-e0c7-4713-9e9a-6a5a7294d248">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[进厂时间]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="60" y="78" width="120" height="18" uuid="e9c8328d-22ea-4e66-a23f-5015ffbd8248">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{naEmployee}]]></textFieldExpression>
|
||||
</textField>
|
||||
<line>
|
||||
<reportElement x="0" y="96" width="555" height="1" uuid="f6716829-054d-4fed-a12c-2f4d227724c7">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Dotted"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<line>
|
||||
<reportElement x="0" y="114" width="555" height="1" uuid="404f9e68-011f-4c55-88f6-f1231a22908b">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Dotted"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<line>
|
||||
<reportElement x="0" y="113" width="555" height="1" uuid="09d822fb-3735-4773-b954-9c83f3eefdef">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="420" y="78" width="135" height="18" uuid="09e44308-af9c-4bf2-b88d-b6c213fa2c43">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{vin}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="360" y="78" width="60" height="18" uuid="180c43d9-ea5c-4074-9b9e-b1ffaa905250">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[VIN码]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="60" y="96" width="120" height="18" uuid="85a6bfa2-352f-4f7e-b690-625639120c96">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{carNoWhole}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="96" width="60" height="18" uuid="fb4b2bbf-ccd0-4201-9e4b-d98ad63364ca">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[车牌号]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
<band height="56">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<componentElement>
|
||||
<reportElement x="0" y="20" width="555" height="36" uuid="dab34898-b275-4c88-8129-eefa0454fc8a">
|
||||
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
|
||||
<property name="com.jaspersoft.studio.table.style.table_header" value="Table 2_TH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.column_header" value="Table 2_CH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.detail" value="Table 2_TD"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
|
||||
<datasetRun subDataset="List1" uuid="c97f7286-0f00-496d-af86-5689f8ef21e5">
|
||||
<dataSourceExpression><![CDATA[$P{serviceDetailVOList}]]></dataSourceExpression>
|
||||
</datasetRun>
|
||||
<jr:column width="20" uuid="2a312910-46b6-42d0-a6f9-5b10690854b7">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
|
||||
<jr:columnHeader height="18" rowSpan="1">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="px"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
</jr:columnHeader>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="px"/>
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="20" height="18" uuid="c20bd145-2589-4e34-acd8-c52ecb01f627"/>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$V{COLUMN_COUNT}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="150" uuid="e296f490-d79c-4dc7-b064-4df5beea7f6a">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
|
||||
<jr:columnHeader height="18" rowSpan="1">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="150" height="18" uuid="2cf4018d-7073-47cd-ba69-6239dea208d5"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[项目名称]]></text>
|
||||
</staticText>
|
||||
</jr:columnHeader>
|
||||
<jr:detailCell height="18">
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="150" height="18" uuid="6d5dabce-2317-4d34-b286-b035b0319c86"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{serviceName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="125" uuid="901500f5-9ff9-42de-985e-5d31c114a41c">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
|
||||
<jr:columnHeader height="18" rowSpan="1">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="125" height="18" uuid="fda635b8-de40-4302-a86e-27d94d5977cf"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[协作项目名称]]></text>
|
||||
</staticText>
|
||||
</jr:columnHeader>
|
||||
<jr:detailCell height="18">
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="125" height="18" uuid="776c62d8-14e6-4bd5-9548-ecc3f933440a"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{cooperationServiceName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="120" uuid="cf002016-6b32-41b9-ad57-b813114477bf">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column4"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:columnHeader height="18" rowSpan="1">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="120" height="18" uuid="04d9aca5-e6c0-4688-b7f4-93dcf012c025"/>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="0"/>
|
||||
</textElement>
|
||||
<text><![CDATA[协作门店]]></text>
|
||||
</staticText>
|
||||
</jr:columnHeader>
|
||||
<jr:detailCell height="18">
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="120" height="18" uuid="b7b12dc9-c060-47b9-8252-ad264faed596"/>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{cooperationOrgName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="70" uuid="275bd3e2-9eb4-4274-8693-06306f525ed9">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column5"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:columnHeader height="18" rowSpan="1">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="70" height="18" uuid="a8222aea-c4e9-47c4-9c88-4043e14b06bc"/>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="0" rightIndent="0"/>
|
||||
</textElement>
|
||||
<text><![CDATA[审核状态]]></text>
|
||||
</staticText>
|
||||
</jr:columnHeader>
|
||||
<jr:detailCell height="18">
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="70" height="18" uuid="afca6b83-a1d0-4752-8fdb-b977d5c29aad"/>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{auditStatus}.equals("0")?"未审核":"已审核"]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="70" uuid="efa6775a-c855-4f28-9c4b-f2f940ca48ed">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column6"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:columnHeader height="18" rowSpan="1">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="70" height="18" uuid="bc6f3a7c-ae6f-48a8-abea-958935d8ee5c"/>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="0" rightIndent="0"/>
|
||||
</textElement>
|
||||
<text><![CDATA[协作成本]]></text>
|
||||
</staticText>
|
||||
</jr:columnHeader>
|
||||
<jr:detailCell height="18">
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="70" height="18" uuid="e473f232-b64b-4d97-a953-f8a62d80e2f0"/>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{cooperationCost}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
</jr:table>
|
||||
</componentElement>
|
||||
</band>
|
||||
<band height="19">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="20" y="1" width="60" height="18" uuid="fc27cb3d-0e5d-4143-b83d-daf432fa56c8">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[小计]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="0" width="555" height="1" uuid="98b923f9-1e14-4cc5-b508-12141cefb96e">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="485" y="1" width="70" height="18" uuid="bac7389a-d2c8-4df5-9e2b-bd0fba391750">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{amountAll}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="50">
|
||||
<staticText>
|
||||
<reportElement x="185" y="25" width="70" height="18" uuid="c9a9b702-c7eb-4be8-b07e-365064dee61f">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph firstLineIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[技师签名:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="370" y="25" width="70" height="18" uuid="a35c01ab-c3e4-46ca-a47f-e24e82171c0e">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph firstLineIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[审核人签名:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="25" width="70" height="18" uuid="fa883b7a-2acb-44e9-a646-e9e7c5f2aef8">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph firstLineIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[顾问签名:]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
</detail>
|
||||
</jasperReport>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# 协作单接口文档
|
||||
|
||||
# 协作单接口文档
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| title | 打印抬头(门店简称) | | String |
|
||||
| billNo | 工单号 | | String |
|
||||
| creatorName | 制单人 | | String |
|
||||
| printTime | 打印时间 | | String |
|
||||
| naEmployee | 服务顾问 | | String |
|
||||
| billDate | 进厂日期 | | String |
|
||||
| deliveryTime | 交车时间(出厂时间) | | String |
|
||||
| naCustomer | 车主姓名 | | String |
|
||||
| cellPhone | 车主电话 | | String |
|
||||
| carModel | 车型 | | String |
|
||||
| carNoWhole | 车牌号 | | String |
|
||||
| vin | 车辆VIN码 | | String |
|
||||
| amountAll | 小计 | | BigDecimal |
|
||||
| serviceDetailVOList | 协作项目集合 | | List<CooperationServicePrintAttribute> |
|
||||
|
||||
## CooperationServicePrintAttribute
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| serviceName | 工单项目名称 | | String |
|
||||
| cooperationServiceName | 协作项目名称 | | String |
|
||||
| cooperationOrgName | 协作门店 | | String |
|
||||
| auditStatus | 审核状态 | | String |
|
||||
| cooperationCost | 协作成本 | | BigDecimal |
|
||||
|
||||
> 更新: 2023-08-28 16:06:07 原文: <https://xcz.yuque.com/ombipo/rpc7ms/awq306g9g8fg78or>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# 增项单接口文档
|
||||
|
||||
# 增项单接口文档
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| title | 打印抬头(门店名称+"新增项目确认单") | | String |
|
||||
| sourceBillNo | 关联工单号 | | String |
|
||||
| printTime | 打印时间 | | String |
|
||||
| naEmployee | 服务顾问 | | String |
|
||||
| arrivalTime | 进厂日期 | | String |
|
||||
| deliveryTime | 交车时间 | | String |
|
||||
| naCustomer | 车主姓名 | | String |
|
||||
| cellPhone | 车主电话 | | String |
|
||||
| repairPerson | 送修人 | | String |
|
||||
| repairPersonContact | 送修人联系方式 | | String |
|
||||
| carModel | 车型 | | String |
|
||||
| carNoWhole | 车牌号 | | String |
|
||||
| carColor | 车身颜色 | | String |
|
||||
| engineCode | 发动机号 | | String |
|
||||
| vin | 车辆VIN码 | | String |
|
||||
| mileage | 进厂里程 | | String |
|
||||
| oilCapacity | 进厂油量 | | String |
|
||||
| merchantAddress | 商家联系地址 | | String |
|
||||
| merchantTel | 商家联系方式(固定电话) | | String |
|
||||
| merchantPhone | 商家联系方式(手机) | | String |
|
||||
| workHourPriceSubtotal | 工时费(小计) | | String |
|
||||
| amountSubtotal | 材料费(小计) | | String |
|
||||
| attachedServiceVoList | 增项服务项目集合 | | List<ServicePrintAttribute> |
|
||||
| attachedStuffVoList | 增项配件材料集合 | | List<PartPrintAttribute> |
|
||||
|
||||
**ServicePrintAttribute**
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| serviceName | 工单项目名称 | | String |
|
||||
| labelName | 业务分类名称 | | String |
|
||||
| customCode | 项目编码 | | String |
|
||||
| price | 工时单价 | | Double |
|
||||
| workHour | 工时 | | Double |
|
||||
| number | 项目数量 | | Integer |
|
||||
| discount | 折扣 | | Double |
|
||||
| discountedSubtotal | 折后金额 | | Double |
|
||||
| subtotal | 金额 | | Double |
|
||||
| serviceMemo | 项目备注 | | String |
|
||||
| isMember | 当前项目是否使用会员 | | Integer |
|
||||
| nameMember | 会员项目的来源名(如套餐代码) | | String |
|
||||
| empNameStr | 服务项目明细对应修理工名称组装字符串 | | String |
|
||||
|
||||
If you get gains,please give a like
|
||||
|
||||
**PartPrintAttribute**
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| partName | 材料名称 | | String |
|
||||
| labelName | 业务分类名称 | | String |
|
||||
| partBrand | 配件品牌 | | String |
|
||||
| customCode | 材料编码 | | String |
|
||||
| price | 单价 | | Double |
|
||||
| number | 材料数量 | | Integer |
|
||||
| unit | 单位 | | String |
|
||||
| discount | 折扣 | | Double |
|
||||
| discountedSubtotal | 折后金额 | | Double |
|
||||
| subtotal | 金额 | | Double |
|
||||
| partMemo | 材料备注 | | String |
|
||||
| isMember | 当前项目是否使用会员 | | Integer |
|
||||
| nameMember | 会员项目的来源名(如套餐代码) | | String |
|
||||
| employeeName | 员工名称 | | String |
|
||||
| empNameStr | 材料明细对应修理工名称组装字符串 | | String |
|
||||
|
||||
> 更新: 2023-09-05 10:34:22 原文: <https://xcz.yuque.com/ombipo/rpc7ms/gpobrxn8mn5lzthk>
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
# 委托单接口文档
|
||||
|
||||
# 委托单接口文档
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| printOrgName | 打印抬头 | | String |
|
||||
| orgName | 维修厂名称 | | String |
|
||||
| billNo | 工单号 | | String |
|
||||
| naEmployee | 服务顾问 | | String |
|
||||
| employeePhone | 服务顾问手机号 | | String |
|
||||
| naCustomer | 单位名称(客户姓名) | | String |
|
||||
| carNoWhole | 车牌号整体 = carPrefix + carNo | | String |
|
||||
| cellPhone | 联系电话(客户) | | String |
|
||||
| repairPerson | 送修人 | | String |
|
||||
| repairPersonContact | 送修人联系方式 | | String |
|
||||
| billDate | 进厂日期 | | String |
|
||||
| deliveryTime | 交车时间(出厂时间) | | String |
|
||||
| mileage | 出厂里程(进厂里程) | | BigDecimal |
|
||||
| oilCapacity | 当前油量 | | String |
|
||||
| vin | 车辆VIN码 | | String |
|
||||
| carModelShort | 车型简称 | | String |
|
||||
| signaturePhotoUrl | 签名图片 | | String |
|
||||
| orgContactNumber | 联系电话(维修厂) | | String |
|
||||
| orgDetailAddress | 联系地址(维修厂) | | String |
|
||||
| orgContactMobile | 联系电话(维修厂) | | String |
|
||||
| printContentEntrust | 委托单免责条款 | | String |
|
||||
| serviceSubtotalVip | 服务项目明细小计(会员项目) | | BigDecimal |
|
||||
| stuffSubtotalVip | 材料收入小计(会员项目) | | BigDecimal |
|
||||
| serviceSubtotalAll | 工时费小计 | | BigDecimal |
|
||||
| stuffSubtotalAll | 材料费小计 | | BigDecimal |
|
||||
| serviceList | 工单对应项目集合 | | List<ServicePrintAttribute> |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
| | | | |
|
||||
|
||||
## ServicePrintAttribute
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| serviceName | 项目名称 | | String |
|
||||
| price | 工时单价 | | BigInteger |
|
||||
| workHour | 工时 | | BigInteger |
|
||||
| subtotal | 金额 | | BigInteger |
|
||||
| serviceMemo | 附加信息备注 | | String |
|
||||
| isMember | 当前项目是否使用会员 | | Integer |
|
||||
| empNameStr | 服务项目明细对应修理工名称组装字符串 | | String |
|
||||
| | | | |
|
||||
| | | | |
|
||||
|
||||
> 更新: 2022-11-30 15:56:54 原文: <https://xcz.yuque.com/ombipo/rpc7ms/eppwl9lml80qq2bi>
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# 定金单打印接口文档
|
||||
|
||||
# 定金单打印接口文档
|
||||
|
||||
# 接口出参
|
||||
|
||||
| 字段 | 含义 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| title | 标题(门店名称) | String |
|
||||
| abbreviation | 门店简称 | String |
|
||||
| billNO | 定金单号 | String |
|
||||
| printTime | 打印时间 | String |
|
||||
| customerName | 客户姓名 | String |
|
||||
| cellPhone | 手机号码 | String |
|
||||
| carNo | 适用车辆(顿号、隔开) | String |
|
||||
| orgName | 适用门店(顿号、隔开) | String |
|
||||
| balanceStatus | 结算状态 | String |
|
||||
| receivedAmount | 已收金额 | Double |
|
||||
| amount | 收款交易金额 | Double |
|
||||
| amountAll | 定金单收款小计 | Double |
|
||||
| preRefundBalance | 定金单退款前余额 | Double |
|
||||
| advancesReceivedBalance | 定金单收款后剩余面额 | Double |
|
||||
| memo | 定金备注 | String |
|
||||
| settlePerson | 结算人 | String |
|
||||
| employeeName | 收款人 | String |
|
||||
| businessDate | 收款时间 | String |
|
||||
| billDate | 交易时间 | String |
|
||||
| transactionDate | 交易时间 | String |
|
||||
| detailAddress | 联系地址 | String |
|
||||
| contactMobile | 联系方式(手机+固定电话) | String |
|
||||
| naServicePerson | 服务顾问 | String |
|
||||
| gatheringList | 收款方式 | List |
|
||||
| L paymentType | 支付方式 | String |
|
||||
| L amount | 金额 | BigDecimal |
|
||||
| relationServices | 适用项目列表 | List |
|
||||
| L infoId | 项目id | BigInteger |
|
||||
| L infoName | 项目名称 | String |
|
||||
| L labelName | 业务分类 | String |
|
||||
| relationParts | 适用材料列表 | List |
|
||||
| L infoId | 材料id | BigInteger |
|
||||
| L infoName | 材料名称 | String |
|
||||
| L labelName | 业务分类 | String |
|
||||
| relationCars | 适用车辆列表 | List |
|
||||
| L idCar | 车辆信息id | BigInteger |
|
||||
| L carNo | 车牌号 | String |
|
||||
| L vin | vin码 | String |
|
||||
|
||||
# 范例
|
||||
|
||||
```plaintext
|
||||
收款收款{
|
||||
"data": {
|
||||
"preRefundBalance": 20000,
|
||||
"memo": "我是备注。",
|
||||
"title": "演示主店",
|
||||
"carNo": "藏AVB2131",
|
||||
"naServicePerson": "唐铭远",
|
||||
"contactMobile": "15051779785",
|
||||
"employeeName": "刘思杰",
|
||||
"amount": 10000,
|
||||
"orgName": "演示主店测试、第一分店",
|
||||
"advancesReceivedBalance": 10000,
|
||||
"amountAll": 10000,
|
||||
"balanceStatus": "7100",
|
||||
"billDate": "2024-07-11 16:27:49",
|
||||
"businessDate": "2024-07-24 14:49:01",
|
||||
"receivedAmount": 10000,
|
||||
"abbreviation": "演示主店测试",
|
||||
"transactionDate": "2024-07-11 16:27:49",
|
||||
"relationServices": [
|
||||
{
|
||||
"infoName": "龙膜全车贴膜(不含撕膜)",
|
||||
"infoId": "10545055918005551735",
|
||||
"infoType": 1,
|
||||
"id": "239",
|
||||
"labelName": "其他",
|
||||
"idSubscription": "11159"
|
||||
}
|
||||
],
|
||||
"customerName": "牛洋",
|
||||
"gatheringList": [
|
||||
{
|
||||
"amount": 4000,
|
||||
"paymentType": "支付宝"
|
||||
},
|
||||
{
|
||||
"amount": 3000,
|
||||
"paymentType": "现金"
|
||||
},
|
||||
{
|
||||
"amount": 3000,
|
||||
"paymentType": "挂账"
|
||||
}
|
||||
],
|
||||
"detailAddress": "西藏自治区那曲市班戈县西藏自治区那曲市班戈县北拉镇邮政所",
|
||||
"billNO": "DJD20240711001",
|
||||
"cellPhone": "17625046227",
|
||||
"printTime": "2024-07-11 17:28:08",
|
||||
"relationParts": [
|
||||
{
|
||||
"infoName": "奔腾CI-4 15W40 4*4L 胜牌 (706650)",
|
||||
"infoId": "10545055918005692400",
|
||||
"infoType": 2,
|
||||
"id": "240",
|
||||
"labelName": "业务分类测试",
|
||||
"idSubscription": "11159"
|
||||
}
|
||||
],
|
||||
"relationCars": [
|
||||
{
|
||||
"carNo": "AV616E",
|
||||
"vin": "LSVAA49J132047371",
|
||||
"idCar": 15809106713748983890
|
||||
}
|
||||
],
|
||||
},
|
||||
"storeId": 4060685614487994527,
|
||||
"tempId": 41
|
||||
}
|
||||
```
|
||||
> 更新: 2024-07-25 20:54:41 原文: <https://xcz.yuque.com/ombipo/rpc7ms/io3q0kkop242geg6>
|
||||
+942
@@ -0,0 +1,942 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
|
||||
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="dingjin" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="ecb495fd-dade-4203-bf0d-1beb50748cbb">
|
||||
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
|
||||
<subDataset name="Bean" uuid="c4b2280f-9d97-43a6-af41-df8e4c3837f1">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="index" class="java.lang.String"/>
|
||||
<field name="paymentType" class="java.lang.String"/>
|
||||
<field name="amount" class="java.math.BigDecimal"/>
|
||||
</subDataset>
|
||||
<subDataset name="SubscriptionInfoRelationBean" uuid="63a5d0a7-7ac9-4e14-b3f3-d03138dc8910">
|
||||
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="infoName" class="java.lang.String"/>
|
||||
<field name="labelName" class="java.lang.String"/>
|
||||
</subDataset>
|
||||
<parameter name="title" class="java.lang.String"/>
|
||||
<parameter name="billNO" class="java.lang.String"/>
|
||||
<parameter name="printTime" class="java.lang.String"/>
|
||||
<parameter name="customerName" class="java.lang.String"/>
|
||||
<parameter name="cellPhone" class="java.lang.String"/>
|
||||
<parameter name="carNo" class="java.lang.String"/>
|
||||
<parameter name="orgName" class="java.lang.String"/>
|
||||
<parameter name="amountAll" class="java.math.BigDecimal"/>
|
||||
<parameter name="memo" class="java.lang.String"/>
|
||||
<parameter name="settlePerson" class="java.lang.String"/>
|
||||
<parameter name="employeeName" class="java.lang.String"/>
|
||||
<parameter name="billDate" class="java.lang.String"/>
|
||||
<parameter name="detailAddress" class="java.lang.String"/>
|
||||
<parameter name="contactMobile" class="java.lang.String"/>
|
||||
<parameter name="gatheringList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="transactionDate" class="java.lang.String"/>
|
||||
<parameter name="naServicePerson" class="java.lang.String"/>
|
||||
<parameter name="relationServices" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="relationParts" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="balanceStatus" class="java.lang.String">
|
||||
<parameterDescription><![CDATA[]]></parameterDescription>
|
||||
</parameter>
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<detail>
|
||||
<band height="103" splitType="Stretch">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="1" width="555" height="30" uuid="6b893528-b46e-4a7d-b95d-62163a92812c">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="16"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{title}!=null?$P{title} + "定金收款单":""]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="40" width="50" height="18" uuid="df2d5fff-f94e-48ce-8327-d04b0ed8a8eb">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[定金单号:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="405" y="40" width="50" height="18" uuid="6100aced-372a-4449-91cf-1b49465ddce1">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[打印时间:]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="68" width="555" height="1" uuid="6820699a-64e5-4110-aab9-059fcfa04956">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<staticText>
|
||||
<reportElement x="0" y="68" width="50" height="15" uuid="2d6a570f-5cff-411e-a42d-5f3833a49674">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[客户姓名]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="150" y="68" width="50" height="15" uuid="e25f93dc-e5d5-4f43-bd78-20bbd337df95">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[手机号码]]></text>
|
||||
</staticText>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="50" y="68" width="100" height="15" uuid="ae838d08-7ea2-4a9f-9bcb-ad039eea0139">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{customerName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="200" y="68" width="100" height="15" uuid="f1280a32-7f86-45b7-b15e-e16b88db6f69">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{cellPhone}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="50" y="40" width="100" height="18" uuid="f86834e1-12c3-4f52-ab27-921980da0a86">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{billNO}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="455" y="40" width="100" height="18" uuid="f01ca83f-cc02-4c16-8edf-1ef24a479592">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{printTime}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="300" y="69" width="150" height="15" uuid="28fd8ca7-cd61-45c9-a5de-ecf081f3623c">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{naServicePerson} != null ? "服务顾问 " + $P{naServicePerson} : null]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="15">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="0" width="505" height="15" isPrintWhenDetailOverflows="true" uuid="bc87056d-ec39-4fa8-bc1b-cf2475f0b712">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<box bottomPadding="1"/>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{carNo}!=null?$P{carNo}:""]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="50" height="15" uuid="be24a675-674a-4e69-904f-462adddebdf2">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[限定车辆]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="0" width="555" height="1" uuid="e0b7e65f-b9cd-4f1d-9896-f7b35076c20f">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="15">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="50" height="15" uuid="c951994c-2dc6-4a82-835e-9c132409edd0">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[适用门店]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="0" width="505" height="15" isPrintWhenDetailOverflows="true" uuid="6450a724-3257-45c0-b5ac-d970e2608683">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<box bottomPadding="1"/>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{orgName}!=null?$P{orgName}:""]]></textFieldExpression>
|
||||
</textField>
|
||||
<line>
|
||||
<reportElement x="0" y="0" width="555" height="1" uuid="95190ceb-a559-4b6b-99ed-4d92568db2ce">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="15">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="50" y="0" width="505" height="15" uuid="73423a33-419c-424a-bf28-1f278c95f6de">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{amountAll}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="50" height="15" uuid="bbfe6557-435b-4a06-89e7-68f5fa5f866e">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[定金金额]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="0" width="555" height="1" uuid="34772527-56ed-496e-83e8-638d8f87a7d4">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="18">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement stretchType="ContainerBottom" x="0" y="0" width="50" height="15" isRemoveLineWhenBlank="true" uuid="31b57e69-4dea-4593-84c4-5f6e5d7c5cad">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[定金备注]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement stretchType="ContainerBottom" x="50" y="0" width="505" height="15" isRemoveLineWhenBlank="true" isPrintWhenDetailOverflows="true" uuid="d8c6dc34-aedb-46ab-ab17-1b75d37cee18">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{memo}]]></textFieldExpression>
|
||||
</textField>
|
||||
<line>
|
||||
<reportElement x="0" y="0" width="555" height="1" uuid="024b26b4-5ca0-4d1a-a713-2b1a04e4facb">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="6">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{relationServices} != null]]></printWhenExpression>
|
||||
<line>
|
||||
<reportElement x="0" y="5" width="555" height="1" uuid="3286a86a-5ca6-42c8-8302-9412dd51255b">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<line>
|
||||
<reportElement x="0" y="2" width="555" height="1" uuid="6e4f8003-b171-43ba-b3d1-1e863feaa14f">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="62">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{relationServices} != null]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="555" height="18" uuid="186a3a0f-9415-48af-8fc5-f0b91f01c8dd">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[限定项目条件]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="18" width="555" height="1" uuid="784988f1-bab7-4893-b7b0-45b8c7d36113">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<staticText>
|
||||
<reportElement x="10" y="21" width="40" height="18" uuid="3581da6d-7736-4324-8d00-46a8101328b8">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[序号]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="60" y="21" width="260" height="18" uuid="729944a0-c591-4d3c-bd82-10852a27ddb5">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[项目名称]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="340" y="21" width="215" height="18" uuid="91d16827-3716-49bf-9589-fef01692e051">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[业务分类]]></text>
|
||||
</staticText>
|
||||
<componentElement>
|
||||
<reportElement x="10" y="43" width="530" height="15" uuid="70e14cdb-d3db-4143-a95b-06c79be594e9">
|
||||
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
|
||||
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="net.sf.jasperreports.export.headertoolbar.table.name" value=""/>
|
||||
</reportElement>
|
||||
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
|
||||
<datasetRun subDataset="SubscriptionInfoRelationBean" uuid="65597dd8-2b18-4b8f-af59-9a14a8c656f9">
|
||||
<dataSourceExpression><![CDATA[$P{relationServices}]]></dataSourceExpression>
|
||||
</datasetRun>
|
||||
<jr:column width="20" uuid="9f5cb566-9a81-471e-83b7-eed0ae9a15cc">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="15">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="px"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="20" height="15" uuid="5b2a2317-f511-4c5b-8146-7422aa7bb656"/>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="299" uuid="50b00464-f90b-4b48-8dc9-3d06ac503250">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="15">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement stretchType="RelativeToBandHeight" x="0" y="0" width="299" height="15" uuid="4d201729-3ee9-42c2-b659-49966309bb6a">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph lineSpacing="Single" leftIndent="33"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{infoName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="211" uuid="9589926e-9fb0-432d-a202-7701e2654ee5">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="15">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="211" height="15" uuid="41413b43-582e-4a5e-9025-bbe735caf9f7">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph rightIndent="170"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{labelName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
</jr:table>
|
||||
</componentElement>
|
||||
</band>
|
||||
<band height="6">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{relationParts} != null]]></printWhenExpression>
|
||||
<line>
|
||||
<reportElement x="0" y="4" width="555" height="1" uuid="2f6ac67a-48f3-4ead-a04d-dc0a5ee9ec77">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="59">
|
||||
<printWhenExpression><![CDATA[$P{relationParts} != null]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="555" height="18" uuid="0cf72aee-23b8-46aa-a29f-cf3f3b5f60fa">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[限定材料条件]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="18" width="555" height="1" uuid="55b86547-d6ca-42e0-854e-2e08893a2b5f">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<staticText>
|
||||
<reportElement x="10" y="20" width="40" height="18" uuid="91997de4-2512-47f0-ab50-a9611a851818">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[序号]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="60" y="20" width="260" height="18" uuid="821e871f-4d45-4c6e-a153-bdb11222edfd">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[材料名称]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="340" y="20" width="215" height="18" uuid="cb6c5a3e-3605-4451-b6e6-57bbd3b20aaa">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[业务分类]]></text>
|
||||
</staticText>
|
||||
<componentElement>
|
||||
<reportElement x="10" y="40" width="530" height="15" uuid="d9989d91-ae8e-494b-99ae-e466e8480b93">
|
||||
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
|
||||
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
|
||||
<datasetRun subDataset="SubscriptionInfoRelationBean" uuid="2250db78-4f45-464b-afac-5c1fbe1c526a">
|
||||
<dataSourceExpression><![CDATA[$P{relationParts}]]></dataSourceExpression>
|
||||
</datasetRun>
|
||||
<jr:column width="20" uuid="a3a9ba5c-d15d-45a0-9529-905dd13d1bb3">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="15">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="px"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="20" height="15" uuid="80a4d1e9-c964-49e0-9165-e83d7f53c955"/>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="299" uuid="5b5b73ee-6e45-440c-abcc-c47295a8f445">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="15">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement stretchType="RelativeToBandHeight" x="0" y="0" width="299" height="15" uuid="f344b148-d0c4-4f7e-9b21-1a350e385f04">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph lineSpacing="Single" leftIndent="33"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{infoName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="211" uuid="bc6a0624-08f4-4831-b786-d96ecbad358f">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="15">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="211" height="15" uuid="43282bdc-4735-4173-b9be-1e34a4c7ecf1">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph rightIndent="170"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{labelName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
</jr:table>
|
||||
</componentElement>
|
||||
</band>
|
||||
<band height="6">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<line>
|
||||
<reportElement x="0" y="4" width="555" height="1" uuid="6020511d-2f28-4dc2-80f1-9176d218b3be">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="19">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement x="20" y="0" width="50" height="18" uuid="3581da6d-7736-4324-8d00-46a8101328b8">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[收款方式]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="18" width="555" height="1" uuid="784988f1-bab7-4893-b7b0-45b8c7d36113">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<staticText>
|
||||
<reportElement x="455" y="0" width="75" height="18" uuid="e5017d0f-3946-4dd0-8cdd-cc01396607c0">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
<paragraph leftIndent="0" rightIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[金额]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
<band height="15">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
|
||||
<componentElement>
|
||||
<reportElement x="0" y="0" width="530" height="15" uuid="e508f855-fa78-4d64-a6e4-a8a8a46aca77">
|
||||
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
|
||||
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
|
||||
<datasetRun subDataset="Bean" uuid="b65bc3e9-f219-4cb4-9e37-06389e44b27a">
|
||||
<dataSourceExpression><![CDATA[$P{gatheringList}]]></dataSourceExpression>
|
||||
</datasetRun>
|
||||
<jr:column width="20" uuid="ee067303-224b-4228-9945-59b00d0f5cab">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="15">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="px"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="20" height="15" uuid="69411085-ae93-4494-a5be-5f1c08c669e6"/>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="435" uuid="c73b03e1-b451-4ba7-aa66-506770b0d16b">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="15">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="435" height="15" uuid="513d0744-5c1a-4cbc-9ab0-a494b491c01f"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{paymentType}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="75" uuid="e1f4c79b-33e8-46e3-8d01-2405584f90dc">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="15">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="75" height="15" uuid="41c011f0-5a98-40a5-96c5-27cad152cd7b"/>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph rightIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{amount}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
</jr:table>
|
||||
</componentElement>
|
||||
</band>
|
||||
<band height="15">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
|
||||
<line>
|
||||
<reportElement x="0" y="0" width="555" height="1" uuid="125ba998-bf5b-433a-9954-6043f153ff42">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<staticText>
|
||||
<reportElement x="20" y="0" width="50" height="15" uuid="f6849ec7-9e15-47e5-b71e-dcd53f0b84e0">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[小计]]></text>
|
||||
</staticText>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="455" y="0" width="75" height="15" uuid="6fc715c5-3a85-4a39-8b51-ff75731aba06">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="0" rightIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{amountAll}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="25">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement x="0" y="10" width="50" height="15" uuid="97da61eb-2f1d-46a4-8ed4-c3172d371408">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[结算人:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="200" y="10" width="50" height="15" uuid="319ada1b-ff86-4b3c-82d6-15bc9981b1d9">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[收款人:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="405" y="10" width="50" height="15" uuid="01e213a0-948f-4a51-9b39-f3da64729c60">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[收款时间:]]></text>
|
||||
</staticText>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="50" y="10" width="100" height="15" uuid="92b95e36-c1a5-4311-94db-f615b29958f1">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{employeeName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="250" y="10" width="100" height="15" uuid="389f723a-1618-48f7-8e25-890a4902b19b">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{employeeName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="455" y="10" width="100" height="15" uuid="d8abcebc-b5b3-4c86-89b2-b45c218f353e">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{billDate}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="109">
|
||||
<staticText>
|
||||
<reportElement x="0" y="25" width="50" height="15" uuid="55ff22be-c6bb-4bbb-bb41-8cefc91675e0">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[办理人签名]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="300" y="25" width="50" height="15" uuid="ff44db31-a42f-477e-9e52-378b94c34217">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[客户签名]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="55" width="50" height="15" uuid="9afa1f35-7bf2-4b9b-aa4d-9989db266bd5">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[联系地址:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="70" width="50" height="15" uuid="a439d4d9-7268-40cb-8f44-41c75c328179">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[联系方式:]]></text>
|
||||
</staticText>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="50" y="55" width="505" height="15" uuid="0fd29c3e-c04b-4552-85b0-55b2720d57df">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{detailAddress}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="50" y="70" width="505" height="15" uuid="421732bc-d6d4-4f97-8480-565eaa109ffc">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{contactMobile}.split(" ")[1].length() == 5 ? $P{contactMobile}.split(" ")[0] : $P{contactMobile}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
</detail>
|
||||
</jasperReport>
|
||||
+641
@@ -0,0 +1,641 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
|
||||
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="223" pageHeight="842" whenNoDataType="NoPages" columnWidth="223" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="0" uuid="5195ed21-e8c1-4daf-ab55-5f5dc9c07b0a">
|
||||
<property name="com.jaspersoft.studio.unit." value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.topMargin" value="mm"/>
|
||||
<property name="com.jaspersoft.studio.unit.bottomMargin" value="mm"/>
|
||||
<property name="com.jaspersoft.studio.unit.leftMargin" value="mm"/>
|
||||
<property name="com.jaspersoft.studio.unit.rightMargin" value="mm"/>
|
||||
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
|
||||
<property name="com.jaspersoft.studio.unit.pageWidth" value="mm"/>
|
||||
<property name="ireport.zoom" value="1.0"/>
|
||||
<property name="ireport.x" value="0"/>
|
||||
<property name="ireport.y" value="0"/>
|
||||
<subDataset name="Dataset1" uuid="7e6cf9c7-d927-4f84-a0f6-e84863998e11">
|
||||
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="paymentType" class="java.lang.String"/>
|
||||
<field name="amount" class="java.math.BigDecimal"/>
|
||||
</subDataset>
|
||||
<subDataset name="SubscriptionInfoRelationBean" uuid="63b98521-dd7d-419f-bae4-f6a776630dd5">
|
||||
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="infoName" class="java.lang.String"/>
|
||||
<field name="labelName" class="java.lang.String"/>
|
||||
</subDataset>
|
||||
<parameter name="title" class="java.lang.String"/>
|
||||
<parameter name="abbreviation" class="java.lang.String"/>
|
||||
<parameter name="billNO" class="java.lang.String"/>
|
||||
<parameter name="printTime" class="java.lang.String"/>
|
||||
<parameter name="customerName" class="java.lang.String"/>
|
||||
<parameter name="cellPhone" class="java.lang.String"/>
|
||||
<parameter name="carNo" class="java.lang.String"/>
|
||||
<parameter name="orgName" class="java.lang.String"/>
|
||||
<parameter name="amountAll" class="java.math.BigDecimal"/>
|
||||
<parameter name="memo" class="java.lang.String"/>
|
||||
<parameter name="settlePerson" class="java.lang.String"/>
|
||||
<parameter name="employeeName" class="java.lang.String"/>
|
||||
<parameter name="billDate" class="java.lang.String"/>
|
||||
<parameter name="detailAddress" class="java.lang.String"/>
|
||||
<parameter name="contactMobile" class="java.lang.String"/>
|
||||
<parameter name="gatheringList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="transactionDate" class="java.lang.String"/>
|
||||
<parameter name="naServicePerson" class="java.lang.String"/>
|
||||
<parameter name="relationServices" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="relationParts" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="balanceStatus" class="java.lang.String"/>
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<detail>
|
||||
<band height="20" splitType="Stretch">
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="222" height="20" isPrintWhenDetailOverflows="true" uuid="fdd7c75d-7f0c-42a3-afa1-9927522a4dd1">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{title}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="22">
|
||||
<staticText>
|
||||
<reportElement x="0" y="1" width="222" height="15" uuid="2f71d4ac-e294-4763-a1ad-8e24fcb09055">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体"/>
|
||||
</textElement>
|
||||
<text><![CDATA[定金单]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="15" y="16" width="195" height="1" uuid="1fac1b83-9a99-4059-94f8-705f03630c06">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="12">
|
||||
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="44" height="12" uuid="8210a1bf-fb5f-465d-8e13-15ee265db665">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[收款时间]]></text>
|
||||
</staticText>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="44" y="0" width="177" height="12" uuid="e79b3ef6-3727-46d0-8de1-035b71f358f4">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="15"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{transactionDate}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="12">
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="44" height="12" uuid="924e0a29-acb7-4430-98fd-0dd08bf26d01">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[单号]]></text>
|
||||
</staticText>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="44" y="0" width="177" height="12" uuid="35315081-9a78-4f78-8df5-5ad786dd56b9">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="15"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{billNO}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="12">
|
||||
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="44" height="12" uuid="af0870ec-d979-4c13-8743-c7a0c3621815">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[收款人]]></text>
|
||||
</staticText>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="44" y="0" width="177" height="12" uuid="3b66f5f5-f6d9-4d05-90e7-af9561373bb6">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="15"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{employeeName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="12">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{naServicePerson}!=null]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="44" height="12" uuid="ccdb57d5-cdaf-4189-930f-fc1165c936ee">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[服务顾问]]></text>
|
||||
</staticText>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="44" y="-1" width="177" height="12" uuid="62146321-e5d6-4c36-b91c-db8c0a5653de">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体"/>
|
||||
<paragraph leftIndent="15"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{naServicePerson}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="24">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="0" y="-1" width="44" height="12" uuid="53391abc-4be2-40a8-8c2d-a770e6c13f18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[车主姓名]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="11" width="44" height="12" uuid="e3b541a2-001d-4a3c-b67d-c687f923f78e">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[车主电话]]></text>
|
||||
</staticText>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="44" y="0" width="177" height="12" uuid="7f7dfe97-543c-4dae-8af6-d7113d7a438e">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="15"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{customerName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="44" y="11" width="177" height="12" isPrintWhenDetailOverflows="true" uuid="b8c5fc03-800c-4e05-bfe1-e47ec4dfbcb8">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="15"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{cellPhone}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="12">
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="44" height="12" uuid="92f7d079-701e-433b-80f4-54b2bffec66c">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[适用门店]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="44" y="0" width="177" height="12" isPrintWhenDetailOverflows="true" uuid="0e37d554-d28f-405d-8f0a-ea70b390ee9d">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="15"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{orgName}!=null?$P{orgName}:""]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="12">
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="44" height="12" uuid="a58d2826-ecce-4305-a937-880861b60320">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[限定车辆]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="44" y="0" width="177" height="12" isPrintWhenDetailOverflows="true" uuid="fb93e076-c62e-40f1-ad63-412181861bbb">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="15"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{carNo}!=null?$P{carNo}:""]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="12">
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="33" height="12" uuid="3776a7e6-1f34-4e6e-b685-28ea1beb953f">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[备注:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="33" y="0" width="188" height="12" isPrintWhenDetailOverflows="true" uuid="49649536-e37f-4a5c-8fbf-35e71314c888"/>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{memo}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="6">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{relationServices} != null]]></printWhenExpression>
|
||||
<line>
|
||||
<reportElement x="5" y="4" width="202" height="1" uuid="d13f3282-835d-4a3f-bfaf-de1bc0f02c6f">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="42">
|
||||
<printWhenExpression><![CDATA[$P{relationServices} != null]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement x="5" y="6" width="64" height="12" uuid="b46dad1a-e4e2-4cbb-8078-8fadef5c2421">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[限定项目条件]]></text>
|
||||
</staticText>
|
||||
<componentElement>
|
||||
<reportElement x="12" y="21" width="200" height="12" uuid="7544bbec-c542-4d41-ae3c-a434b41bfc67">
|
||||
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
|
||||
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
|
||||
<property name="net.sf.jasperreports.export.headertoolbar.table.name" value=""/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd" whenNoDataType="Blank">
|
||||
<datasetRun subDataset="SubscriptionInfoRelationBean" uuid="0c7af790-dee9-440a-9406-7ca725234db3">
|
||||
<dataSourceExpression><![CDATA[$P{relationServices}]]></dataSourceExpression>
|
||||
</datasetRun>
|
||||
<jr:column width="130" uuid="854655c8-8e29-4621-8d50-301a091b089c">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
|
||||
<jr:detailCell height="12">
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement stretchType="RelativeToBandHeight" x="0" y="0" width="130" height="12" uuid="17589590-fe21-4b4b-ac94-6c89446150af"/>
|
||||
<textElement>
|
||||
<font fontName="黑体"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{infoName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="70" uuid="4a3d6479-0f51-4e1b-8499-f5e408b62814">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
|
||||
<jr:detailCell height="12">
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="70" height="12" uuid="948b7821-5590-4c82-a0ba-540f0c14ea99"/>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体"/>
|
||||
<paragraph leftIndent="23"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{labelName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
</jr:table>
|
||||
</componentElement>
|
||||
</band>
|
||||
<band height="10">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{relationParts} != null]]></printWhenExpression>
|
||||
<line>
|
||||
<reportElement x="5" y="4" width="202" height="1" uuid="8e4022d7-288e-46d5-9e8b-322f809389b5">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="42">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{relationParts} != null]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement x="5" y="6" width="64" height="12" uuid="3eb5fc85-6d1a-4e66-a6bb-90ffab0419bf">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[限定材料条件]]></text>
|
||||
</staticText>
|
||||
<componentElement>
|
||||
<reportElement x="12" y="21" width="200" height="12" uuid="0e8c9514-c83d-46bb-a398-df11b50450b3">
|
||||
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
|
||||
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd" whenNoDataType="Blank">
|
||||
<datasetRun subDataset="SubscriptionInfoRelationBean" uuid="a7ec5475-0753-496e-ba0c-821200ca3b09">
|
||||
<dataSourceExpression><![CDATA[$P{relationParts}]]></dataSourceExpression>
|
||||
</datasetRun>
|
||||
<jr:column width="130" uuid="521fa5b3-9477-4345-a4f1-22b89cd526b4">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
|
||||
<jr:detailCell height="12">
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement stretchType="RelativeToBandHeight" x="0" y="0" width="130" height="12" uuid="00aa7b2c-66f5-4b2a-86ef-e353042a169e"/>
|
||||
<textElement>
|
||||
<font fontName="黑体"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{infoName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="70" uuid="7d0f5ffc-76f9-4f63-9218-1af7cb5a5fbd">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
|
||||
<jr:detailCell height="12">
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="70" height="12" uuid="006c2c0f-2b11-4379-964b-6b7769307ee7"/>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体"/>
|
||||
<paragraph leftIndent="23"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{labelName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
</jr:table>
|
||||
</componentElement>
|
||||
</band>
|
||||
<band height="6">
|
||||
<line>
|
||||
<reportElement x="5" y="4" width="202" height="1" uuid="394e187e-5449-424f-807c-bb0c62e2d18d">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="29">
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="130" y="5" width="70" height="12" uuid="3d050094-7d49-4e3f-bca4-47d99321ed82">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{amountAll}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="5" width="130" height="12" uuid="4e9ec6ff-a558-48bb-a529-e94936a53b12">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[金额]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="15" y="23" width="195" height="1" uuid="267b9040-1d0d-4050-8906-04ebcdfba7b0">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="18">
|
||||
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
|
||||
<componentElement>
|
||||
<reportElement x="0" y="5" width="200" height="12" uuid="9ba8d55a-8e43-4aee-af93-ffdb8f2ec5ce">
|
||||
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
|
||||
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd" whenNoDataType="Blank">
|
||||
<datasetRun subDataset="Dataset1" uuid="a4a70f4b-c3cb-4482-95ff-149fb66ea7f6">
|
||||
<dataSourceExpression><![CDATA[$P{gatheringList}]]></dataSourceExpression>
|
||||
</datasetRun>
|
||||
<jr:column width="130" uuid="3b60f38e-2a7c-43b7-b976-071e57aeadf1">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
|
||||
<jr:detailCell height="12">
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="130" height="12" uuid="faf9cca2-d266-463d-91ee-cc7cc3575f24"/>
|
||||
<textElement>
|
||||
<font fontName="黑体"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{paymentType}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="70" uuid="56829dd0-7286-4bbf-825a-48d016b353bd">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
|
||||
<jr:detailCell height="12">
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="70" height="12" uuid="f8450d4c-7ad6-4237-ba69-56862a2d9a6d"/>
|
||||
<textElement>
|
||||
<font fontName="黑体"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{amount}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
</jr:table>
|
||||
</componentElement>
|
||||
</band>
|
||||
<band height="44">
|
||||
<line>
|
||||
<reportElement x="15" y="5" width="195" height="1" uuid="425e895d-8b95-436e-bd82-55a6c4d4cbf8">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<staticText>
|
||||
<reportElement x="0" y="11" width="44" height="12" uuid="c3e05a21-6ba4-4708-991a-825a994a1b2f">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[客户签字]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="15" y="28" width="195" height="1" uuid="b6bb8778-b57c-4fda-9e09-1a7eac4ca4e7">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<staticText>
|
||||
<reportElement x="0" y="32" width="222" height="12" uuid="8a83c68d-8555-4889-a063-2f78dce9af4a">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[请妥善保管购物凭证,谢谢惠顾!]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
<band height="12">
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="53" y="0" width="169" height="12" isPrintWhenDetailOverflows="true" uuid="429e5e34-e7cc-4b2e-a7ec-f9bdc72268ce">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="0"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{detailAddress}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="53" height="12" uuid="91cf76df-ccf1-417b-8ad1-73b7e1be2306"/>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[门店地址:]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
<band height="24">
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="53" y="12" width="169" height="12" uuid="bee0af12-b474-4e3a-8218-141663bc62f2">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="0"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{printTime}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isBlankWhenNull="true">
|
||||
<reportElement x="33" y="0" width="189" height="12" uuid="0f0d0893-f0ea-4221-b89f-b7a8ab6461be">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="0"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{contactMobile}.split(" ")[1].length() == 5 ? $P{contactMobile}.split(" ")[0] : $P{contactMobile}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="33" height="12" uuid="d007cd43-b97e-4837-badf-79a6f4fad415">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[电话:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="12" width="53" height="12" uuid="3bd289b6-ad7a-4e24-b8b0-cbf579a979f2">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement>
|
||||
<font fontName="黑体" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[打印时间:]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
</detail>
|
||||
</jasperReport>
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
# 工单结算单接口文档
|
||||
|
||||
# 打印单最新接口参数
|
||||
|
||||
#### maintain接口
|
||||
|
||||
**接口:/print/dispatchPrint/genUrl**
|
||||
|
||||
**方法:post**
|
||||
|
||||
支持场景:
|
||||
|
||||
各类结算单
|
||||
|
||||
不支持:上海结算单
|
||||
|
||||
**入参:**
|
||||
|
||||
```plaintext
|
||||
{
|
||||
"pkId": "14581820313319918809",
|
||||
"rowCode": "costSettlePrint",
|
||||
"rowId": "12"
|
||||
}
|
||||
```
|
||||
```plaintext
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"url": "http://s1.f6yc.com/printserver/test/printFile/201912/191213154046701.pdf"
|
||||
},
|
||||
"message": "SUCCESS"
|
||||
}
|
||||
```
|
||||
|
||||
#### erp接口
|
||||
|
||||
**/print/getPrintPDFPath.do**
|
||||
|
||||
templateId=56&templateType=newSettlePrint&idSourceBill=10546443563897503197
|
||||
|
||||
**rest get**
|
||||
|
||||
**支持各种新版打印类型**
|
||||
|
||||
```plaintext
|
||||
{
|
||||
"code": 200,
|
||||
"data": "http://s1.f6yc.com/printserver/test/printFile/201912/191213154046701.pdf",
|
||||
"message": "SUCCESS"
|
||||
}
|
||||
```
|
||||
|
||||
#### jasper取参对照
|
||||
|
||||
```plaintext
|
||||
{
|
||||
"data": {
|
||||
"cellPhone": "15421562365", //联系电话
|
||||
"naCustomer": "0322新", //单位名称
|
||||
"repairPerson": "", //送修人
|
||||
"carOwnerName":"", // 车辆所有人
|
||||
"accountNumber": "", //账号
|
||||
"billNo": "GD20190517001", //工单号
|
||||
"carNoWhole": "苏1542", //车牌号
|
||||
""
|
||||
"carColor":"" //车身颜色
|
||||
"orgDetailAddress": "江苏省盐城市阜宁县豆豆",//联系地址
|
||||
"vin": "11111111111111111", //车辆VIN码
|
||||
"naEmployee": "员工1(旧1)", //服务顾问
|
||||
"billDate": "2019-05-17 11:49",//进厂日期
|
||||
"businessTypeName": "维修", //维修类别
|
||||
"deliveryTime": "2019-05-17 12:49",//交车时间(出厂时间)
|
||||
"email": "126544@qq.com", //组织邮件
|
||||
"maintainType": "GD", //工单类型
|
||||
"billStatus":"6300", //单据状态
|
||||
"orgContactMobile": "15315256232", //联系电话
|
||||
"memo": "", //工单备注
|
||||
"orgMemo":"", // 门店备注
|
||||
"carMemo":"", // 车辆备注
|
||||
"printCount": "1", //打印次数
|
||||
"orgContactNumber": "", //联系电话-承修方信息
|
||||
"carSeriesName": "商用车", //车系名称
|
||||
"carBrandName": "商用车", //品牌名称
|
||||
"balanceStatus": "7000", //结算状态
|
||||
"printTime": "2019-07-26 11:43:48",//打印时间
|
||||
"firstSettlementTime":"2019-07-26 11:43:48",//结算时间(第一次收款时间)
|
||||
"orgName": "新公司测试", //单位名称-承修方信息
|
||||
"abbreviation": "门店简称", //门店简称
|
||||
"engineNumber": "123", //发动机号
|
||||
"transmissionNo": "123", //变速箱号
|
||||
"creationtime": "2019-05-17 11:50:46.0",//创建时间
|
||||
"creatorName": "员工1(旧1)", //创建人名称
|
||||
"employeePhone":"18734033191", //服务顾问手机号
|
||||
"paymentTypeDetails":"记账", //支付方式(记账公司)汇总
|
||||
"bankAccount": "", //开户银行
|
||||
"naInsurer":"", //理赔公司名称
|
||||
"insurancepolicyNo":"", //理赔单理赔保险单号
|
||||
"mergePackageContent": "1", //套餐合并标识
|
||||
"totalStuffNum": 1.0, //材料数量合计
|
||||
"selfTotalStuffNum": 0.0, //自带材料数量合计
|
||||
"serviceNum": "1", //维修项目小计
|
||||
"spreadRate": 0.0, //进销差价率
|
||||
"managementCost": 0.0, //进销差价合计
|
||||
"amountAll": 160.0, //应收总计
|
||||
"serviceDisCountSubTotal": 100.0, //项目折后金额合计
|
||||
"stuffSubtotalAll": 60.0, //材料费小计
|
||||
"serviceSubtotalAll": 100.0, //工时费小计
|
||||
"receiptAmount": 160.0, //实收金额
|
||||
"chineseAmount": "壹佰陆拾元整", //实收金额(大写)
|
||||
"oweAmount": 160.0, //未收金额
|
||||
"remainAmount":1.0, //结算金额tsf
|
||||
"receivedAmount":1.0, //收款金额tsf
|
||||
"settleOweAmout":1.0, //结算单中用的待付金额(未收)
|
||||
"settleOweAmoutChinese":"壹", //待付金额大写
|
||||
"settleReceivedAmout":1.0, //实付金额
|
||||
"settleReceivedAmoutChinese":1.0, //实付金额大写
|
||||
"totalWorkHour": 1.0, //项目工时合计
|
||||
"serviceFavourableTotal": 52.0, //项目优惠金额合计
|
||||
"serviceFavourableCommonTotal": 52.0,//普通项目优惠金额合计
|
||||
"stuffSubtotalAll": 532.0, //材料费小计
|
||||
"partFavourableTotal": 102.0, //材料优惠金额合计
|
||||
"partFavourableCommonTotal": 92.0, //普通材料优惠金额合计
|
||||
"stuffDisCountTotal": 60.0, //材料折后金额合计
|
||||
"extraCostTotal": 0.0, //附加费小计
|
||||
"allOtherCost": 0.0, //附加费合计应收
|
||||
"packageFavourable": 0.0, //套餐优惠
|
||||
"czkExpense": 0.0, //储值卡消费金额
|
||||
"vipExpense": 0.0, //会员卡消费金额
|
||||
"czkExpenseFavourable": 0.0, //储值卡优惠金额
|
||||
"czkDiscountFavourable": 0.0, //储值卡办卡优惠金额
|
||||
"vipExpenseFavourable": 0.0, //计次卡/套餐卡优惠金额
|
||||
"partinfoDiscountFavourable": 0.0, //材料折扣优惠
|
||||
"partinfoFavourable": 0.0, //材料项目(非会员项目)客户等级优惠
|
||||
"couponFavourable": 0.0, //优惠券优惠
|
||||
"pointFavourable": 0.0, //积分优惠
|
||||
"discountFavourable": 0.0, //结算时设置的结清优惠
|
||||
"gatheringFavourable": 0.0, //收银时设置的收银优惠
|
||||
"customerLevelFavourable": 0.0, //客户级别优惠金额
|
||||
"serviceFavourable": 0.0, //服务项目(非会员项目)客户等级优惠
|
||||
"disCountAll": 0.0, //总优惠合计
|
||||
"disCountAllBak":0.0, //总优惠合计bak
|
||||
"printContent": "", //免责条款
|
||||
"printContentEntrust":"", //委托单免责条款
|
||||
"mainCostList": [ //维修结算费用集合
|
||||
{
|
||||
"subtotal": 60.0, //价格
|
||||
"sortNumber": "1", //序号
|
||||
"costName": "材料费" //名称
|
||||
},
|
||||
{
|
||||
"subtotal": 100.0,
|
||||
"sortNumber": "2",
|
||||
"costName": "工时费"
|
||||
},
|
||||
{
|
||||
"subtotal": 160.0,
|
||||
"sortNumber": "3",
|
||||
"costName": "合计"
|
||||
}
|
||||
],
|
||||
"partList": [ //工单对应配件材料集合
|
||||
{
|
||||
"unit": "个", //单位
|
||||
"isBring": 0, //是否自带,1表示自带,0表示非自带
|
||||
"discountedSubtotal": 60.0, //折后金额
|
||||
"price": 60.0, //价格
|
||||
"partName": "分全", //材料名称
|
||||
"number": 1.0, //数量
|
||||
"subtotal": 60.0, //金额
|
||||
"taxRateOutput": 0.13, //销项税率
|
||||
"singleFavourable":0.0, //优惠金额
|
||||
"partBrand":"", //配件品牌
|
||||
"spec":"", //规格型号
|
||||
"supplierCode":"", //供应商编码(零件号)
|
||||
"customCode":"", //材料编码
|
||||
"isMember":0, //是否是会员卡材料 1是
|
||||
"discount":0.5, //折扣
|
||||
"partMemo":"", //备注
|
||||
"employeeName":"", //员工名称(维修技师)
|
||||
"cargoSpace":"", //货位
|
||||
"outStockEmployeeName":"", //领料人
|
||||
"sortNumber": "1" //序号
|
||||
}
|
||||
],
|
||||
"serviceList": [ //工单对应项目集合
|
||||
{
|
||||
"discountedSubtotal": 100.0,//折后金额
|
||||
"price": 100.0, //工时单价
|
||||
"workHour": 1.0, //工时
|
||||
"subtotal": 100.0, //金额
|
||||
"taxRateOutput": 0.13, //销项税率
|
||||
"sortNumber": "1", //序号
|
||||
"discount":1, //折扣
|
||||
"singleFavourable":0.0, //优惠金额
|
||||
"isMember":1, //是否是会员卡项目 1是 0否
|
||||
"empNameStr":"", //修理工
|
||||
"unusedNumber":1, //会员卡项目-未使用次数
|
||||
"number":2, //会员卡项目-总次数
|
||||
"infiniteFlag":1, //是否无限,0:否,1:是
|
||||
"serviceName": "0322新" //项目名称
|
||||
}
|
||||
],
|
||||
"cardList": [ //会员卡列表
|
||||
{
|
||||
"favourable": 20, //优惠
|
||||
"amount": 2019799, //余额
|
||||
"consumeAmount":10, //本次消费金额
|
||||
"memberCardNo": "123232rg",//卡号
|
||||
"name": "测试洗车卡项目2" //卡名称
|
||||
}
|
||||
],
|
||||
"czkList": [ //储值卡列表
|
||||
{
|
||||
"favourable": 20, //优惠
|
||||
"amount": 2019799, //余额
|
||||
"consumeAmount":10, //本次消费金额
|
||||
"memberCardNo": "123232rg",//卡号
|
||||
"name": "测试洗车卡项目2" //卡名称
|
||||
}
|
||||
],
|
||||
"extraPrintVo": { //附加项目
|
||||
"processItemName": "加工", //加工条目名称
|
||||
"checkCustomName": "检测费", //检测费名称
|
||||
"diagnosisCustomName": "诊断费", //诊断费名称
|
||||
"diagnosisItemName": "维修诊断", //维修诊断项目
|
||||
"diagnosisMemo": "", //诊断费备注
|
||||
"diagnosisCost": 0.0, //诊断费
|
||||
"commissionCustomName": "代办费", //代办费名称
|
||||
"managementCustomName": "管理费", //管理费名称
|
||||
"commissionMemo": "", //代办费备注
|
||||
"processMemo": "", //加工费
|
||||
"commissionCost": 0.0, //代办费
|
||||
"checkCost": 0.0, //检测费
|
||||
"managementCost": 0.0, //管理费
|
||||
"processCustomName": "加工费", //加工费名称
|
||||
"processCost": 0.0, //加工费
|
||||
"managementMemo": "", //管理费备注
|
||||
"checkMemo": "" //检测费备注
|
||||
}
|
||||
},
|
||||
"storeId": 25965086392720693,
|
||||
"tempId": 123
|
||||
}
|
||||
```
|
||||
|
||||
#### 参数说明(完整)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| \------基础信息 | | |
|
||||
| **billNo** | String | _工单号_ |
|
||||
| **maintainType** | String | _工单类型_ |
|
||||
| **balanceStatus** | String | _结算状态_<br/>_"7000" -- 未结算(即存在待付金额)_<br/>_"7100" -- 已结算_<br/>_"7200" -- 部分结算_ |
|
||||
| **creatorName** | String | _创建人名称_ |
|
||||
| **naEmployee** | String | _服务顾问_ |
|
||||
| **spreadRate** | Double | _进销差价率_ |
|
||||
| carCategoryName | String | _客户车辆分类名称_ |
|
||||
| **creationtime** | String | _创建时间_ |
|
||||
| **memo** | String | _工单备注_ |
|
||||
| **printCount** | String | _打印次数_ |
|
||||
| **printTime** | String | _打印时间_ |
|
||||
| **deliveryTime** | String | _交车时间(出厂时间),收款取收款时间,未收款完工的取完工时间,未完工的取预计交车时间_ |
|
||||
| **mergePackageContent** | String | _套餐合并标识_ |
|
||||
| **printContent** | String | _免责条款_ |
|
||||
| **engineNumber** | String | _发动机号_ |
|
||||
| **transmissionNo** | String | 变速箱号 |
|
||||
| **printMaintainGuaZi** | String | _guazi标识_ |
|
||||
| **amountAll** | Double | _应收总计_ |
|
||||
| **disCountAll** | Double | _总优惠合计_ |
|
||||
| **oweAmount** | Double | _未收金额_ |
|
||||
| **vipExpense** | Double | _会员卡消费金额_ |
|
||||
| **vipExpenseFavourable** | Double | _计次卡/套餐卡优惠金额_ |
|
||||
| **czkExpense** | Double | _储值卡消费金额_ |
|
||||
| **czkExpenseFavourable** | Double | _储值卡优惠金额_ |
|
||||
| **czkSettleFavourable** | Double | _储值卡结算优惠金额(仅未收款返回)_ |
|
||||
| **accountAmount** | Double | 记账金额 |
|
||||
| **totalOweAmount** | Double | _未付金额_ |
|
||||
| **serviceFavourable** | Double | _服务项目(非会员项目)客户等级优惠_ |
|
||||
| **partinfoFavourable** | Double | _材料项目(非会员项目)客户等级优惠_ |
|
||||
| **partinfoDiscountFavourable** | Double | _材料折扣优惠_ |
|
||||
| **pointFavourable** | Double | _积分优惠_ |
|
||||
| **packageFavourable** | Double | _套餐优惠_ |
|
||||
| **discountFavourable** | Double | _结算时设置的结清优惠_ |
|
||||
| **gatheringFavourable** | Double | _收银时设置的收银优惠_ |
|
||||
| **couponFavourable** | Double | _优惠券优惠_ |
|
||||
| **customerLevelFavourable** | Double | _客户级别优惠金额_ |
|
||||
| **customerLevelName** | String | _客户级别_ |
|
||||
| **customerDetailAddress** | String | 客户详细地址 |
|
||||
| **channelName** | String | 来店途径名称 |
|
||||
| **receiptMemo** | String | 收银备注 |
|
||||
| **customerSourceName** | String | _客户来源名称_ |
|
||||
| **carSourceName** | String | _车辆来源名称_ |
|
||||
| \-----托修方信息 | | |
|
||||
| **naCustomer** | String | _单位名称/托修方_ |
|
||||
| **repairPerson** | String | _送修人_ |
|
||||
| **carNoWhole** | String | _车牌号整体_ |
|
||||
| **carBrandName** | String | _品牌名称_ |
|
||||
| **carSeriesName** | String | _车系名称_ |
|
||||
| carMemo | String | 车辆备注 |
|
||||
| **businessTypeName** | String | _维修类别_ |
|
||||
| **vin** | String | _车辆VIN码_ |
|
||||
| carFuelTypeNameOriginal | carFuelTypeNameOriginal | 燃料类型 |
|
||||
| **billDate** | String | _进厂日期_ |
|
||||
| **mileage** | java.math.BigDecimal | _出厂里程_ |
|
||||
| **contractNumber** | | _合同编号(空)_ |
|
||||
| **certificateNumber** | | _合格证号(空)_ |
|
||||
| **cellPhone** | String | _联系电话_ |
|
||||
| **email** | String | _组织邮件_ |
|
||||
| \------承修方信息 | | |
|
||||
| **orgName** | String | _单位名称_ |
|
||||
| **abbreviation** | String | 门店简称 |
|
||||
| **orgContactNumber** | String | _联系电话_ |
|
||||
| **orgDetailAddress** | String | _联系地址_ |
|
||||
| **orgContactMobile** | String | _联系电话_ |
|
||||
| **bankAccount** | String | _开户银行_ |
|
||||
| **accountNumber** | String | _账号_ |
|
||||
| **businessLicenseCode** | String | 营业执照编码 |
|
||||
| \------项目信息 | | |
|
||||
| serviceList | array | 项目条目 |
|
||||
| #### orderNumber | String | 序号(验证可用) |
|
||||
| sortNumber | String | _序号_ |
|
||||
| customCode | String | 项目编码 |
|
||||
| serviceName | String | _项目名称_ |
|
||||
| **labelName** | String | _业务分类名称_ |
|
||||
| **nameMember** | String | _会员项目的来源名_ |
|
||||
| **labelName** | String | 业务分类 |
|
||||
| price | Double | _工时单价_ |
|
||||
| workHour | Double | _工时_ |
|
||||
| subtotal | Double | _金额_ |
|
||||
| ```plaintext<br> taxRateOutput <br>``` | BigDecimal | _销项税率_ |
|
||||
| ```plaintext<br> singleFavourable <br>``` | Double | 优惠金额 |
|
||||
| discountedSubtotal | Double | _折后金额_ |
|
||||
| **serviceMemo** | String | _单据服务项目备注_ |
|
||||
| discount | Double | 折扣 |
|
||||
| unusedNumber | Integer | 会员卡项目-未使用次数 |
|
||||
| number | Integer | 会员卡项目-总次数 |
|
||||
| ```plaintext<br>favourableVoList <br>``` | List<FavourableDetailPrintVo> | 优惠明细 |
|
||||
| ```plaintext<br>discountType <br>``` | Integer | 优惠类型(编码) |
|
||||
| ```plaintext<br>discountTypeName <br>``` | String | 优惠类型名称 |
|
||||
| amount | Double | 优惠金额 |
|
||||
| ```plaintext<br>sourceId <br>``` | String | 优惠项目的主键,如:如果优惠项是优惠券,那么该字段为优惠券的id |
|
||||
| **memo** | String | 项目说明 |
|
||||
| **qualityCheckEmployeeName** | String | 质检人姓名 |
|
||||
| **qualityCheckEmployeeCode** | String | 质检人工号 |
|
||||
| **cooperationMemo** | String | 协作备注 |
|
||||
| **totalWorkHour** | Double | _项目工时合计_ |
|
||||
| **serviceSubtotalAll** | Double | _工时费小计_ |
|
||||
| **serviceNum** | Double | _维修项目小计_ |
|
||||
| **serviceDisCountSubTotal** | Double | _项目折后金额合计_ |
|
||||
| \-------材料信息 | | |
|
||||
| partList | array | 材料条目 |
|
||||
| #### orderNumber | String | 序号(验证可用) |
|
||||
| sortNumber | String | _序号_ |
|
||||
| customCode | String | 材料编码 |
|
||||
| partName | String | _材料名称_ |
|
||||
| **partBrand** | String | _配件品牌_ |
|
||||
| spec | String | 规格型号 |
|
||||
| standard | String | 规格型号(旧) |
|
||||
| **partShowName** | String | _配件名称规格型号品牌_ |
|
||||
| **supplierCode** | String | _供应商编码(零件号)_ |
|
||||
| unit | String | _单位_ |
|
||||
| number | Double | _数量_ |
|
||||
| **nameMember** | String | _会员项目的来源名_ |
|
||||
| price | Double | _价格_ |
|
||||
| cost | Double | _材料成本_ |
|
||||
| subtotal | Double | _退货金额_ |
|
||||
| taxRateOutput | BigDecimal | _销项税率_ |
|
||||
| singleFavourable | Double | 优惠金额 |
|
||||
| discountedSubtotal | Double | _折后金额_ |
|
||||
| **partMemo** | String | _单据服务材料备注_ |
|
||||
| discount | Double | 折扣 |
|
||||
| **applyModel** | String | 适用车型 |
|
||||
| ```plaintext<br>cargoSpace <br>``` | String | 材料货位 |
|
||||
| **defSeats** | List<String> | 材料货位列表 |
|
||||
| ```plaintext<br>favourableVoList <br>``` | List<FavourableDetailPrintVo> | 优惠明细 |
|
||||
| ```plaintext<br>discountType <br>``` | Integer | 优惠类型(编码) |
|
||||
| ```plaintext<br>discountTypeName <br>``` | String | 优惠类型名称 |
|
||||
| ```plaintext<br>amount <br>``` | Double | 优惠金额 |
|
||||
| ```plaintext<br>sourceId <br>``` | String | 优惠项目的主键,如:如果优惠项是优惠券,那么该字段为优惠券的id |
|
||||
| \-------自带材料(新版维修/贴膜)信息 | | |
|
||||
| **bringPartList** | array | 自带材料条目 |
|
||||
| **partShowName** | String | 自带材料名称(文本) |
|
||||
| **photoList** | List<String> | 自带图片路径(url) |
|
||||
| empNameStr | String | 技师 |
|
||||
| outStockEmployeeName | String | 领料人 |
|
||||
| **isBring** | String | _是否自带_ |
|
||||
| **selfPartList** | array | _工单对应配件自带材料集合(内容同上面材料)_ |
|
||||
| **totalStuffNum** | String | _材料数量合计_ |
|
||||
| **selfTotalStuffNum** | String | _自带材料数量合计_ |
|
||||
| **stuffSubtotalAll** | Double | _材料费小计_ |
|
||||
| **partFavourableTotal** | Double | _材料优惠金额合计_ |
|
||||
| **partFavourableCommonTotal** | Double | _普通材料优惠金额合计_ |
|
||||
| **stuffDisCountTotal** | Double | _材料折后金额合计_ |
|
||||
| **managementCost** | Double | _进销差价合计_ |
|
||||
| \------附加费用信息 | | |
|
||||
| **extraChargeList** | | |
|
||||
| **sortNumber** | String | _序号_ |
|
||||
| **extraName** | String | _附加费名称_ |
|
||||
| **subtotal** | Double | _金额_ |
|
||||
| **memo** | String | _备注_ |
|
||||
| \-----维修结算费用集合(江苏结算单) | | |
|
||||
| **mainCostList** | array | |
|
||||
| **sortNumber** | String | _序号_ |
|
||||
| **costName** | String | _名称_ |
|
||||
| **memo** | String | _备注_ |
|
||||
| **subtotal** | Double | _金额_ |
|
||||
| \----_其他结算费用集合(江苏结算单)_ | | |
|
||||
| **otherCostList** | array | 内容同上 维修结算费用集合 |
|
||||
| **extraCostTotal** | Double | _附加费小计_ |
|
||||
| **allOtherCost** | Double | _附加费合计应收_ |
|
||||
| **favourableExtraCost** | Double | _附加费优惠金额_ |
|
||||
| **receiptAmount** | Double | _实收金额(已收金额-储值卡金额,如未收款,则还加入了欠款金额+客户等级优惠-积分优惠-结清优惠)_ |
|
||||
| **receiptAmountChinese** | String | _实收金额大写(逻辑同上)_ |
|
||||
| **amountReal** | Double | 工单收款后真正的实收 |
|
||||
| **chineseAmount** | String | _实收金额(大写)(逻辑同上)_ |
|
||||
| **payItemTogetherChinese** | String | _付款方式总额大写_ |
|
||||
| **payItemTogetherExcludeAccountAmountChinese** | String | 付款总额(排除记账金额)大写 |
|
||||
| **settleOweAmout** | Double | 结算单中用的待付金额(未收),使用后台逻辑算好 |
|
||||
| **settleOweAmoutChinese** | String | 待付金额大写 |
|
||||
| \-----结算付款方式及优惠保存信息 | array | |
|
||||
| **settlementPayItemList** | | |
|
||||
| **payWay** | String | _付款方式_ |
|
||||
| **payAmount** | Double | _付款金额_ |
|
||||
| **chinesePayAmount** | String | _大写付款方式_ |
|
||||
| **accountAgreementName** | String | _记账客户名称_ |
|
||||
| \-----付款方式信息 | array | |
|
||||
| **payItemList** | | |
|
||||
| **payWay** | String | _付款方式_ |
|
||||
| **payAmount** | Double | _付款金额_ |
|
||||
| **chinesePayAmount** | String | _大写付款方式_ |
|
||||
| \-----附加项目(江苏结算单) | | |
|
||||
| **extraPrintVo** | obj | |
|
||||
| **commissionCustomName** | String | _代办费自定义名称_ |
|
||||
| **commissionCost** | Double | _代办费成本_ |
|
||||
| **commissionMemo** | String | _代办费备注_ |
|
||||
| **diagnosisCustomName** | String | _诊断费自定义名称_ |
|
||||
| **diagnosisCost** | Double | _诊断费成本_ |
|
||||
| **diagnosisItemName** | String | _诊断详细名称_ |
|
||||
| **diagnosisMemo** | String | _诊断费备注_ |
|
||||
| **checkCustomName** | String | _检查费自定义名称_ |
|
||||
| **checkCost** | Double | _检查费成本_ |
|
||||
| **checkMemo** | String | _检查费备注_ |
|
||||
| **processCustomName** | String | _加工费自定义名称_ |
|
||||
| **processCost** | Double | _加工费成本_ |
|
||||
| **processMemo** | String | _加工费备注_ |
|
||||
| **processItemName** | String | _加工详细名称_ |
|
||||
| **managementCustomName** | String | _管理费自定义名称_ |
|
||||
| **managementCost** | Double | _管理费成本_ |
|
||||
| **managementMemo** | String | _管理费备注_ |
|
||||
| \-----二期新增字段 | | |
|
||||
| **oilCapacity** | String | 油量 |
|
||||
| **nextMileage** | Double | _下次保养里程(工单数据源,目前维保、洗车单读取)_ |
|
||||
| **nextMaintainDate** | String | _下次保养日期(工单数据源,目前维保、洗车单读取)_ |
|
||||
| **nextMileageRemind** | Double | _下次服务里程(服务提醒数据源,目前维修、贴膜单读取)_ |
|
||||
| **nextMaintainDateRemind** | Long | _下次服务时间(服务提醒数据源,目前维修、贴膜单读取)_ |
|
||||
| **repairPersonContact** | String | _送修人联系方式_ |
|
||||
| **memberCardNo** | String | _会员号_ |
|
||||
| **points** | String | _积分_ |
|
||||
| **czkList** | array | 储值卡列表 |
|
||||
| **name** | String | 名称 |
|
||||
| **memberCardNo** | String | 卡号 |
|
||||
| ```plaintext<br> **cardOwner** <br>``` | String | 持卡人 |
|
||||
| **amount** | Double | 金额 |
|
||||
| **cardList** | array | 套餐卡列表 |
|
||||
| **name** | String | 名称 |
|
||||
| **memberCardNo** | String | 卡号 |
|
||||
| ```plaintext<br> **cardOwner** <br>``` | String | 持卡人 |
|
||||
| **amount** | Double | 金额 |
|
||||
| **combineServiceAndPartList** | array | 项目材料组合列表 |
|
||||
| **servicePrintVo** | 参见serviceList | |
|
||||
| **partPrintVo** | 参见partList | |
|
||||
|
||||
# 案例记录:
|
||||
|
||||
#### 1.结算前 待付 结算后实付(跟进结算状态判断,7100 为已结算)
|
||||
|
||||
```plaintext
|
||||
$P{balanceStatus}.equals("7100")?($P{amountAll}.subtract($P{vipExpense}).subtract($P{czkExpense}).subtract($P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable})).setScale( 2, BigDecimal.ROUND_HALF_EVEN ).toString()+($P{payItemTogether}==null?"":"("+$P{payItemTogether}+")")):($P{amountAll}.subtract($P{vipExpense}).subtract($P{czkExpense}).subtract($P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable})).setScale( 2, BigDecimal.ROUND_HALF_EVEN ))
|
||||
```
|
||||
|
||||
* 对应的汉字
|
||||
|
||||
|
||||
```plaintext
|
||||
$P{balanceStatus}.equals("7100")?$P{payItemTogetherChinese}:$P{settleOweAmoutChinese}
|
||||
```
|
||||
|
||||
# 工具类jar包附件下载:
|
||||
|
||||
[请至钉钉文档查看附件《print-core-1.0.7.jar》](https://alidocs.dingtalk.com/i/nodes/vy20BglGWOexYpophlEGoZvGJA7depqY?iframeQuery=anchorId%3DX02mjljl3qzo6fk6o7712b)
|
||||
|
||||
### 数字金额转中文方法调用示例:
|
||||
|
||||
**数字金额**:$P{amount}==null?BigDecimal.ZERO:$P{amount}
|
||||
**转中文**:**com.f6car.printserver.core.CharacterUtil.chinese(**$P{amount}==null?BigDecimal.ZERO:$P{amount})
|
||||
|
||||
### 日期时间戳转日期示例:
|
||||
|
||||
**日期格式选择:**java.lang.Long
|
||||
|
||||
**日期时间戳**:$P{nextMaintainDateRemind} **转为目标格式**:$P{nextMaintainDateRemind} == null ? "" : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date($P{nextMaintainDateRemind}))
|
||||
|
||||
其中,"yyyy-MM-dd HH:mm:ss" 根据实际需求指定,比如到日则选择 "yyyy-MM-dd"
|
||||
|
||||
### 洗车单
|
||||
|
||||
洗车单小票不支持定制,定制洗车单的模板名称必须包含“洗车单”三个字 ,否则无法显示对应模板
|
||||
|
||||
> 更新: 2025-02-24 17:08:50 原文: <https://xcz.yuque.com/ombipo/rpc7ms/ro5fs1>
|
||||
+1034
File diff suppressed because it is too large
Load Diff
+262
@@ -0,0 +1,262 @@
|
||||
# 打印单定制介绍
|
||||
|
||||
# 打印单定制介绍
|
||||
|
||||
| **最常用的基础操作** |
|
||||
| --- |
|
||||
| [1、新增静态文本](#jwTdk) |
|
||||
| [2、新增动态字段(例如想要展示结算单中的工单号)](#fKwpz) |
|
||||
| [3、列表中增减字段(例如结算单中的项目列表和材料列表)](#IUyOs) |
|
||||
| [4、新增边框和样式设置](#RKqIf) |
|
||||
| [6、保存+输出](#M31IF) |
|
||||
| [7、打印单后台配置](#gr6xz) |
|
||||
|
||||
## 常用链接地址:
|
||||
|
||||
###### 打印单后台地址
|
||||
|
||||
[http://print.f6yc.com/print-server/ui/index.html#/template/classification](http://print.f6yc.com/print-server/ui/index.html#/template/classification)
|
||||
|
||||
###### 打印单模板样式
|
||||
|
||||
[https://xcz.yuque.com/ombipo/rpc7ms/fbd6ay?singleDoc#](https://xcz.yuque.com/ombipo/rpc7ms/fbd6ay?singleDoc#) 《打印单各类模板样式》
|
||||
|
||||
###### 打印单参数表
|
||||
|
||||
[https://xcz.yuque.com/ombipo/rpc7ms/ro5fs1?singleDoc#](https://xcz.yuque.com/ombipo/rpc7ms/ro5fs1?singleDoc#) 《打印单最新接口参数》
|
||||
|
||||
###### 打印单工具简易开发教程(附带案例)
|
||||
|
||||
[《打印单定制简易开发教程》](https://alidocs.dingtalk.com/i/nodes/dQPGYqjpJYgZGbvbCdEKGDGZWakx1Z5N?utm_scene=team_space)
|
||||
|
||||
## 工具下载
|
||||
|
||||
jdk1.8 使用 jaspersoft6.8版本
|
||||
|
||||
[请至钉钉文档查看附件《Jaspersoft Studio-6.8.0.zip》](https://alidocs.dingtalk.com/i/nodes/20eMKjyp81R0ndXdsdYe4BaDWxAZB1Gv?corpId=&iframeQuery=anchorId%3DX02mgkgykvfbfbiqcbc8b4)
|
||||
|
||||
WIN:
|
||||
|
||||
[请至钉钉文档查看附件《Jaspersoft Studio-6.3.1.final.rar》](https://alidocs.dingtalk.com/i/nodes/20eMKjyp81R0ndXdsdYe4BaDWxAZB1Gv?corpId=&iframeQuery=anchorId%3DX02mki20wvdslzwftp0ao)
|
||||
|
||||
MAC:
|
||||
|
||||
[请至钉钉文档查看附件《TIBCOJaspersoftStudio-6.3.1.final-mac-x86\_64.zip》](https://alidocs.dingtalk.com/i/nodes/20eMKjyp81R0ndXdsdYe4BaDWxAZB1Gv?corpId=&iframeQuery=anchorId%3DX02mki26xyvtjfkmi00ki)
|
||||
|
||||
## 打印单模板修改流程
|
||||
|
||||
#### 1、下载需要的模板
|
||||
|
||||
###### 通过模板名称,直接到模板管理中通过模板名称查询并下载
|
||||
|
||||

|
||||
|
||||
#### 2、打开编辑工具 TIBCO Jaspersoft Studio
|
||||
|
||||
###### 打开文件夹,双击Jaspersoft Studio.exe 运行工具
|
||||
|
||||

|
||||
|
||||
###### 点击File-->Open File-->选择下载的模板文件
|
||||
|
||||
###### 进入编辑模板的页面
|
||||
|
||||

|
||||
|
||||
#### 3、常见编辑操作
|
||||
|
||||
##### 1.新增静态文本
|
||||
|
||||
###### 新增组件到模板中
|
||||
|
||||

|
||||
|
||||
###### 双击组件编辑显示文本
|
||||
|
||||

|
||||
|
||||
###### 调整组件大小和位置参数
|
||||
|
||||

|
||||
|
||||
##### 2.新增动态字段(例如想要展示结算单中的工单号)
|
||||
|
||||
###### 在参数表中搜索想要的参数名称和类型:名称是:billNo 类型是文本信息=java.lang.String
|
||||
|
||||

|
||||
|
||||
###### 拖拽一个 Text Field 组件到模板中
|
||||
|
||||

|
||||
|
||||
###### 双击组件写入公式固定写法$P{参数名称}
|
||||
|
||||
###### 
|
||||
|
||||
###### 遇到提示:The current expression is not valid. Please verify it!;表明这个参数在模板中没有预先创建,需要手动创建参数信息
|
||||
|
||||

|
||||
|
||||
###### 模板中预设参数信息:Outline-->Parameters(右键单击)-->Create Parameter
|
||||
|
||||

|
||||
|
||||
###### 编辑参数信息:Name(填写参数名称);Class(数字就选择:java.math.BigDecimal 文本就选择java.lang.String)
|
||||
|
||||

|
||||
|
||||
##### 3.列表中增减字段(例如结算单中的项目列表和材料列表)
|
||||
|
||||
###### 例如查找材料名称,可以发现参数名是partName,是在一个名字叫partList 的列表里面的,在材料信息的列表中能使用到的参数就只有partList下的这个参数,其他参数无法在列表中直接使用(例如工单号在材料列表中展示不了)
|
||||
|
||||

|
||||
|
||||
###### 双击需要编辑的列表,进入列表编辑页面
|
||||
|
||||

|
||||
|
||||
###### 编辑方式与新增动态字段相同,但是固定写法从$P{参数名称} 改为 $F{参数名称}
|
||||
|
||||

|
||||
|
||||
###### 提示The current expression is not valid. Please verify it! 参数没有预设时,在列表编辑页面中新增,逻辑与上面相同
|
||||
|
||||
##### 4.新增边框和样式设置
|
||||
|
||||
###### 选择需要编辑的组件,选择Boeders 进行编辑
|
||||
|
||||

|
||||
|
||||
#### 4、常见语法介绍
|
||||
|
||||
###### 文本拼接参数:$P{参数名称}+"特定文本内容" —— 例如打印单标题:$P{printOrgName}+"结算单"
|
||||
|
||||
###### 小数保留2位小数或多位:$P{参数名称}.setScale( 保留几位小数, BigDecimal.ROUND\_DOWN ) ——例如折后金额小计,保留2位小数:$P{stuffSubtotalAll}.setScale( 2, BigDecimal.ROUND\_DOWN )
|
||||
|
||||
###### 字符串截取:$P{参数名称}.substring(起始位置,截取长度)——例如进厂日期,保留前10位:$P{billDate}.substring(0,10)
|
||||
|
||||
###### 三元运算-IF判断:(关系表达式) ? 表达式1 : 表达式2 ——例如打印单标题:($P{printOrgName}==null?$P{orgName}:($P{printOrgName}.isEmpty()?$P{orgName}:$P{printOrgName}))+"结算单"
|
||||
|
||||
###### 常见运算:
|
||||
|
||||
是否相等:”==“ 或者 $P{参数名称}.equals("文本内容")
|
||||
|
||||
加:$P{参数名称1}.add($P{参数名称2})
|
||||
|
||||
减:$P{参数名称1}.subtract($P{参数名称2})
|
||||
|
||||
乘:$P{参数名称1}.multiply($P{参数名称2}) ;$P{参数名称1}.multiply(new BigDecimal(1.13))
|
||||
|
||||
除:$P{参数名称1}.divide($P{参数名称2}, 2, BigDecimal.ROUND_HALF_UP)
|
||||
|
||||
#### 4.1、高级语法介绍
|
||||
|
||||
###### jar包导入:例如金额转大写,研发通过编写一个jar工具包实现特定功能,下面是导入jar包步骤
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
启用成功后按照研发语法实现具体功能,比如
|
||||
|
||||
###### 从list中取特定的值写入外层表格中:该公式使用jdk1.8语法,jaspersoft6.8可用
|
||||
|
||||
下面表达式的意思是,从支付方式列表(payItemList) 中找到
|
||||
|
||||
支付方式(payWay)
|
||||
|
||||
等于“记账”的
|
||||
|
||||
第一个支付金额(payAmount)
|
||||
|
||||
```java
|
||||
$P{payItemList}.getData().stream()
|
||||
.filter(map -> "记账".equals(map.get("payWay")))
|
||||
.map(map -> {
|
||||
Object amt = map.get("payAmount");
|
||||
return amt == null ? BigDecimal.ZERO : new BigDecimal(amt.toString());
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(BigDecimal.ZERO)
|
||||
```
|
||||
|
||||
#### 5、格式预览
|
||||
|
||||
###### 工具中只能预览模板的样式,涉及到参数判断的需要将模板上传到门店后在F6系统工单中打印预览
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### 6、保存+输出
|
||||
|
||||
###### 保存
|
||||
|
||||

|
||||
|
||||
###### 选择.jrxml的文件,右键选择Compile Report 进行编译
|
||||
|
||||

|
||||
|
||||
###### 选择.jasper的文件,右键选择Export Files to...
|
||||
|
||||

|
||||
|
||||
###### 另存到桌面
|
||||
|
||||

|
||||
|
||||
#### 7、打印单后台配置
|
||||
|
||||
###### 新增/编辑模板
|
||||
|
||||
注意:不要随便删除模板,删除一定要再三确认清楚,避免出现误操作的情况(删除不可恢复) 预计5.16号后 对删除的功能二次确认进行优化。
|
||||
|
||||

|
||||
|
||||
###### 模板名称命名规范
|
||||
|
||||
* **简单调整**\*\***\*\*模板名称:基础表+特殊修改需求**
|
||||
|
||||
|
||||
**模板编码:修改人姓名首字母英文大写+模板分类+日期**
|
||||
|
||||
**模板备注:模板各修改点**
|
||||
|
||||

|
||||
|
||||
* **定制调整**\*\***\*\*模板名称:门店名称+定制**
|
||||
|
||||
|
||||
**模板编码:修改人姓名首字母英文大写+模板分类+日期**
|
||||
|
||||
**模板备注:模板各修改点**
|
||||
|
||||

|
||||
|
||||
###### 给指定门店配置打印单
|
||||
|
||||

|
||||
|
||||
#### 8、常见打印分类及对应的通用模板
|
||||
|
||||
| 常见打印单分类 | 对应系统上的打印模块 | 通用模板名称 | 模板编码 |
|
||||
| --- | --- | --- | --- |
|
||||
| 新结算单打印 | 维保单 | F6标准结算单(壹) | newSettleFirst |
|
||||
| 结算单-新 | 除维保单的其他单据(维修单、贴膜单......) | 无 | 无 |
|
||||
| 附表-新 | 新附表 | 无 | 无 |
|
||||
| 销售单 | 销售单 | 销售单(日期版) | xiaoshodanriqiban |
|
||||
| 洗车单 | 洗车单 | 洗车单 | wash01 |
|
||||
| 报价单打印 | 报价单 | 报价单打印 | quotationPrint |
|
||||
| 新库存入库单打印 | 入库单 | 新库存入库单打印 | 9001 |
|
||||
| 新库存出库单打印 | 出库单 | 新库存出库单打印 | 9002 |
|
||||
| | | | |
|
||||
| **注:采购单和采购退货单走打印平台定制,先向赵亚妮提供门店编码、门店名称,开通后再上传配置门店生效** | | | |
|
||||
|
||||
**注意:结算单要在收款后页面打印,请确保模板名称中包含“结算单”三个字**
|
||||
|
||||
> 更新: 2025-05-16 13:54:24 原文: <https://xcz.yuque.com/ombipo/obbigo/kg2qc1sbszwk48bf>
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
# 报价单接口参数
|
||||
|
||||
# 报价单接口参数
|
||||
|
||||
# 参数说明
|
||||
|
||||
## 主单信息
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| partDetailVoList | 材料列表 | | List<QuotationServiceDetailPrintVo> |
|
||||
| serviceDetailVoList | 项目列表 | | List<QuotationPartDetailPrintVo> |
|
||||
| amountChinese | 商品金额中文大写 | | |
|
||||
| realAmountWithoutCardChinese | 待付金额中文大写 | | |
|
||||
|
||||
## QuotationPartDetailPrintVo
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| pkId | 无意义主键 | 是 | BigInteger |
|
||||
| idOwnOrg | 门店id | 是 | BigInteger |
|
||||
| idQuotation | 报价单id | 是 | BigInteger |
|
||||
| idPart | 材料id | 否 | BigInteger |
|
||||
| partName | 材料名称(非组合,对应材料名称字段) | 是 | String |
|
||||
| partShowName | 材料在界面上显示的名称(组合供应商编码等信息) | 是 | String |
|
||||
| idMdmPart | 云材料ID | 否 | String |
|
||||
| labelId | 业务分类id | 否 | BigInteger |
|
||||
| labelName | 荣誉的业务分类名称 | 否 | String |
|
||||
| number | 数量 | 是 | BigDecimal |
|
||||
| price | 单价 | 是 | BigDecimal |
|
||||
| subtotal | 总价 | 是 | BigDecimal |
|
||||
| idEmployee | 服务员工id | 否 | String |
|
||||
| employeeName | 服务员工姓名 | 否 | String |
|
||||
| isMember | 套餐2,套餐卡1,普通0 | 是 | byte |
|
||||
| memo | 备注 | 否 | String |
|
||||
| discount | 折扣 | 是 | BigDecimal |
|
||||
| realSubtotal | 折后总计 | 是 | BigDecimal |
|
||||
| stockNumber | 门店库存数量 | 否 | BigDecimal |
|
||||
| groupId | 公司id | 是 | BigInteger |
|
||||
| unit | 单位 | 否 | String |
|
||||
| spec | 规格型号 | 否 | String |
|
||||
| brand | 品牌名称 | 否 | String |
|
||||
| brandId | 品牌id | 否 | String |
|
||||
| supplierCode | 零件号 | 否 | String |
|
||||
|
||||
## QuotationServiceDetailPrintVo
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| pkId | 无意义主键 | 是 | BigInteger |
|
||||
| customCode | 项目编码 | 否 | String |
|
||||
| serviceName | 项目名称 | 是 | String |
|
||||
| workHour | 工时 | 否 | BigDecimal |
|
||||
| price | 单价 | 是 | BigDecimal |
|
||||
| subtotal | 工时费 | 是 | BigDecimal |
|
||||
| discount | 折扣 | 是 | BigDecimal |
|
||||
| realSubtotal | 折后金额 | 是 | BigDecimal |
|
||||
| memo | 备注 | 否 | String |
|
||||
|
||||
> 更新: 2025-05-26 14:19:50 原文: <https://xcz.yuque.com/ombipo/rpc7ms/gcgxuuzvmyhs4eoe>
|
||||
+675
@@ -0,0 +1,675 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
|
||||
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="jiesuandan" pageWidth="595" pageHeight="842" columnWidth="575" leftMargin="15" rightMargin="5" topMargin="10" bottomMargin="10" uuid="8fdb09f3-da43-46f9-a6cb-2b26a2247961">
|
||||
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
|
||||
<property name="com.jaspersoft.studio.unit." value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/>
|
||||
<style name="Table_TH" mode="Opaque" backcolor="#FFFFFF">
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineColor="#000000"/>
|
||||
<topPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.5" lineColor="#000000"/>
|
||||
</box>
|
||||
</style>
|
||||
<style name="Table_CH" mode="Opaque" backcolor="#FFFFFF">
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineColor="#000000"/>
|
||||
<topPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.5" lineColor="#000000"/>
|
||||
</box>
|
||||
</style>
|
||||
<style name="Table_TD" mode="Opaque" backcolor="#FFFFFF">
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineColor="#000000"/>
|
||||
<topPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.5" lineColor="#000000"/>
|
||||
</box>
|
||||
</style>
|
||||
<subDataset name="Dataset1" uuid="22e86b94-acb8-45ed-960f-04558f91ad82">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="row1" class="java.lang.String"/>
|
||||
<field name="row2" class="java.lang.String"/>
|
||||
<field name="row3" class="java.lang.String">
|
||||
<fieldDescription><![CDATA[]]></fieldDescription>
|
||||
</field>
|
||||
<field name="row4" class="java.lang.String"/>
|
||||
<field name="row5" class="java.lang.String"/>
|
||||
<field name="row6" class="java.lang.String"/>
|
||||
<field name="row7" class="java.lang.String"/>
|
||||
<field name="row8" class="java.lang.String"/>
|
||||
<field name="row9" class="java.lang.String"/>
|
||||
<field name="row10" class="java.lang.String"/>
|
||||
<field name="row11" class="java.lang.String"/>
|
||||
<field name="row12" class="java.lang.String"/>
|
||||
<field name="row13" class="java.lang.String"/>
|
||||
<field name="row14" class="java.lang.String"/>
|
||||
</subDataset>
|
||||
<parameter name="orgName" class="java.lang.String"/>
|
||||
<parameter name="gatheringTime" class="java.lang.String"/>
|
||||
<parameter name="title" class="java.lang.String"/>
|
||||
<parameter name="printTime" class="java.lang.String"/>
|
||||
<parameter name="rowList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="column1" class="java.lang.String"/>
|
||||
<parameter name="column2" class="java.lang.String"/>
|
||||
<parameter name="column3" class="java.lang.String"/>
|
||||
<parameter name="column4" class="java.lang.String"/>
|
||||
<parameter name="column5" class="java.lang.String"/>
|
||||
<parameter name="column6" class="java.lang.String"/>
|
||||
<parameter name="column7" class="java.lang.String"/>
|
||||
<parameter name="column8" class="java.lang.String"/>
|
||||
<parameter name="column9" class="java.lang.String"/>
|
||||
<parameter name="column10" class="java.lang.String"/>
|
||||
<parameter name="column11" class="java.lang.String"/>
|
||||
<parameter name="column12" class="java.lang.String"/>
|
||||
<parameter name="column13" class="java.lang.String"/>
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<detail>
|
||||
<band height="92">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="-10" width="575" height="84" uuid="7ddf9306-7451-4ea4-bd57-cee86f8e13c9">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="16"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{title}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement positionType="Float" mode="Opaque" x="1" y="74" width="48" height="18" backcolor="#FFFFFF" uuid="5da51596-6830-44c5-98e4-691007be1a4e">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[所属门店:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="49" y="74" width="526" height="18" uuid="954bf3e6-7bf3-4deb-becf-217784bdc82a">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{orgName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="25">
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="260" y="0" width="315" height="18" uuid="36caab98-d260-4527-b87e-568ac54f61eb">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="0" rightIndent="5"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA["收款时间:"+$P{gatheringTime}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="36">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="46" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="b7ced69b-d8a8-45d7-b05b-3a3b83324d93">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column2}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column2}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="90" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="c53a49f2-2d34-455e-b62d-b249f1fced35">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column3}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column3}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="134" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="3385087d-7234-411d-8d79-738cac0295fd">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column4}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column4}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="266" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="ccde0db9-971e-4912-9249-27361902f1b9">
|
||||
<printWhenExpression><![CDATA[$P{column7}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column7}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="310" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="0ae42faa-c6a8-47d6-9620-48ec03fd35d0">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column8}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column8}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="178" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="62d27b1c-bebc-4a97-8fa0-2a6a497ab35e">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column5}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column5}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="222" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="e459b7e6-0136-4c4e-ac76-6a4894311481">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column6}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column6}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="442" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="dbb18e36-2d02-4970-9440-c2d8634e5c39">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column11}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column11}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="398" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="49242b96-56f8-4c62-85f6-87a0342c936d">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column10}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column10}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="486" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="900e6d7d-dc55-4f19-8c11-3ad4429f66ac">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column12}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column12}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="354" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="e0da6daf-273a-4d28-b333-7c91e68a3ee6">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column9}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column9}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="530" y="0" width="45" height="36" backcolor="#D4D4D4" uuid="ad9f6d64-65f9-4dcd-8dbc-37e5899c528a">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{column13}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{column13}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement mode="Opaque" x="1" y="0" width="45" height="36" backcolor="#D4D4D4" uuid="162abd58-679f-4c66-a256-458805e89193">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<text><![CDATA[]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="16" y="0" width="30" height="18" uuid="652d5210-41df-4c62-a4ba-97f2e42b65ac">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<text><![CDATA[内容]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="1" y="18" width="30" height="18" uuid="6e239a7c-f268-4ba1-b26d-f59cea6bb5ff">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<text><![CDATA[方式]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement mode="Transparent" x="1" y="0" width="45" height="36" backcolor="#D9D9D9" uuid="b02cf093-98f9-4cb6-8bc2-d6611a303b4e"/>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.5"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="19">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<componentElement>
|
||||
<reportElement isPrintRepeatedValues="false" x="1" y="0" width="574" height="18" uuid="23ae66f5-6e0a-4c24-9fa5-2237d31a1aea">
|
||||
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
|
||||
<property name="com.jaspersoft.studio.table.style.table_header" value="Table 1_TH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.column_header" value="Table 1_CH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.detail" value="Table 1_TD"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
|
||||
<datasetRun subDataset="Dataset1" uuid="f221c952-cad7-4dff-9cf7-ca32d2ddef3b">
|
||||
<dataSourceExpression><![CDATA[$P{rowList}]]></dataSourceExpression>
|
||||
</datasetRun>
|
||||
<jr:column width="45" uuid="9627a917-c5b0-4b80-b21f-449e4a1af32a">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="px"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="0" y="0" width="45" height="18" backcolor="#D4D4D4" uuid="05c9f6b2-819e-415f-8505-e8be2889cab5">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$F{row1}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row1}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="cd30f1df-f54b-42a6-bf80-fcecb57824dc">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="fe6de99c-5be1-4d46-9282-c4d77f126dc9">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$F{row2}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row2}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="badfb61b-4c3d-443e-9d96-ce2f8871c148">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="bbc0c8d1-ff73-4d70-bfd9-a3f514bf894b">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$F{row3}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row3}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="e54a7626-9237-4d3e-bb62-d48c41475cfd">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column4"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="8b267259-7609-4612-b6f2-417498e8409f">
|
||||
<printWhenExpression><![CDATA[$F{row4}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row4}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="1b3cbfb0-6d88-4b14-9215-cedc2a5b9c5e">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column5"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="7f6b00bd-a8ce-42ca-94e3-0f9202796b7d">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$F{row5}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row5}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="867931a4-9fef-46d7-85df-5d77f61bc7a9">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column6"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="e38c4d71-5e05-43c6-9d85-cbbdc4b2859b">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$F{row6}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row6}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="807004cb-f00a-4852-bc11-2a5ad1ae65b5">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column7"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="5cb940bc-d863-486d-a8e2-730002019abb">
|
||||
<printWhenExpression><![CDATA[$F{row7}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row7}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="baa16adc-a1b3-4e93-a6ee-5f5b705fd72d">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column8"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="f66b1d3d-3144-4bea-b57a-571187c122f3">
|
||||
<printWhenExpression><![CDATA[$F{row8}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row8}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="bee2d403-9d56-4d85-8687-2c4101cc6d06">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column9"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="9e46c6f4-841b-467d-97f1-0542179f7cc6">
|
||||
<printWhenExpression><![CDATA[$F{row9}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row9}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="fd8e7505-d366-4fde-96ac-1df719f7272e">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column10"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="f0783afb-cce2-47d8-be32-55ab498515a6">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$F{row10}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row10}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="53cbd27d-048c-4200-8dd5-b21671c68e4b">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column11"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="cce29502-097d-4763-b5c0-330093e14041">
|
||||
<printWhenExpression><![CDATA[$F{row11}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row11}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="44" uuid="9f2c3f4f-bc90-430d-b865-795def749d2d">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column12"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="0" width="44" height="18" uuid="29c0a84c-f4d0-4e4b-8d19-3d13a024552b">
|
||||
<printWhenExpression><![CDATA[$F{row12}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row12}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="45" uuid="3def2e39-4c08-458e-8c36-df8eac735c65">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column13"/>
|
||||
<jr:detailCell height="18">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement mode="Opaque" x="0" y="0" width="45" height="18" backcolor="#FFFFFF" uuid="573d45d1-bf4e-43e1-8282-49508130a3c0">
|
||||
<printWhenExpression><![CDATA[$F{row13}!=null]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.4"/>
|
||||
</box>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="8"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{row13}]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
</jr:table>
|
||||
</componentElement>
|
||||
</band>
|
||||
<band height="75">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="454" y="10" width="52" height="18" uuid="c1af5cd0-e851-4fd6-a16b-f461a743d336">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[出纳签字:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="2" y="50" width="48" height="18" uuid="1cf7fa11-8f01-4764-a9a8-d981c9f32ca6">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[打印时间:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="50" width="124" height="18" uuid="b2daacc3-cb7c-443a-946b-be951bf080d5">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{printTime}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="454" y="50" width="52" height="18" uuid="3c4cbf80-712b-4e51-a861-1b1643642612">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[收银签字:]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
</detail>
|
||||
</jasperReport>
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
# 新版附表打印接口文档
|
||||
|
||||
# 新版附表打印接口文档
|
||||
|
||||
# 接口出参
|
||||
|
||||
| 字段 | 含义 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| billNo | 附表单号 | String |
|
||||
| fromBillNo | 附表源工单号 | String |
|
||||
| fromMaintainType | 来源单据类型 | String |
|
||||
| billDate | 进厂日期 | String |
|
||||
| creatorName | 创建人名称 | String |
|
||||
| creationtime | 创建时间 | String |
|
||||
| naEmployee | 服务顾问 | String |
|
||||
| employeePhone | 服务顾问手机号 | String |
|
||||
| businessTypeName | 业务类型 | String |
|
||||
| nextMaintainDate | 下次保养日期 | String |
|
||||
| oilCapacity | 当前油量 | String |
|
||||
| mileage | 出厂里程(进厂里程) | Double |
|
||||
| nextMileage | 下次保养里程 | Double |
|
||||
| vin | 车辆VIN码 | String |
|
||||
| carNoWhole | 车牌号 | String |
|
||||
| carModel | 品牌车型全称 | String |
|
||||
| carModelShort | 车型简称 | String |
|
||||
| carColor | 车身颜色 | String |
|
||||
| carCategoryName | 车辆分类名称 | String |
|
||||
| carBrandName | 车辆品牌名称 | String |
|
||||
| carSeriesName | 车系名称 | String |
|
||||
| engineNumber | 发动机号 | String |
|
||||
| transmissionNo | 变速箱号 | String |
|
||||
| registerDate | 车辆注册日期 | String |
|
||||
| cardDate | 车辆发证日期 | String |
|
||||
| carNatureOfUseName | 车辆使用性质 | String |
|
||||
| carFuelTypeName | 车辆燃料(能源)类型 | String |
|
||||
| carSourceName | 车辆来源 | String |
|
||||
| carOwnerName | 车辆所有人姓名 | String |
|
||||
| naCustomer | 客户姓名 | String |
|
||||
| customerSourceName | 客户来源名称 | String |
|
||||
| customerDetailAddress | 客户详细地址 | String |
|
||||
| cellPhone | 联系电话(客户) | String |
|
||||
| memberCardNo | 会员卡号 | String |
|
||||
| points | 客户积分 | String |
|
||||
| customerLevelName | 客户等级名称 | String |
|
||||
| repairPerson | 送修人 | String |
|
||||
| repairPersonContact | 送修人联系方式 | String |
|
||||
| memo | 备注 | String |
|
||||
| completeDate | 完工日期 | String |
|
||||
| firstGatheringTime | 初次收款时间 | String |
|
||||
| estimatedDeliveryTime | 预计交车时间 | String |
|
||||
| deliveryTime | 交车时间 | String |
|
||||
| printContent | 免责条款 | String |
|
||||
| printContentJs | 免责条款江苏 | printContentJs |
|
||||
| storeLogo | 门店logo | String |
|
||||
| orgName | 门店名称 | String |
|
||||
| orgMemo | 门店备注 | String |
|
||||
| orgContacts | 联系人(维修厂) | String |
|
||||
| orgContactNumber | 联系电话(维修厂) | String |
|
||||
| orgDetailAddress | 联系地址(维修厂) | String |
|
||||
| orgContactMobile | 联系电话(维修厂) | String |
|
||||
| fax | 传真 | String |
|
||||
| email | 组织邮件 | String |
|
||||
| bankAccount | 开户银行 | String |
|
||||
| accountNumber | 账号 | String |
|
||||
| businessLicenseCode | 企业执照号 | String |
|
||||
| channelName | 来店途径名称 | String |
|
||||
| printOrgName | 打印抬头(需读取配置) | String |
|
||||
| amountAll | 应收总计(合计金额) | Double |
|
||||
| amountAllChinese | 应收总计(合计金额)中文大写 | String |
|
||||
| disCountAll | 总优惠合计(附表:项目优惠+材料优惠+收银优惠) | Double |
|
||||
| disCountAllBak | 项目优惠+材料优惠+收银优惠,等同于disCountAll | Double |
|
||||
| amountReal | 实收金额 | Double |
|
||||
| chineseAmount | 实收金额(中文大写) | Double |
|
||||
| oweAmount | 未收金额 | Double |
|
||||
| vipExpense | 套餐卡消费金额(附表为0) | Double |
|
||||
| vipExpenseFavourable | 套餐卡优惠金额(附表为0) | Double |
|
||||
| czkExpense | 储值卡消费金额(附表为0) | Double |
|
||||
| czkExpenseFavourable | 储值卡优惠金额(附表为0) | Double |
|
||||
| remainAmount | 结算金额tsf<br/>附表:应收-项目优惠-材料优惠 | Double |
|
||||
| receivedAmount | 收款金额tsf<br/>附表:等同于已收金额 | Double |
|
||||
| serviceList | 项目集合 | List |
|
||||
| └─ sortNumber | 序号 | String |
|
||||
| └─ orderNumber | 序号(全部) | String |
|
||||
| └─ name | 名称 | String |
|
||||
| └─ serviceName | 项目名称 | String |
|
||||
| └─ labelName | 业务分类名称 | String |
|
||||
| └─ price | 工时单价 | Double |
|
||||
| └─ workHour | 工时 | Double |
|
||||
| └─ subtotal | 金额 | Double |
|
||||
| └─ singleFavourable | 优惠金额 | Double |
|
||||
| └─ discountedSubtotal | 折后金额 | Double |
|
||||
| └─ serviceMemo | 附加信息备注 | String |
|
||||
| └─ discount | 折扣 | Double |
|
||||
| └─ empNameStr | 服务项目明细对应修理工名称组装字符串 | String |
|
||||
| └─ infiniteFlag | 是否无限,0:否,1:是 | Integer |
|
||||
| └─ customCode | 自定义编码 | String |
|
||||
| totalWorkHour | 项目工时合计 | Double |
|
||||
| totalWorkHourVip | VIP项目工时合计(附表为0) | Double |
|
||||
| serviceSubtotalAll | 工时费小计 | Double |
|
||||
| serviceSubtotalVip | 服务项目明细小计(会员项目,附表为0) | Double |
|
||||
| serviceNum | 维修项目小计 | String |
|
||||
| serviceFavourable | 服务项目(非会员项目)客户等级优惠(附表为0) | Double |
|
||||
| serviceDiscountFavourable | 项目优惠 | Double |
|
||||
| serviceFavourableTotal | 项目优惠金额合计 | Double |
|
||||
| serviceFavourableCommonTotal | 普通项目优惠金额合计 | Double |
|
||||
| serviceDisCountSubTotal | 项目折后金额合计 | Double |
|
||||
| partList | 工单对应配件材料集合 | List |
|
||||
| └─ sortNumber | 序号 | String |
|
||||
| └─ orderNumber | 序号(全部) | String |
|
||||
| └─ name | 名称 | String |
|
||||
| └─ partName | 材料名称 | String |
|
||||
| └─ partShowName | 材料名称(全) | String |
|
||||
| └─ partBrand | 配件品牌 | String |
|
||||
| └─ standard | 配件名称规格型号品牌 | String |
|
||||
| └─ spec | 规格型号 | String |
|
||||
| └─ supplierCode | 供应商编码 | String |
|
||||
| └─ unit | 单位 | String |
|
||||
| └─ number | 数量 | Double |
|
||||
| └─ price | 价格(单价) | Double |
|
||||
| └─ subtotal | 金额(材料金额) | Double |
|
||||
| └─ discount | 折扣 | Double |
|
||||
| └─ singleFavourable | 优惠金额 | Double |
|
||||
| └─ discountedSubtotal | 折后金额 | Double |
|
||||
| └─ partMemo | 备注 | String |
|
||||
| └─ customCode | 自定义编码 | String |
|
||||
| └─ employeeName | 员工名称 | String |
|
||||
| └─ outStockEmployeeName | 领料人 | String |
|
||||
| └─ empNameStr | 明细对应修理工名称组装字符串 | String |
|
||||
| └─ labelName | 业务分类名称 | String |
|
||||
| └─ idPart | 配件材料pk | BigInteger |
|
||||
| └─ idInfo | 本地材料id(长码) | String |
|
||||
| └─ applyModel | 适用车型 | String |
|
||||
| stuffNum | 材料数目合计 | String |
|
||||
| totalStuffNum | 材料数量合计 | String |
|
||||
| totalStuffNumVip | Vip材料数量合计(附表为0) | String |
|
||||
| stuffSubtotalAll | 材料费小计 | Double |
|
||||
| stuffSubtotalVip | 材料收入小计(会员项目,附表为0) | Double |
|
||||
| partinfoFavourable | 材料项目(非会员项目)客户等级优惠(附表为0) | Double |
|
||||
| partinfoDiscountFavourable | 材料折扣优惠 | Double |
|
||||
| partFavourableTotal | 材料优惠金额合计 | Double |
|
||||
| partFavourableCommonTotal | 普通材料优惠金额合计 | Double |
|
||||
| stuffDisCountTotal | 材料折后金额合计 | Double |
|
||||
| pointFavourable | 积分优惠(附表为0) | Double |
|
||||
| packageFavourable | 套餐优惠(附表为0) | Double |
|
||||
| discountFavourable | 结清优惠(附表为0) | Double |
|
||||
| gatheringFavourable | 收银优惠 | Double |
|
||||
| couponFavourable | 优惠券优惠(附表为0) | Double |
|
||||
| czkDiscountFavourable | 储值卡折扣优惠(附表为0) | Double |
|
||||
| customerLevelFavourable | 客户级别优惠金额(附表为0) | Double |
|
||||
| extraChargeList | 附加费用集合 | List |
|
||||
| └─ sortNumber | 序号 | String |
|
||||
| └─ extraName | 附加费名称 | String |
|
||||
| └─ subtotal | 金额 | Double |
|
||||
| └─ memo | 备注 | String |
|
||||
| extraCostTotal | 附加费小计 | Double |
|
||||
| extraNumber | 附加费数量小计 | String |
|
||||
| managementCost | 管理费 | Double |
|
||||
| extraPrintVo | 附加项目 | ExtraPrintAttribute |
|
||||
| └─ commissionCustomName | 代办费自定义名称 | String |
|
||||
| └─ commissionCost | 代办费金额 | Double |
|
||||
| └─ commissionMemo | 代办费备注 | String |
|
||||
| └─ diagnosisCustomName | 诊断费自定义名称 | String |
|
||||
| └─ diagnosisCost | 诊断费金额 | Double |
|
||||
| └─ diagnosisItemName | 诊断详细名称 | String |
|
||||
| └─ diagnosisMemo | 诊断费备注 | String |
|
||||
| └─ checkCustomName | 检查费自定义名称 | String |
|
||||
| └─ checkItemName | 诊断详细名称 | String |
|
||||
| └─ checkCost | 检查费金额 | Double |
|
||||
| └─ checkMemo | 检查费备注 | String |
|
||||
| └─ processCustomName | 加工费自定义名称 | String |
|
||||
| └─ processCost | 加工费金额 | Double |
|
||||
| └─ processMemo | 加工费备注 | String |
|
||||
| └─ processItemName | 加工详细名称 | String |
|
||||
| └─ managementCustomName | 管理费自定义名称 | String |
|
||||
| └─ managementCost | 管理费金额 | Double |
|
||||
| └─ managementMemo | 管理费备注 | String |
|
||||
| └─ fuelName | 加油费 | String |
|
||||
| └─ fuelAmount | 加油费金额 | Double |
|
||||
| └─ trailName | 拖车费 | String |
|
||||
| └─ trailAmount | 拖车费金额 | Double |
|
||||
| allOtherCost | 附加费合计应收 | Double |
|
||||
| receiptAmount | 收据金额(附表:应收-项目优惠-材料优惠-收银优惠) | Double |
|
||||
| receiptAmountChinese | 收据金额中文大写 | String |
|
||||
| payItemList | 付款方式集合 | List |
|
||||
| └─ payWay | 付款方式 | String |
|
||||
| └─ payAmount | 付款金额 | Double |
|
||||
| └─ chinesePayAmount | 付款金额中文大写 | String |
|
||||
| payItemTogether | 付款方式拼接 | String |
|
||||
| payItemTogetherChinese | 付款方式总额中文大写 | String |
|
||||
| paymentTypeDetails | 支付方式汇总 | String |
|
||||
| settleOweAmout | 结算单中用的待付金额(未收) | Double |
|
||||
| settleOweAmoutChinese | 结算单中用的待付金额大写(未收) | String |
|
||||
| settleReceivedAmout | 结算单中用的实付金额(实收) | Double |
|
||||
| settleReceivedAmoutChinese | 结算单中用的实付金额大写(未收) | String |
|
||||
| realPayAmountChinese | 客户实付大写(应收-所有优惠) | String |
|
||||
| naInsurer | 理赔公司名称 | String |
|
||||
| insurancepolicyNo | 理赔单理赔保险单号 | String |
|
||||
| insuranceCompany | 保险公司名称 | String |
|
||||
| contactName | 联系人姓名 | String |
|
||||
| contactCellphone | 联系人电话 | String |
|
||||
| insuranceNo | 商业险单号 | String |
|
||||
| insuranceNoTCI | 交强险单号 | String |
|
||||
| insuranceExpiryDate | 商业险到期日 | String |
|
||||
| insuranceExpiryDateTCI | 交强险到期日 | String |
|
||||
| printEmployeeName | 打印人姓名 | String |
|
||||
| printTime | 打印时间 | String |
|
||||
| printCount | 打印次数 | String |
|
||||
|
||||
# 备注1
|
||||
|
||||
1. ++新版附表没有“状态”字段,所有模板中切勿使用 billStatus【单据状态】、balanceStatus【结算状态】来进行判断输出;采用直接取值方式填值++
|
||||
|
||||
1. ++没有了balanceStatus,采用收款方式列表payItemList判空的方式来验证是否有收款信息++
|
||||
|
||||
1. ++收银金额可写作:++$P{payItemList}.getRecordCount()==0?$P{receivedAmount}:$P{receivedAmount}.setScale( 2, BigDecimal.ROUND\_HALF\_EVEN ).toString()+"("+$P{paymentTypeDetails}+")"
|
||||
|
||||
2. ++待付金额可写作:$P{oweAmount}++
|
||||
|
||||
3. ++实付金额可写作:$P{amountReal}++
|
||||
|
||||
4. ++合计金额可写作:++$P{payItemList}.getRecordCount()==0?($P{settleOweAmout}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ):($P{settleReceivedAmout}).setScale( 2, BigDecimal.ROUND_HALF_EVEN )
|
||||
|
||||
5. ++大写可写作:++$P{payItemList}.getRecordCount()==0?$P{settleOweAmoutChinese}:$P{chineseAmount}
|
||||
|
||||
2. ++新版附表没有“单据类型”字段(原附表的maintainType为"GDFB"、"LPDFB"两种附表类型),所有模板中切勿使用 maintainType【单据类型】字段作为判断条件。根据情况可以取 fromMaintainType【来源单据类型】进行判断。如:++
|
||||
|
||||
1. ++是否展示理赔公司、理赔单等理赔相关信息栏,老附表判断逻辑为 maintainType.equals('LPDFB')。可更换为 fromMaintainType.equals('LPD')++
|
||||
|
||||
|
||||
# 备注2
|
||||
|
||||
1. 出参标注颜色为 **绿 色** 的字段,为 ++**附表本身内容**++(如车牌号、VIN码等) 或 ++**无法变更内容**++(如门店信息等)
|
||||
|
||||
1. 客户在附表页面直接变更信息,会直观反映在打印内容中。
|
||||
|
||||
2. 出参标注颜色为 **橙 色** 的字段,为 ++**通过客户ID、车辆ID、项目ID、材料ID**++ 等,++**反查**++ 基础数据获得内容,
|
||||
|
||||
1. 若客户通过 ++**附表页面选择组件方式**++ 修改附表信息,因ID发生变化,该部分打印内容会随之变化,变更信息将 ++**会体现在打印内容中**++。
|
||||
|
||||
2. 若客户通过 ++**手动填写文本内容方式**++ 修改附表信息,因ID未发生变化,该部分打印内容不会变化,变更信息将 ++**不会体现在打印内容中**++,若客户不满意结果,请客户直接将 ++**基础数据进行变更**++ 后,通过页面组件选择后再进行打印。
|
||||
|
||||
|
||||
> 更新: 2024-01-11 12:11:34 原文: <https://xcz.yuque.com/ombipo/rpc7ms/nmoz9mzqf2q8micw>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# 材料标签接口参数
|
||||
|
||||
# 材料标签接口参数
|
||||
|
||||
打印平台模版分类:材料价格通用标签打印
|
||||
|
||||
材料标签支持一次打多张,与labelList集合里的元素个数相匹配:
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| labelList | 材料标签列表 | | List<Label> |
|
||||
| <br/><br/> | | | |
|
||||
|
||||
Label:
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| name | 材料名称 | | String |
|
||||
| supplierCode | 零件号 | | String |
|
||||
| customCode | 材料编码 | | String |
|
||||
| sellPrice | 销售价格 | | BigDecimal |
|
||||
| date | 打印日期 | | String yyyy/MM/dd |
|
||||
| <br/><br/> | | | |
|
||||
|
||||
# 采购库存材料标签打印
|
||||
|
||||
### 打印平台模版分类:
|
||||
|
||||
不带供应商信息:采购库存通用标签打印-新
|
||||
|
||||
带供应商信息:采购库存通用标签打印-包含供应商-新
|
||||
|
||||

|
||||
|
||||
### 打印数据体
|
||||
|
||||
材料标签支持一次打多张,于labelList集合里的元素个数相匹配:
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| labelList | 材料名称 | | List<Object> |
|
||||
|
||||
Object:
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| partName | 材料名称 | | String |
|
||||
| partBrand | 材料品牌 | | String |
|
||||
| customCode | 材料编码 | | String |
|
||||
| partShowName | 组合名称(材料品牌,名称,规格型号组合) | | String |
|
||||
| supplierCode | 材料供应商编码 | | String |
|
||||
| spec | 材料规格型号 | | String |
|
||||
| unit | 材料单位 | | String |
|
||||
| barCode | 材料编码 | | String |
|
||||
| billDate | 单据日期 | | String yyyy-MM-dd |
|
||||
| supplierName | 供应商名称 | | String |
|
||||
| storageName | 材料仓库名称 | | String |
|
||||
| defSeat | 材料货位 | | String |
|
||||
| date | 打印的当前日期 | | String yyyy/MM/dd |
|
||||
|
||||
> 更新: 2023-09-14 14:27:45 原文: <https://xcz.yuque.com/ombipo/rpc7ms/mvq9tlfxeg2k6v9y>
|
||||
+3125
File diff suppressed because it is too large
Load Diff
+100
@@ -0,0 +1,100 @@
|
||||
# 检测单接口参数
|
||||
|
||||
# 检测单接口参数
|
||||
|
||||
# 参数说明
|
||||
|
||||
## 主单信息
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| carCheckPackageName | 模板名称 | | String |
|
||||
| title | 标题 | | String |
|
||||
| billNo | 检测单号 | | String |
|
||||
| printTime | 打印时间 | | String |
|
||||
| naEmployee | 服务顾问 | | String |
|
||||
| billDate | 进厂时间 | | String |
|
||||
| deliveryTime | 预计交车 | | String |
|
||||
| naCustomer | 车主 | | String |
|
||||
| carModel | 车型 | | String |
|
||||
| cellPhone | 车主电话 | | String |
|
||||
| carNoWhole | 车牌号 | | String |
|
||||
| vin | vin码 | | String |
|
||||
| repairPerson | 送修人 | | String |
|
||||
| repairPersonContact | 送修人联系方式 | | String |
|
||||
| mileage | 进厂里程 | | String |
|
||||
| oilCapacity | 进厂油量 | | String |
|
||||
| nextMileage | 下次保养里程 | | String |
|
||||
| nextMaintainDate | 下次保养日期 | | String |
|
||||
| customerMemo | 车主描述 | | String |
|
||||
| merchantAddress | 联系地址 | | String |
|
||||
| merchantPhone | 联系方式(手机 + 固定电话) | | String |
|
||||
| qrCode | 二维码 | | String |
|
||||
| qrCodeToB | 新版检测单B端二维码 | | String |
|
||||
| qrCodeToC | 新版检测单C端二维码 | | String |
|
||||
| icon | 车辆环视图 | | String |
|
||||
| maintainBillNo | 结算单号 | | String |
|
||||
| showComputerCheckInfo | 是否展示电脑检测 | | Boolean |
|
||||
| computerCheckInfoList | 电脑检测 | | List<ComputerPrintItem> |
|
||||
| personalCheckInfoList | 包含正常和异常人工检测项 | | List<PersonalPrintItem> |
|
||||
| sortedPersonalCheckInfoList | 包含正常和异常人工检测项,问题项目排序靠前 | | List<PersonalPrintItem> |
|
||||
| optionPersonalCheckInfoList | 异常人工检测项 | | List<PersonalPrintItem> |
|
||||
| normalPersonalCheckInfoList | 正常人工检测项 | | List<PersonalPrintItem> |
|
||||
| iconMemo | 环视图备注 | | String |
|
||||
| iconResult | 环视图结论 | | String |
|
||||
| warningLightResult | 警示灯结论 | | String |
|
||||
| warningLightMemo | 警示灯备注 | | String |
|
||||
| showWarningLightItem | 是否展示警示灯 | | Boolean |
|
||||
| warningLightItemList | 警示灯 | | List<WarningLightItem> |
|
||||
| warningLightBrightItemUrls | 警示灯列表字符串 | | String |
|
||||
| employeeName | 服务技师 | | String |
|
||||
| | | | |
|
||||
|
||||
## ComputerPrintItem
|
||||
|
||||
| 字段 | 含义 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| index | 序号 | String |
|
||||
| errorCode | 故障码 | String |
|
||||
| itemName | 检测项目 | String |
|
||||
| optionNameS | 建议处理(Y) | String |
|
||||
| optionNameE | 择期处理(Y) | String |
|
||||
| optionNameU | 急需处理(Y) | String |
|
||||
| memo | 备注 | String |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
|
||||
## PersonalPrintItem(检测小类)
|
||||
|
||||
| 字段 | 含义 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| index | 序号 | BigInteger |
|
||||
| itemComponent | 检测部件(小类名称) | String |
|
||||
| memo | 备注 | String |
|
||||
| childList | 子项目列表 | List<PrintTinyItem> |
|
||||
|
||||
## PrintTinyItem(检测项目)
|
||||
|
||||
| 字段 | 含义 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| itemName | 检测项目 | String |
|
||||
| itemResults | 检测结果 | String |
|
||||
| optionNameS | 建议处理 Y | String |
|
||||
| optionNameE | 择期处理 Y | String |
|
||||
| optionNameU | 急需处理 Y | String |
|
||||
| memo | 备注 | String |
|
||||
|
||||
## ComputerPrintItem
|
||||
|
||||
| 字段 | 含义 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| warningIconCode | 警示灯 | String |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
|
||||
> 更新: 2025-02-25 13:55:59 原文: <https://xcz.yuque.com/ombipo/rpc7ms/zayna4s335straki>
|
||||
+1135
File diff suppressed because it is too large
Load Diff
+842
@@ -0,0 +1,842 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
|
||||
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="carWash" pageWidth="136" pageHeight="842" columnWidth="136" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="10" uuid="8fdb09f3-da43-46f9-a6cb-2b26a2247961">
|
||||
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
|
||||
<property name="com.jaspersoft.studio.unit." value="mm"/>
|
||||
<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
|
||||
<style name="Table_TH" mode="Opaque" backcolor="#FFFFFF">
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineColor="#000000"/>
|
||||
<topPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.5" lineColor="#000000"/>
|
||||
</box>
|
||||
</style>
|
||||
<style name="Table_CH" mode="Opaque" backcolor="#FFFFFF">
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineColor="#000000"/>
|
||||
<topPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.5" lineColor="#000000"/>
|
||||
</box>
|
||||
</style>
|
||||
<style name="Table_TD" mode="Opaque" backcolor="#FFFFFF">
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineColor="#000000"/>
|
||||
<topPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.5" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.5" lineColor="#000000"/>
|
||||
</box>
|
||||
</style>
|
||||
<subDataset name="Dataset1" uuid="22e86b94-acb8-45ed-960f-04558f91ad82">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="serviceName" class="java.lang.String"/>
|
||||
<field name="workHour" class="java.math.BigDecimal"/>
|
||||
<field name="leftCount" class="java.math.BigDecimal"/>
|
||||
<field name="unusedNumber" class="java.lang.Integer"/>
|
||||
<field name="number" class="java.lang.Integer"/>
|
||||
<field name="subtotal" class="java.math.BigDecimal"/>
|
||||
<field name="empNameStr" class="java.lang.String"/>
|
||||
</subDataset>
|
||||
<subDataset name="Dataset1sub" uuid="22e86b94-acb8-45ed-960f-04558f91ad82">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="serviceName" class="java.lang.String"/>
|
||||
<field name="workHour" class="java.math.BigDecimal"/>
|
||||
<field name="leftCount" class="java.math.BigDecimal"/>
|
||||
<field name="unusedNumber" class="java.lang.Integer"/>
|
||||
<field name="number" class="java.lang.Integer"/>
|
||||
<field name="subtotal" class="java.math.BigDecimal"/>
|
||||
</subDataset>
|
||||
<subDataset name="Dataset2" uuid="b9fe50e4-5070-473e-9238-1aa624bd7ae5">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="customCode" class="java.lang.String"/>
|
||||
<field name="partName" class="java.lang.String"/>
|
||||
<field name="unit" class="java.lang.String"/>
|
||||
<field name="number" class="java.math.BigDecimal"/>
|
||||
<field name="price" class="java.math.BigDecimal"/>
|
||||
<field name="subtotal" class="java.math.BigDecimal"/>
|
||||
<field name="partBrand" class="java.lang.String"/>
|
||||
<field name="standard" class="java.lang.String"/>
|
||||
<field name="supplierCode" class="java.lang.String"/>
|
||||
<field name="discountedSubtotal" class="java.math.BigDecimal"/>
|
||||
<field name="partMemo" class="java.lang.String"/>
|
||||
<field name="isBring" class="java.lang.String"/>
|
||||
<field name="singleFavourable" class="java.math.BigDecimal"/>
|
||||
<field name="isMember" class="java.lang.Integer"/>
|
||||
<field name="discount" class="java.math.BigDecimal"/>
|
||||
<field name="spec" class="java.lang.String"/>
|
||||
<sortField name="isMember" order="Descending"/>
|
||||
</subDataset>
|
||||
<subDataset name="DatasetPay" uuid="6f96d8c9-fa56-4586-9b6d-846e9a678fc3">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="payWay" class="java.lang.String"/>
|
||||
<field name="payAmount" class="java.math.BigDecimal"/>
|
||||
<field name="chinesePayAmount" class="java.lang.String"/>
|
||||
</subDataset>
|
||||
<subDataset name="DatasetExtra" uuid="6d2afb78-c6ff-45a0-90a2-6e65d25da398">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="extraName" class="java.lang.String"/>
|
||||
<field name="subtotal" class="java.math.BigDecimal"/>
|
||||
<field name="memo" class="java.lang.String"/>
|
||||
</subDataset>
|
||||
<subDataset name="Dataset_sub" uuid="6a79ccac-0c3d-4ada-a547-6d9f5ef3a7c9">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="serviceName" class="java.lang.String"/>
|
||||
<field name="workHour" class="java.math.BigDecimal"/>
|
||||
<field name="leftCount" class="java.math.BigDecimal"/>
|
||||
<field name="unusedNumber" class="java.lang.Integer"/>
|
||||
<field name="number" class="java.lang.Integer"/>
|
||||
<field name="subtotal" class="java.math.BigDecimal"/>
|
||||
</subDataset>
|
||||
<subDataset name="CopyOfDataset_1" uuid="fad36331-4b02-4d0a-99c9-d3608c7abffb">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="serviceName" class="java.lang.String"/>
|
||||
<field name="workHour" class="java.math.BigDecimal"/>
|
||||
<field name="leftCount" class="java.math.BigDecimal"/>
|
||||
<field name="unusedNumber" class="java.lang.Integer"/>
|
||||
<field name="number" class="java.lang.Integer"/>
|
||||
<field name="subtotal" class="java.math.BigDecimal"/>
|
||||
</subDataset>
|
||||
<parameter name="orgName" class="java.lang.String"/>
|
||||
<parameter name="detailAddress" class="java.lang.String"/>
|
||||
<parameter name="contactNumber" class="java.lang.String"/>
|
||||
<parameter name="fax" class="java.lang.String"/>
|
||||
<parameter name="bankAccount" class="java.lang.String"/>
|
||||
<parameter name="email" class="java.lang.String"/>
|
||||
<parameter name="accountNumber" class="java.lang.String"/>
|
||||
<parameter name="naCustomer" class="java.lang.String"/>
|
||||
<parameter name="contactName" class="java.lang.String"/>
|
||||
<parameter name="contactCellphone" class="java.lang.String"/>
|
||||
<parameter name="billNo" class="java.lang.String"/>
|
||||
<parameter name="billDate" class="java.lang.String"/>
|
||||
<parameter name="mileage" class="java.math.BigDecimal"/>
|
||||
<parameter name="carNoWhole" class="java.lang.String"/>
|
||||
<parameter name="carModelShort" class="java.lang.String"/>
|
||||
<parameter name="deliveryTime" class="java.lang.String"/>
|
||||
<parameter name="serviceSubtotalAll" class="java.math.BigDecimal"/>
|
||||
<parameter name="stuffSubtotalAll" class="java.math.BigDecimal"/>
|
||||
<parameter name="extraCostTotal" class="java.math.BigDecimal"/>
|
||||
<parameter name="amountAll" class="java.math.BigDecimal"/>
|
||||
<parameter name="totalWorkHour" class="java.math.BigDecimal"/>
|
||||
<parameter name="numberCount" class="java.lang.String"/>
|
||||
<parameter name="serviceList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="partList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="businessTypeName" class="java.lang.String"/>
|
||||
<parameter name="creatorName" class="java.lang.String"/>
|
||||
<parameter name="carModel" class="java.lang.String"/>
|
||||
<parameter name="vin" class="java.lang.String"/>
|
||||
<parameter name="printCount" class="java.lang.String"/>
|
||||
<parameter name="serviceNum" class="java.lang.String"/>
|
||||
<parameter name="serviceDisCountSubTotal" class="java.math.BigDecimal"/>
|
||||
<parameter name="totalStuffNum" class="java.lang.String"/>
|
||||
<parameter name="couponFavourable" class="java.math.BigDecimal"/>
|
||||
<parameter name="pointFavourable" class="java.math.BigDecimal"/>
|
||||
<parameter name="partinfoDiscountFavourable" class="java.math.BigDecimal"/>
|
||||
<parameter name="naEmployee" class="java.lang.String"/>
|
||||
<parameter name="memo" class="java.lang.String"/>
|
||||
<parameter name="repairPerson" class="java.lang.String"/>
|
||||
<parameter name="cellPhone" class="java.lang.String"/>
|
||||
<parameter name="carBrandName" class="java.lang.String"/>
|
||||
<parameter name="orgContactMobile" class="java.lang.String"/>
|
||||
<parameter name="orgDetailAddress" class="java.lang.String"/>
|
||||
<parameter name="selfPartList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="payItemList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="extraChargeList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
|
||||
<parameter name="stuffDisCountTotal" class="java.math.BigDecimal"/>
|
||||
<parameter name="selfTotalStuffNum" class="java.lang.String"/>
|
||||
<parameter name="czkExpense" class="java.math.BigDecimal"/>
|
||||
<parameter name="disCountAll" class="java.math.BigDecimal"/>
|
||||
<parameter name="packageFavourable" class="java.math.BigDecimal"/>
|
||||
<parameter name="serviceFavourable" class="java.math.BigDecimal"/>
|
||||
<parameter name="partinfoFavourable" class="java.math.BigDecimal"/>
|
||||
<parameter name="gatheringFavourable" class="java.math.BigDecimal"/>
|
||||
<parameter name="discountFavourable" class="java.math.BigDecimal"/>
|
||||
<parameter name="oweAmount" class="java.math.BigDecimal"/>
|
||||
<parameter name="amountReal" class="java.math.BigDecimal"/>
|
||||
<parameter name="printTime" class="java.lang.String"/>
|
||||
<parameter name="creationtime" class="java.lang.String"/>
|
||||
<parameter name="extraNumber" class="java.lang.String"/>
|
||||
<parameter name="carSeriesName" class="java.lang.String"/>
|
||||
<parameter name="qRCodeStr" class="java.lang.String"/>
|
||||
<parameter name="vipExpense" class="java.math.BigDecimal"/>
|
||||
<parameter name="storeLogo" class="java.lang.String"/>
|
||||
<parameter name="partFavourableTotal" class="java.math.BigDecimal"/>
|
||||
<parameter name="serviceFavourableTotal" class="java.math.BigDecimal"/>
|
||||
<parameter name="partFavourableCommonTotal" class="java.math.BigDecimal"/>
|
||||
<parameter name="serviceFavourableCommonTotal" class="java.math.BigDecimal"/>
|
||||
<parameter name="serviceSubtotalVip" class="java.math.BigDecimal"/>
|
||||
<parameter name="stuffSubtotalVip" class="java.math.BigDecimal"/>
|
||||
<parameter name="nextMileage" class="java.math.BigDecimal"/>
|
||||
<parameter name="printContent" class="java.lang.String"/>
|
||||
<parameter name="repairPersonContact" class="java.lang.String"/>
|
||||
<parameter name="payItemTogether" class="java.lang.String"/>
|
||||
<parameter name="payItemTogetherChinese" class="java.lang.String"/>
|
||||
<parameter name="printOrgName" class="java.lang.String"/>
|
||||
<parameter name="orgContactNumber" class="java.lang.String"/>
|
||||
<parameter name="balanceStatus" class="java.lang.String"/>
|
||||
<parameter name="receiptAmount" class="java.math.BigDecimal"/>
|
||||
<parameter name="stubPrintFlag" class="java.lang.String"/>
|
||||
<parameter name="stubServiceList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource">
|
||||
<parameterDescription><![CDATA[]]></parameterDescription>
|
||||
</parameter>
|
||||
<parameter name="czkDetailInfo" class="java.lang.String"/>
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<variable name="tempServiceList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource">
|
||||
<variableExpression><![CDATA[$P{serviceList}]]></variableExpression>
|
||||
</variable>
|
||||
<variable name="tempServiceList2" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource">
|
||||
<variableExpression><![CDATA[$P{serviceList}]]></variableExpression>
|
||||
</variable>
|
||||
<detail>
|
||||
<band height="43">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="1" width="136" height="40" uuid="749214fe-d497-4af5-809c-60db5b97deee">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="11"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[($P{printOrgName}==null?$P{orgName}:($P{printOrgName}.isEmpty()?$P{orgName}:$P{printOrgName}))+"洗车单"]]></textFieldExpression>
|
||||
</textField>
|
||||
<line>
|
||||
<reportElement positionType="Float" x="0" y="42" width="136" height="1" uuid="9902b948-f3bc-4318-9162-169d3a18d40e">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="11">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="40" height="11" uuid="a9ea9a61-8b35-4714-abd6-c1b3f01daf4c">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[打印时间:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="32" y="0" width="104" height="11" uuid="3e42cdc1-f715-455d-965b-a3862546efe3">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{printTime}.substring(0,16)]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="11">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="40" height="11" uuid="d8179c10-e152-47cf-98ed-6d25100423b1">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[洗车单号:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="32" y="0" width="104" height="11" uuid="73392fa1-f678-4f97-b702-97d591431a76">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{billNo}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="11">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="32" y="0" width="104" height="11" uuid="4b8e8259-abcb-4b55-8932-645ab3c168db">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{naEmployee}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement key="" x="0" y="0" width="40" height="11" uuid="aa005a7f-11f1-4362-94dd-7d4b737f85fb">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[服务顾问:]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
<band height="11">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="24" y="0" width="112" height="11" uuid="4c9e2e05-12f0-4c10-a4a7-cfac0dedc4c7">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{carNoWhole}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement key="" x="0" y="0" width="40" height="11" uuid="506d2cfc-d948-4a43-971c-e2ae1f4f4a6e">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[车牌号:]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
<band height="11">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="18" y="0" width="118" height="11" uuid="7c4f4788-dd00-484c-8762-c907f94aba06">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{naCustomer}.substring(0, 1) + "(*^_^*)" + $P{naCustomer}.substring($P{naCustomer}.length() > 10 ? 10 : $P{naCustomer}.length(), $P{naCustomer}.length())]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="29" height="11" uuid="1e0990e3-d082-4c2a-b861-ea88ae864819">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[客户:]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
<band height="12">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="32" y="0" width="104" height="11" uuid="aa23dce0-7f6f-4324-8368-9040c5460fce">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{cellPhone}.length() <= 3 ? $P{cellPhone}: $P{cellPhone}.substring(0, 3) + "(*^_^*)" + $P{cellPhone}.substring($P{cellPhone}.length() > 7 ? 7 : $P{cellPhone}.length(), $P{cellPhone}.length())]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="40" height="11" uuid="17c41cd7-40bc-417a-b81f-31cb88c34931">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[客户电话:]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement positionType="Float" x="0" y="11" width="136" height="1" uuid="83b4e0cf-0321-463b-93ab-cf578aecc422">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="17">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{serviceList}!=null]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement positionType="Float" x="0" y="0" width="46" height="16" uuid="3b60f428-f1be-46b4-8844-000b66d37cb3">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7" isBold="false"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[项目]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement positionType="Float" x="46" y="0" width="38" height="16" uuid="3dce837c-7930-438f-88cf-d0ca69283abf">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[工时费(元)]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement positionType="Float" x="84" y="0" width="52" height="16" uuid="4f6f1957-c5d3-45d4-9f9e-59f03d002151">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[卡剩余次数]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement positionType="Float" x="0" y="16" width="136" height="1" uuid="45924492-61d2-4ec2-9dae-9edf6181e7a4">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Dashed"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="11">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{serviceList}!=null]]></printWhenExpression>
|
||||
<componentElement>
|
||||
<reportElement isPrintRepeatedValues="false" x="2" y="0" width="136" height="11" uuid="ecd9cd2e-ad62-4a61-a8a7-6f894e8b881b">
|
||||
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
|
||||
<property name="com.jaspersoft.studio.table.style.table_header" value="Table 1_TH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.column_header" value="Table 1_CH"/>
|
||||
<property name="com.jaspersoft.studio.table.style.detail" value="Table 1_TD"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
|
||||
<datasetRun subDataset="Dataset1" uuid="12ce7a05-4dbc-42aa-ab97-ca225ebd48e1">
|
||||
<dataSourceExpression><![CDATA[$P{serviceList}]]></dataSourceExpression>
|
||||
</datasetRun>
|
||||
<jr:column width="46" uuid="4a339d5c-d93e-4d9b-a7f5-478cb3bc4c66">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<jr:detailCell height="11">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement key="" x="0" y="0" width="46" height="11" uuid="d1b9c27f-da5c-43d5-b245-b2ce10285445">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{serviceName}.indexOf("卡消费") > 0 ? $F{serviceName}.replaceAll("卡消费",$F{empNameStr}): $F{serviceName}+"("+$F{empNameStr}+")"]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="38" uuid="36d33a43-8dc0-421c-89b2-6c17b4c992c2">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<jr:detailCell height="11">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement key="" x="0" y="0" width="38" height="11" uuid="b0a21e7b-9ef2-453a-8b0b-f81e4a8d02ae">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{subtotal}.setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
<jr:column width="52" uuid="922b68cd-f23e-4d67-9717-74621fa31ed2">
|
||||
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<jr:detailCell height="11">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="px"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="px"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement key="" x="0" y="0" width="52" height="11" uuid="683ff861-1457-4841-8904-a5b8030d6a63">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[($F{unusedNumber}==null?"":$F{unusedNumber}+"/")+($F{number}==null?"":$F{number})]]></textFieldExpression>
|
||||
</textField>
|
||||
</jr:detailCell>
|
||||
</jr:column>
|
||||
</jr:table>
|
||||
</componentElement>
|
||||
</band>
|
||||
<band height="20">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="0" y="8" width="40" height="11" uuid="26dc81d8-29cc-4024-b7b4-d25a9ed72773">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[合计金额:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="37" y="8" width="99" height="11" uuid="2872a1d3-dafc-430d-9cfd-fef01cdcc46a">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{amountAll}.subtract($P{vipExpense}).setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="33" y="9" width="7" height="11" uuid="f8d751a0-c2f4-45c9-bd4d-168f7250ab40">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="微软雅黑" size="7"/>
|
||||
</textElement>
|
||||
<text><![CDATA[¥]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement positionType="Float" x="0" y="5" width="136" height="1" uuid="aa43bd00-5440-4c07-a3d5-bbbf892646bb">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="11">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ).compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="37" y="-1" width="99" height="11" uuid="d4b00569-8830-4a81-98b9-b02516e9e433">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="33" y="0" width="7" height="11" uuid="7d3b3e0c-8646-455a-ac17-10c3e46db01f">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="微软雅黑" size="7"/>
|
||||
</textElement>
|
||||
<text><![CDATA[¥]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="-1" width="40" height="11" uuid="0eb1e983-dbb7-4e21-b8ee-db6f492cdfab">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[优惠金额:]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
<band height="22">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="37" y="10" width="99" height="11" uuid="00e5ff0d-525e-4d27-ab0d-5a988300256b">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{balanceStatus}.equals("7000")?($P{amountAll}.subtract($P{vipExpense}).subtract($P{czkExpense}).subtract($P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable})).setScale( 2, BigDecimal.ROUND_HALF_EVEN )):($P{oweAmount}.setScale( 2, BigDecimal.ROUND_HALF_EVEN ))]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="10" width="40" height="11" uuid="e5619fe5-5014-44c7-be70-428e7fa89a99">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[待付金额:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="-1" width="40" height="11" uuid="b3f329b3-6432-412c-ac54-3c3f1fdbe034">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[实付金额:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="37" y="-1" width="99" height="11" uuid="8bbae461-eac5-4e83-9f39-4a27a55bed68">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{balanceStatus}.equals("7000")?"0.00":($P{amountAll}.subtract($P{vipExpense}).subtract($P{oweAmount}).subtract($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).subtract($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).subtract($P{czkExpense}).
|
||||
subtract($P{serviceFavourable}).subtract($P{partinfoFavourable}).subtract($P{discountFavourable}).subtract($P{couponFavourable}).subtract($P{pointFavourable}).subtract($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ))]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="33" y="0" width="7" height="11" uuid="d257edd4-940d-4804-a256-5e23b7fc781e">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="微软雅黑" size="7"/>
|
||||
</textElement>
|
||||
<text><![CDATA[¥]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="33" y="11" width="7" height="11" uuid="162688e4-1a42-4f5c-b406-e5c7c780a696">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="微软雅黑" size="7"/>
|
||||
</textElement>
|
||||
<text><![CDATA[¥]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
<band height="11">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
|
||||
<staticText>
|
||||
<reportElement x="40" y="-1" width="7" height="11" uuid="389acfa8-bc97-47ca-b7de-61e45f6392f8">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="微软雅黑" size="7"/>
|
||||
</textElement>
|
||||
<text><![CDATA[¥]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="50" height="11" uuid="ef352217-3888-4dee-9291-2e1814474afa">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Top">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[储值卡消费:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement positionType="Float" x="43" y="0" width="37" height="11" uuid="9ed79fa8-64a1-401d-b20e-2bd537aa60ce">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Top">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{czkExpense}.setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement positionType="Float" x="80" y="0" width="56" height="11" uuid="9ed79fa8-64a1-401d-b20e-2bd537aa60ce">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Top">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA["("+($P{czkDetailInfo}==null?"":$P{czkDetailInfo}) + ")"]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="15">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="40" height="11" uuid="107cd24a-9c65-454a-857b-d4343360c17c">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[收银金额:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="33" y="1" width="7" height="11" uuid="d3e37020-1703-4943-88cc-f6a27b30051d">
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="微软雅黑" size="7"/>
|
||||
</textElement>
|
||||
<text><![CDATA[¥]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement positionType="Float" x="37" y="2" width="99" height="11" uuid="8cac6572-8613-4439-ac11-8a09c05356a6">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{balanceStatus}.equals("7000")?"0.00":($P{amountAll}.subtract($P{vipExpense}).subtract($P{oweAmount}).subtract($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).subtract($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).subtract($P{czkExpense}).
|
||||
subtract($P{serviceFavourable}).subtract($P{partinfoFavourable}).subtract($P{discountFavourable}).subtract($P{couponFavourable}).subtract($P{pointFavourable}).subtract($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ).toString()+"("+$P{payItemTogether}+")")]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="26">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="0" y="2" width="29" height="11" uuid="fc491228-a3a9-47ab-b354-af9f4c4e0f3e">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Top">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<text><![CDATA[备注:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement positionType="Float" x="20" y="2" width="116" height="11" uuid="b7b78327-0448-424d-970e-bada89ec5d60">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Top">
|
||||
<font fontName="黑体" size="7"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{memo}]]></textFieldExpression>
|
||||
</textField>
|
||||
<line>
|
||||
<reportElement x="1" y="0" width="136" height="1" uuid="1d0bcb53-619a-484c-a904-4e88f609d733">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
</detail>
|
||||
</jasperReport>
|
||||
+433
@@ -0,0 +1,433 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
|
||||
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="3fa22123-efc4-4f3f-a186-6a8f692d17e6">
|
||||
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
|
||||
<subDataset name="List1" uuid="7366c5be-288c-41c7-b295-b8d023ec81ae">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="index" class="java.lang.String"/>
|
||||
<field name="serviceName" class="java.lang.String"/>
|
||||
<field name="cooperationServiceName" class="java.lang.String"/>
|
||||
<field name="cooperationOrgName" class="java.lang.String"/>
|
||||
<field name="auditStatus" class="java.lang.String"/>
|
||||
<field name="cooperationCost" class="java.math.BigDecimal"/>
|
||||
</subDataset>
|
||||
<parameter name="billNo" class="java.lang.String"/>
|
||||
<parameter name="tradingOrgName" class="java.lang.String"/>
|
||||
<parameter name="tradingDate" class="java.lang.String"/>
|
||||
<parameter name="customerName" class="java.lang.String"/>
|
||||
<parameter name="carNo" class="java.lang.String"/>
|
||||
<parameter name="employeeName" class="java.lang.String"/>
|
||||
<parameter name="cardName" class="java.lang.String"/>
|
||||
<parameter name="memberCardNo" class="java.lang.String"/>
|
||||
<parameter name="cardEndDate" class="java.lang.String"/>
|
||||
<parameter name="tradingAmount" class="java.lang.String"/>
|
||||
<parameter name="amount" class="java.lang.String"/>
|
||||
<parameter name="remark" class="java.lang.String"/>
|
||||
<parameter name="orgOfPrinting" class="java.lang.String"/>
|
||||
<parameter name="dateOfPrinting" class="java.lang.String"/>
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<detail>
|
||||
<band height="103" splitType="Stretch">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="10" width="555" height="40" uuid="7c3a0deb-dcb0-406e-ba9c-9f279e1518b0">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="16" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA["卡交易单"]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement key="" x="387" y="58" width="50" height="18" isRemoveLineWhenBlank="true" uuid="86735eee-0435-4238-9b93-c08d86ed318f">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[交易日期:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="58" width="50" height="18" uuid="fef0343d-31f4-4b1c-a521-1a1b736aa485">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[单号:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="200" y="58" width="50" height="18" uuid="627d90fe-c235-45d8-9f4a-92f8d7ec0de7">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[交易门店:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="387" y="80" width="50" height="18" uuid="7af4c3ab-ae72-4f6b-8b40-7b3129bd7eff">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[服务顾问:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="80" width="50" height="18" uuid="74602da5-9195-47dd-9416-9cb229aeccad">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[客户姓名:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="200" y="80" width="50" height="18" uuid="65483940-e0c7-4713-9e9a-6a5a7294d248">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[用卡车辆:]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="78" width="555" height="1" uuid="f6716829-054d-4fed-a12c-2f4d227724c7">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Dotted"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<line>
|
||||
<reportElement x="0" y="100" width="555" height="1" uuid="9c9a29fc-1a65-4869-8d03-3242ff1fe667">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="80" width="140" height="18" uuid="01160695-dd7d-4038-8476-f20a7f997e62"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{customerName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="58" width="140" height="18" uuid="d8bc8e14-2a41-4471-8be9-dae7dee48c2b"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{billNo}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="250" y="58" width="130" height="18" uuid="f258bb2c-70ce-4b49-a305-a4c0db4671b2"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{tradingOrgName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="250" y="80" width="130" height="18" uuid="8ffee383-6250-4697-8381-b3017b0d67ad"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{carNo}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="437" y="58" width="118" height="18" uuid="7daab9f8-bc40-45fc-90b2-510a19fe0578"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{tradingDate}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="437" y="80" width="118" height="18" uuid="5cd6ce11-c901-42cb-b84c-fc7b3a0de3b3"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{employeeName}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="78">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="0" y="51" width="50" height="18" uuid="2cf35160-a982-4dcc-881b-c53adcf3761a">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[剩余金额:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="2" width="50" height="18" uuid="6b4cf2c5-cb81-43b6-82c9-ee238923a378">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="false"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[卡名称:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="25" width="50" height="18" uuid="782a08a7-c605-4a2f-83b7-4e3ad8ee3c4c">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[消费金额:]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="47" width="555" height="1" uuid="8699bef5-cebd-4ebc-bb34-3697568375f7">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Dotted"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="51" width="140" height="18" uuid="4ff3b629-b9e5-49c5-ad89-ce856a556839">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{amount}]]></textFieldExpression>
|
||||
</textField>
|
||||
<line>
|
||||
<reportElement x="0" y="71" width="555" height="1" uuid="98b923f9-1e14-4cc5-b508-12141cefb96e">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="25" width="140" height="18" uuid="7ce3fef6-73c6-49e9-a676-64e402603d93"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{tradingAmount}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="200" y="2" width="50" height="18" uuid="04029be9-7783-4583-9f54-0b15047b2e76">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[卡号:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="388" y="2" width="50" height="18" uuid="471ece50-ac4b-4d63-ba15-e82448944610">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[卡有效期:]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="22" width="555" height="1" uuid="7672b0ab-ea26-468c-a2f5-7188f85f733c">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="2" width="140" height="18" uuid="88cb0ab6-fc29-4d51-b688-0ab2bdb0b2f9"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{cardName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="250" y="2" width="130" height="18" uuid="90dfc3ce-5cf5-42d0-8e6f-670b49922088"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{memberCardNo}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="438" y="2" width="117" height="18" uuid="48056694-c19b-4dc3-a0d7-ef3e32ea4cb4"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{cardEndDate}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="92">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="0" y="17" width="50" height="18" uuid="fc27cb3d-0e5d-4143-b83d-daf432fa56c8">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="false"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[打印门店:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="17" width="140" height="18" uuid="1a73fa76-4113-4dcd-803c-da26e934f441"/>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{orgOfPrinting}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="200" y="17" width="50" height="18" uuid="786b4cf3-7636-455c-bf05-115ff8ca71b7">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Right" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="false"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[打印日期:]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="250" y="17" width="130" height="18" uuid="61b50ddd-d880-403c-855f-3ae2f0403592"/>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{dateOfPrinting}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="-4" width="504" height="18" uuid="d9928920-600f-4030-976c-bdf598467704"/>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{remark}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="-4" width="50" height="18" uuid="03491a27-e480-40c3-aa80-d95b2bed3d95">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="false"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[交易备注:]]></text>
|
||||
</staticText>
|
||||
</band>
|
||||
</detail>
|
||||
</jasperReport>
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
# 结算单(指定内容)接口文档
|
||||
|
||||
# 结算单(指定内容)接口文档
|
||||
|
||||
# 接口
|
||||
|
||||
**接口: /blazer/maintenance/pr****int/file/dispatchCondition**
|
||||
|
||||
**方法: post**
|
||||
|
||||
**支持场景:打印 维保单/洗车单/维修单/贴膜单/理赔单/零售单,支持指定(项目/材料/附加费)打印**
|
||||
|
||||
**支持打印模块(**PrintModuleEnum**):****工时费、材料费、附加费、其他费用、服务费用**
|
||||
|
||||
**入参:**
|
||||
|
||||
```plaintext
|
||||
{
|
||||
"rowId": "10329",
|
||||
"rowCode": "partialSettlementStatement",
|
||||
"pkId": "16126085167868551253",
|
||||
"serviceList": [
|
||||
{
|
||||
"pkId": 16126085168250232896,
|
||||
"module": 1
|
||||
},
|
||||
{
|
||||
"pkId": 16155904656688554043,
|
||||
"module": 4
|
||||
}
|
||||
],
|
||||
"partList": [
|
||||
{
|
||||
"pkId": 11004336,
|
||||
"module": 2
|
||||
},
|
||||
{
|
||||
"pkId": 11004811,
|
||||
"module": 5
|
||||
}
|
||||
],
|
||||
"extraList": [
|
||||
{
|
||||
"type": 4,
|
||||
"module": 3
|
||||
},
|
||||
{
|
||||
"type": 6,
|
||||
"module": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**出参:**
|
||||
|
||||
```plaintext
|
||||
{
|
||||
"preScanUrl": "https://f.f6yc.com/printserver/pdfprint.html?url=https://f.f6yc.com/print-server/test/2024-10/default/49be06dc15444e2793a21fc8c16d3b5c.pdf?Expires=1729152323&OSSAccessKeyId=LTAI4Fcf2C1U99o3e3UQ2bHV&Signature=L86HWRF2ilyXU4BbzKhwXad%2BwXg%3D",
|
||||
"url": "https://f.f6yc.com/print-server/test/2024-10/default/49be06dc15444e2793a21fc8c16d3b5c.pdf?Expires=1729152323&OSSAccessKeyId=LTAI4Fcf2C1U99o3e3UQ2bHV&Signature=L86HWRF2ilyXU4BbzKhwXad%2BwXg%3D"
|
||||
}
|
||||
```
|
||||
|
||||
# jasper取参说明
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| billNo | 工单号 | 是 | String |
|
||||
| maintainType | 工单类型 | 是 | String |
|
||||
| businessTypeName | 业务类别 | 是 | String |
|
||||
| balanceStatus | 结算状态 | 是 | String |
|
||||
| billStatus | 单据状态 | 是 | String |
|
||||
| creatorName | 创建人名称 | 是 | String |
|
||||
| naEmployee | 服务顾问 | 否 | String |
|
||||
| employeePhone | 服务顾问手机号 | 否 | String |
|
||||
| naInsurer | 理赔公司名称 | 否 | String |
|
||||
| insurancepolicyNo | 理赔保险单号 | 否 | Double |
|
||||
| serviceSubtotalVip | 套餐卡项目工时费小计 | 是 | Double |
|
||||
| serviceFavourableCommonTotal | 普通项目优惠金额合计 | 是 | Double |
|
||||
| totalStuffNum | 材料数量合计 | 是 | String |
|
||||
| stuffSubtotalVip | 套餐卡项目材料费小计 | 是 | Double |
|
||||
| stuffSubtotalAll | 材料费小计 | 是 | Double |
|
||||
| partFavourableCommonTotal | 普通材料优惠金额合计 | 是 | Double |
|
||||
| extraNumber | 附加费数量小计 | 是 | String |
|
||||
| extraCostTotal | 附加费小计 | 是 | Double |
|
||||
| favourableTotalList | 优惠明细小计 | 否 | List<FavourableDetailPrintAttribute> |
|
||||
| naCustomer | 客户姓名 | 是 | String |
|
||||
| cellPhone | 客户联系电话 | 是 | String |
|
||||
| repairPerson | 送修人 | 否 | String |
|
||||
| repairPersonContact | 送修人联系方式 | 否 | String |
|
||||
| carNoWhole <br/> | 车牌号 | 是 | String |
|
||||
| vin<br/> | 车辆VIN码 | 否 | String |
|
||||
| carBrandName<br/> | 品牌名称 | 否 | String |
|
||||
| carSeriesName<br/> | 车系名称 | 否 | String |
|
||||
| carModelShort<br/> | 车型简称 | 否 | String |
|
||||
| carModel<br/> | 车型 | 否 | String |
|
||||
| transmissionNo | 变速箱号 | 否 | String |
|
||||
| carFuelTypeName | 燃料类型 | 否 | String |
|
||||
| carColor<br/> | 车身颜色 | 否 | String |
|
||||
| billDate<br/> | 进厂日期 | 是 | String |
|
||||
| estimatedDeliveryTime<br/> | 预计交车时间 | 是 | String |
|
||||
| deliveryTime<br/> | 交车时间(出厂时间) | 是 | String |
|
||||
| mileage<br/> | 进厂里程 | 是 | Double |
|
||||
| nextMaintainDateRemind | 下次服务时间(服务提醒数据源) | 否 | Date |
|
||||
| nextMileageRemind | 下次服务里程(服务提醒数据源) | 否 | Double |
|
||||
| serviceList | 项目列表 | 否 | List<PartialServicePrintAttribute> |
|
||||
| partList | 材料列表 | 否 | List<PartialPartPrintAttribute> |
|
||||
| extraChargeList | 附加费列表 | 否 | List<ExtraChargePrintAttribute> |
|
||||
| extendedModuleList | 扩展模块列表 | 否 | List<ExtendedModulePrintAttribute> |
|
||||
| memo<br/> | 车主备注 | 否 | String |
|
||||
| signaturePhotoUrl<br/> | 签名图片 | 否 | String |
|
||||
| orgName<br/> | 维修厂名称 | 是 | String |
|
||||
| orgContacts<br/> | 维修厂联系人 | 是 | String |
|
||||
| orgDetailAddress<br/> | 维修厂地址 | 是 | String |
|
||||
| orgContactMobile<br/> | 联系电话(维修厂) | 否 | String |
|
||||
| storeLogo<br/> | logo | 否 | String |
|
||||
| printOrgName<br/> | 打印抬头 | 否 | String |
|
||||
| printContent<br/> | 免责条款 | 是 | String |
|
||||
| printCount<br/> | 打印次数 | 是 | String |
|
||||
| printTime<br/> | 打印时间 | 是 | String |
|
||||
|
||||
**PartialServicePrintAttribute**
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| serviceName | 项目名称 | 是 | String |
|
||||
| labelName | 业务分类名称 | 是 | String |
|
||||
| isMember | 当前项目是否使用会员 | 是 | String |
|
||||
| idService | 服务项目id | 是 | BigInteger |
|
||||
| idInfo | 本地项目id | 是 | String |
|
||||
| price | 工时单价 | 是 | Double |
|
||||
| workHour | 工时 | 是 | Double |
|
||||
| subtotal | 金额 | 是 | Double |
|
||||
| singleFavourable | 优惠金额 | 是 | Double |
|
||||
| discountedSubtotal | 折后金额 | 是 | Double |
|
||||
| serviceMemo | 附加信息备注 | 否 | String |
|
||||
| discount | 折扣 | 是 | Double |
|
||||
| empNameStr | 服务项目明细对应修理工名称组装字符串 | 否 | String |
|
||||
| favourableVoList | 优惠明细 | 否 | List<FavourableDetailPrintAttribute> |
|
||||
|
||||
**PartialPartPrintAttribute**
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| partName | 材料名称 | 是 | String |
|
||||
| partShowName | 材料名称(全) | 是 | String |
|
||||
| partBrand | 配件品牌 | 是 | String |
|
||||
| unit | 单位 | 是 | String |
|
||||
| number | 数量 | 是 | Double |
|
||||
| isMember | 当前项目是否使用会员 | 是 | Integer |
|
||||
| price | 价格(单价) | 是 | Double |
|
||||
| subtotal | 金额(材料金额) | 是 | Double |
|
||||
| discount | 折扣 | 是 | Double |
|
||||
| singleFavourable | 优惠金额 | 是 | Double |
|
||||
| discountedSubtotal<br/> | 折后金额 | 是 | Double |
|
||||
| partMemo<br/> | 备注 | 否 | String |
|
||||
| isBring<br/> | 是否自带 | 是 | String |
|
||||
| empNameStr<br/> | 明细对应修理工名称组装字符串 | 否 | String |
|
||||
| outStockEmployeeName<br/> | 领料人 | 否 | String |
|
||||
| labelName<br/> | 业务分类名称 | 是 | String |
|
||||
| idPart<br/> | 配件材料pk | 是 | BigInteger |
|
||||
| idInfo<br/> | 本地材料id | 是 | String |
|
||||
| favourableVoList | 优惠明细 | 否 | List<FavourableDetailPrintAttribute> |
|
||||
|
||||
**ExtraChargePrintAttribute**
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| extraName | 附加费名称 | 是 | String |
|
||||
| subtotal | 金额 | 是 | Double |
|
||||
| memo | 备注 | 否 | String |
|
||||
|
||||
|
||||
**FavourableDetailPrintAttribute**
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| discountType | 优惠类型 | 是 | Integer |
|
||||
| discountTypeName | 优惠类型名称 | 是 | Double |
|
||||
| amount | 优惠金额 | 是 | Double |
|
||||
|
||||
|
||||
|
||||
**ExtendedModulePrintAttribute**
|
||||
|
||||
| 字段 | 含义 | 是否必有 | 类型 |
|
||||
| --- | --- | --- | --- |
|
||||
| module | 打印模块,PrintModuleEnum.code | 是 | Integer |
|
||||
| name | 项目/材料/附加费名称 | 是 | String |
|
||||
| number | 项目对应工时,材料对应数量,附加费为“-” | 是 | Double |
|
||||
| price | 项目对应工时单价,材料对应材料单价,,附加费为“-” | 是 | Double |
|
||||
| subtotal | 项目对应工时费,材料对应材料费,附加费对应为金额 | 是 | Double |
|
||||
| discount | 项目对应折扣,材料对应折扣,附加费为“1.00” | 是 | Double |
|
||||
| discountedSubtotal | 项目对应折后金额,材料对应材料费折后金额,,附加费对应为金额 | 是 | Double |
|
||||
| favourableVoList | 优惠明细 | 否 | List<FavourableDetailPrintAttribute> |
|
||||
|
||||
**PrintModuleEnum 打印模块枚举**
|
||||
|
||||
| **key** | **code** | **name** |
|
||||
| --- | --- | --- |
|
||||
| MAN\_HOUR\_COST | 1 | 工时费模块 |
|
||||
| MATERIAL\_COST | 2 | 材料费模块 |
|
||||
| EXTRA\_COST | 3 | 附加费模块 |
|
||||
| OTHER\_COST | 4 | 其他费用模块 |
|
||||
| SERVICE\_COST | 5 | 服务费用模块 |
|
||||
|
||||
# 工具类jar包附件下载
|
||||
|
||||
[附件: print-core-1.0.7.jar](./attachments/JUhN4OhWjosFCdDg/print-core-1.0.7.jar)
|
||||
|
||||
### 数字金额转中文方法调用示例:
|
||||
|
||||
**数字金额**:$P{amount}==null?BigDecimal.ZERO:$P{amount} **转中文**:com.f6car.printserver.core.CharacterUtil.chinese($P{amount}==null?BigDecimal.ZERO:$P{amount})
|
||||
|
||||
> 更新: 2024-10-17 15:07:46 原文: <https://xcz.yuque.com/ombipo/rpc7ms/qz7dm6e8b06r7t70>
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# 调拨单打印
|
||||
|
||||
# 采购单模版打印说明
|
||||
|
||||
* 前端调用接口
|
||||
|
||||
|
||||
/stock/allot/print?idAllot=
|
||||
|
||||
* 打印使用模版
|
||||
|
||||
|
||||
模版编码=allotInPrint
|
||||
|
||||

|
||||
|
||||
* 打印模版参数
|
||||
|
||||
|
||||
HashMap<String, Object> resultMap
|
||||
|
||||
| 字段 | 说明 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| printFlag | 均价门店 false<br>批次门店 true | |
|
||||
| showPrice | 参配-打印调拨价格及金额<br>勾选 true<br>未勾选 false | |
|
||||
| orgName | 打印抬头<br>调入门店打印:XXX调入单<br>调出门店打印:XXX调出单 | |
|
||||
| billNo | 调拨单号<br>调入门店打印:DRDXXX<br>调出门店打印:DCDXXX | |
|
||||
| idOwnOrg | 门店ID<br>调入门店打印:调入门店ID<br>调出门店打印:调出门店ID | |
|
||||
| naOrgIn | 调入门店名称 | |
|
||||
| naOrgOut | 调出门店名称 | |
|
||||
| detailAddress | 供应商详细地址 | |
|
||||
| statusName | 调拨单状态(制单/待发货/待收货/已完成) | |
|
||||
| billDate | 单据日期(yyyy-MM-dd) | |
|
||||
| creatorName | 创建单名称 | |
|
||||
| nowDateTime | 打印时间(yyyy-MM-dd HH:mm) | |
|
||||
| remark | 备注信息 | |
|
||||
| showRemark | 是否显示备注<br>备注为空 false<br>备注不为空 true | |
|
||||
| sumNumber | 总数量 | |
|
||||
| sumAmount | 总金额<br>价格脱敏时显示 \*\*\*\* | |
|
||||
| printCount | 打印次数 | |
|
||||
| allotDetailVoList | 调拨单明细行 | |
|
||||
| sortNumber | 序号 | |
|
||||
| partShowName | 材料组合名称 | |
|
||||
| customCode | 材料自定义编码 | |
|
||||
| partName | 材料名称 | 20250731追加 |
|
||||
| partBrand | 材料品牌 | 20250731追加 |
|
||||
| supplierCode | 材料零件号 | 20250731追加 |
|
||||
| standard | 材料规格型号 | 20250731追加 |
|
||||
| carNo | 车牌 | 20250731追加 |
|
||||
| defSeat | 货位<br>调入门店打印:调入门店货位<br>调出门店打印:调出门店货位 | |
|
||||
| unit | 单位 | |
|
||||
| price | 单价<br>价格脱敏时显示 \*\*\*\* | |
|
||||
| numCus | 均价门店<br> 调拨个数<br>批次门店&制单&未选择批次<br> 调拨个数<br>批次门店<br> 选择或使用的批次个数 | |
|
||||
| amount | 均价门店<br> 调拨明细行金额<br>批次门店&制单&未选择批次<br> 调拨明细行金额<br>批次门店<br> 选择或使用的批次个数\*调拨单单价<br>备注:价格脱敏时显示 \*\*\*\* | |
|
||||
| orderNo | 批次号 | |
|
||||
| productDate | 批次生成日期 | |
|
||||
| 明细合计行 | | |
|
||||
| sortNumber | 合计行 | |
|
||||
| partShowName | "" | |
|
||||
| unit | "" | |
|
||||
| numCus | 总数量 | |
|
||||
| amount | 总金额<br>价格脱敏时显示 \*\*\*\* | |
|
||||
| defSeat | "" | |
|
||||
| stockNumber | "" | |
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
|
||||
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="3fa22123-efc4-4f3f-a186-6a8f692d17e6">
|
||||
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
|
||||
<subDataset name="List1" uuid="7366c5be-288c-41c7-b295-b8d023ec81ae">
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<field name="index" class="java.lang.String"/>
|
||||
<field name="serviceName" class="java.lang.String"/>
|
||||
<field name="cooperationServiceName" class="java.lang.String"/>
|
||||
<field name="cooperationOrgName" class="java.lang.String"/>
|
||||
<field name="auditStatus" class="java.lang.String"/>
|
||||
<field name="cooperationCost" class="java.math.BigDecimal"/>
|
||||
</subDataset>
|
||||
<parameter name="tradingStoreName" class="java.lang.String"/>
|
||||
<parameter name="tradingTime" class="java.lang.String"/>
|
||||
<parameter name="idSource" class="java.lang.String"/>
|
||||
<parameter name="cardName" class="java.lang.String"/>
|
||||
<parameter name="memberNo" class="java.lang.String"/>
|
||||
<parameter name="noCar" class="java.lang.String"/>
|
||||
<parameter name="customerName" class="java.lang.String"/>
|
||||
<parameter name="paymentTypeAndAmount" class="java.lang.String"/>
|
||||
<parameter name="orgOfPrinting" class="java.lang.String"/>
|
||||
<parameter name="dateOfPrinting" class="java.lang.String"/>
|
||||
<parameter name="amount" class="java.lang.String">
|
||||
<defaultValueExpression><![CDATA[$P{amount}]]></defaultValueExpression>
|
||||
</parameter>
|
||||
<queryString>
|
||||
<![CDATA[]]>
|
||||
</queryString>
|
||||
<detail>
|
||||
<band height="97" splitType="Stretch">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="0" y="10" width="555" height="40" uuid="7c3a0deb-dcb0-406e-ba9c-9f279e1518b0">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Center" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="16" isBold="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA["会员卡交易单"]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="210" y="60" width="178" height="18" uuid="6849ccdb-a3a0-4d32-a556-7a8fd5a19cca">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{tradingTime}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="437" y="60" width="118" height="18" uuid="b347e5b3-390c-44c3-b746-481aa6dc808e">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{idSource}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="60" width="90" height="18" uuid="92646184-6d70-4b6e-9f0e-23b4a7b15fb5">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{tradingStoreName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement key="" x="389" y="60" width="48" height="18" isRemoveLineWhenBlank="true" uuid="86735eee-0435-4238-9b93-c08d86ed318f">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[交易单号]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="60" width="50" height="18" uuid="fef0343d-31f4-4b1c-a521-1a1b736aa485">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[交易门店]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="140" y="60" width="70" height="18" uuid="627d90fe-c235-45d8-9f4a-92f8d7ec0de7">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[交易日期]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="60" width="555" height="1" uuid="329fa736-2c18-4d9d-8fab-f54846315633">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<staticText>
|
||||
<reportElement x="389" y="78" width="48" height="18" uuid="7af4c3ab-ae72-4f6b-8b40-7b3129bd7eff">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[车牌号]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="437" y="78" width="118" height="18" uuid="8971461a-8c2e-4c4e-8c3f-9bd177df4b7e">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{noCar}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="0" y="78" width="50" height="18" uuid="74602da5-9195-47dd-9416-9cb229aeccad">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[卡名称]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="185" y="78" width="94" height="18" uuid="49312902-8cda-4fe3-a92f-d6ecbf9cb893">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{memberNo}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="140" y="78" width="44" height="18" uuid="65483940-e0c7-4713-9e9a-6a5a7294d248">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[卡号]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="78" width="90" height="18" uuid="e9c8328d-22ea-4e66-a23f-5015ffbd8248">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{cardName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="279" y="78" width="40" height="18" uuid="19b4e87c-4a50-42ef-bfd0-7ca0536d5526">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[持卡人]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="319" y="78" width="70" height="18" uuid="86c056e0-0ed9-4024-b868-4133a06c5bab">
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="3"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{customerName}]]></textFieldExpression>
|
||||
</textField>
|
||||
<line>
|
||||
<reportElement x="0" y="78" width="555" height="1" uuid="f6716829-054d-4fed-a12c-2f4d227724c7">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Dotted"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<line>
|
||||
<reportElement x="0" y="96" width="555" height="1" uuid="9c9a29fc-1a65-4869-8d03-3242ff1fe667">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
</band>
|
||||
<band height="78">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<staticText>
|
||||
<reportElement x="0" y="36" width="50" height="18" uuid="2cf35160-a982-4dcc-881b-c53adcf3761a">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
</reportElement>
|
||||
<box>
|
||||
<pen lineWidth="0.5" lineStyle="Solid"/>
|
||||
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
|
||||
</box>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[退款方式:]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="0" width="120" height="18" uuid="6b4cf2c5-cb81-43b6-82c9-ee238923a378">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="10" isBold="true"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[退卡金额(元)]]></text>
|
||||
</staticText>
|
||||
<staticText>
|
||||
<reportElement x="0" y="17" width="50" height="18" uuid="782a08a7-c605-4a2f-83b7-4e3ad8ee3c4c">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[退款金额]]></text>
|
||||
</staticText>
|
||||
<line>
|
||||
<reportElement x="0" y="35" width="555" height="1" uuid="8699bef5-cebd-4ebc-bb34-3697568375f7">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Dotted"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<line>
|
||||
<reportElement stretchType="RelativeToTallestObject" x="0" y="18" width="555" height="1" uuid="18287ba4-b04b-4846-9122-3679f693685c">
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Dotted"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="37" width="504" height="16" uuid="4ff3b629-b9e5-49c5-ad89-ce856a556839">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{paymentTypeAndAmount}]]></textFieldExpression>
|
||||
</textField>
|
||||
<line>
|
||||
<reportElement x="0" y="54" width="555" height="1" uuid="98b923f9-1e14-4cc5-b508-12141cefb96e">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
</reportElement>
|
||||
<graphicElement>
|
||||
<pen lineWidth="0.4" lineStyle="Solid"/>
|
||||
</graphicElement>
|
||||
</line>
|
||||
<staticText>
|
||||
<reportElement x="0" y="55" width="50" height="18" uuid="fc27cb3d-0e5d-4143-b83d-daf432fa56c8">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="false"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[打印门店]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="55" width="110" height="18" uuid="1a73fa76-4113-4dcd-803c-da26e934f441"/>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{orgOfPrinting}]]></textFieldExpression>
|
||||
</textField>
|
||||
<staticText>
|
||||
<reportElement x="160" y="55" width="80" height="18" uuid="786b4cf3-7636-455c-bf05-115ff8ca71b7">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
|
||||
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
|
||||
</reportElement>
|
||||
<textElement textAlignment="Left" verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9" isBold="false"/>
|
||||
<paragraph leftIndent="5"/>
|
||||
</textElement>
|
||||
<text><![CDATA[打印日期]]></text>
|
||||
</staticText>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="240" y="55" width="314" height="18" uuid="61b50ddd-d880-403c-855f-3ae2f0403592"/>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{dateOfPrinting}]]></textFieldExpression>
|
||||
</textField>
|
||||
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
|
||||
<reportElement x="50" y="18" width="504" height="16" uuid="7ce3fef6-73c6-49e9-a676-64e402603d93"/>
|
||||
<textElement verticalAlignment="Middle">
|
||||
<font fontName="黑体" size="9"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$P{amount}]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
<band height="39">
|
||||
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
|
||||
</band>
|
||||
</detail>
|
||||
</jasperReport>
|
||||
+1586
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user