技术选型与功能设计

发布时间:2026/7/1 2:51:57
技术选型与功能设计 Vaadin是一个面向企业级应用的现代 Web 开发框架专注于Java 全栈开发、组件化 UI 构建并提供丰富的开箱即用 Web Components。Vaadin 的优势包括Java 到前端的统一开发体验企业级安全性与长生命周期支持高质量 UI 组件库与设计系统与 Figma 无缝衔接的 Copilot AI 辅助开发能力在最新版本中Vaadin Copilot新增了Figma Importer API实现“从设计到代码”的自动化让开发者可以直接将 Figma 组件复制并粘贴到 Vaadin 项目中生成 Java 或 React 代码。本文基于 Vaadin 官方示例并由慧都整理改写帮助国内开发者快速上手。使用minio服务进行文件的中转与存储。用户提交文件到doc-llm-controller控制面将文件转存到minio中关联此次任务id。然后doc-llm-worker轮询redis发现有需要执行的任务拿到id后根据id从minio拿取文件然后将文件解析成结构化信息再提交到大模型进行文档测试。那么此部分功能流程图大致如下相对应的在整体业务流程中补充文件存取步骤最后如下二、minio配置与使用minio安装部署我们使用docker镜像来部署minio服务暴露9000端口提供给我们自己服务使用docker run -d --name doc-llm-minio -p 9000:9000 -p 9001:9001 --restartalways -e MINIO_ROOT_USERroot -e MINIO_ROOT_PASSWORDpassword -v /home/workspace/minio:/data minio/minio:latest server /data --console-address :9001通过python来调用minio服务# minio下载 pip install miniofrom minio import Minio from minio.error import S3Error import io # 配置minio client Minio( localhost:9000, access_keyroot, secret_keyxiao1234, secureFalse, ) bucket_name doc-llm-bucket try: if not client.bucket_exists(bucket_name): client.make_bucket(bucket_name) else: print(fBucket {bucket_name} already exists.) except S3Error as e: print(fError occurred: {e}) # 通过python上传文件到minio def upload_file(local_file_path, object_name): try: client.fput_object(bucket_name, object_name, local_file_path) print(f{local_file_path} is successfully uploaded as {object_name} to bucket {bucket_name}.) except S3Error as e: print(fError occurred while uploading: {e}) # 文件下载 def download_file(object_name, local_file_path): try: client.fget_object(bucket_name, object_name, local_file_path) print(f{object_name} is successfully downloaded to {local_file_path}.) except S3Error as e: print(fError occurred while downloading: {e}) # 列出所有文件 def list_files(): try: objects client.list_objects(bucket_name) print(fObjects in bucket {bucket_name}:) for obj in objects: print(f- {obj.object_name} (size: {obj.size} bytes)) except S3Error as e: print(fError occurred while listing objects: {e}) # 删除指定文件 def delete_file(object_name): try: client.remove_object(bucket_name, object_name) print(f{object_name} is successfully deleted from bucket {bucket_name}.) except S3Error as e: print(fError occurred while deleting: {e})测试效果如下三、控制面doc-llm-controller服务适配总体思路接口层接收到带文件的创建任务请求先新增一条任务数据到mysql其中doc字段为__PENDING_FILE__。然后拿到任务id后调用推送文件服务将文件关联任务id一起推送到minio结束后更新任务信息doc字段为fminio://{MINIO_BUCKET}/{object_name}。至此控制面业务结束。services层新增file_service.py提供minio服务的调用# 代码样例 def _ensure_bucket(): 确保 bucket 存在 if not _minio_client.bucket_exists(MINIO_BUCKET): _minio_client.make_bucket(MINIO_BUCKET) def save_task_file(task_id: int, file_obj: FileStorage) - str: 把用户上传的文件存到 MinIO文件名格式{task_id}_{orig_filename} 返回存入数据库的 doc 字段值例如minio://doc-llm-bucket/123_xxx.docx ... doc_path fminio://{MINIO_BUCKET}/{object_name} return doc_path给doc_check_service, task_service 增加更新doc方法# doc_check_service def update_task_doc(task_id: int, doc: str) - None: 更新任务的 doc 字段 task task_service.get_task_by_id(task_id) if not task: raise TaskNotFoundError(f任务 {task_id} 不存在) task_service.update_task_doc(task_id, doc) # task_service def update_task_doc(task_id: int, doc: str) - None: 更新任务的 doc 字段 with get_session() as session: task session.scalar( select(TaskDocLLM).where(TaskDocLLM.task_id task_id) ) if not task: raise ValueError(f任务 {task_id} 不存在) task.doc doc更新接口函数兼容传文本信息、文本文件两种方式bp.route(/tasks/, methods[POST]) def create_doc_task(): # 判断是不是文件上传 if request.content_type and multipart/form-data in request.content_type: return _create_task_with_file() # 默认走老的 JSON 逻辑 return _create_task_with_json() def _create_task_with_json(): ... task_id doc_check_service.submit_doc_task(task_name, doc, product, feature) ... def _create_task_with_file(): .... try: # 1. 先写一条任务doc 用占位符保证非空 placeholder_doc __PENDING_FILE__ task_id doc_check_service.submit_doc_task( task_nametask_name, docplaceholder_doc, productproduct, featurefeature, ) doc_path file_service.save_task_file(task_id, file_obj) # 3. 回写 doc 字段 doc_check_service.update_task_doc(task_id, doc_path) ...用postman测试下接口效果大致是OK的接口请求flask这边日志、数据库、minio表现都OK数据一致性有保障四、数据面doc-llm-worker服务适配当前数据流的流转从时间先后顺序最先会写入task到mysql此时doc字段是pending字样然后写入task_id到redis再就是把文件传给minio最后更新mysql.doc为minio的文件路径。doc-llm-worker初始逻辑是读redis队列找到需要执行的任务读mysql拿到doc文本信息调用大模型进行测试。因此数据面doc-llm-worker要做一些适配1.新增文件任务的下载从minio下载文件在file_service层补充函数def download_file(bucket: str, object_name: str) - bytes: 从 MinIO 下载文件并返回 bytes 内容。 调用方式 content download_file(doc-llm-bucket, 15_readme.txt) text content.decode(utf-8) try: response _minio_client.get_object(bucket, object_name) data response.read() return data except S3Error as e: raise RuntimeError(fDownload from minio failed: {e}) from e2.将文件解析其中如果doc是纯文本的话走老逻辑是minio格式的话走文件下载然后解析成文本是pending的话等待知道文件上传ok新增doc_loader.py# app/worker/doc_loader.py import logging from typing import Tuple from app.services import file_service PENDING_MARK __PENDING_FILE__ def _is_minio_path(doc: str) - bool: 判断 doc 是否为 MinIO 路径 - /bucket/object_name - minio://bucket/object_name ... def _parse_minio_path(doc: str) - Tuple[str, str]: 解析 doc 字段为 (bucket, object_name) ... def load_doc_for_task(task) - str: 根据任务对象返回真正要给 LLM 的 doc 文本str 1. doc __PENDING_FILE__ - 抛 DocPendingError 2. doc 是 MinIO 路径 (/bucket/obj) - 从 MinIO 下载并 decode 3. 其他 - 当作普通文本直接返回 doc (task.doc or ).strip() if not doc: raise DocPathError(ftask {task.id} doc is empty) if doc PENDING_MARK: raise DocPendingError(ftask {task.id} doc is still pending file upload) if _is_minio_path(doc): bucket, object_name _parse_minio_path(doc) logging.info( ftask {task.id} doc is minio path, bucket{bucket}, object{object_name} ) content_bytes file_service.download_file(bucket, object_name) return content_bytes.decode(utf-8, errorsreplace) return doc3.worker的处理读redis队列根据任务id找到这条task但当文件任务doc字段还是__PENDING_FILE__时做阻塞等待直到doc字段更新为minio://{bucket}/{object_name}从minio下载文件再处理适配doc_llm_test_worker新增阻塞等待函数def wait_for_doc_ready(task_id: int): 当 doc __PENDING_FILE__ 时等待 doc 字段被控制面更新。 超过最大重试次数仍未更新则抛出异常。 PENDING_RETRY_INTERVAL 2 PENDING_RETRY_MAX 5 for i in range(PENDING_RETRY_MAX): time.sleep(PENDING_RETRY_INTERVAL) task task_service.get_task_by_id(task_id) if not task: raise RuntimeError(ftask {task_id} disappeared during pending wait) doc (task.doc or ).strip() if doc ! doc_loader.PENDING_MARK: logging.info(ftask {task_id} doc is ready after {i1} retries: {doc}) return task logging.info(ftask {task_id} doc still pending (retry {i1}/{PENDING_RETRY_MAX})) raise RuntimeError(ftask {task_id} doc still pending after max1. 功能概览从 Figma 复制、到 Vaadin 自动生成代码通过Vaadin Copilot的Figma Importer API你可以做到从 Figma 复制组件或实例如卡片、按钮在 Vaadin 项目中直接粘贴自动生成对应的JavaFlow或ReactHilla组件代码Figma Importer API 用于将 Simple Design System Card 映射到 Java SDSCard 组件。官方项目基于Demo 项目Figma 组件与实例示例2. 使用前准备启用 Vite 热重载推荐在 Spring Bootapplication.properties中加入vaadin.frontend.hotdeploytrue3. Figma 组件体系与 Importer API 原理许多公司包括 Vaadin都有自己的 Figma 设计系统也可使用公开设计系统如 Simple Design System。Vaadin Copilot24.9 引入的Figma Importer API用于将这些组件映射为真实代码。Figma → Vaadin 的映射关系Figma 的Component≈ Java/TS 中的classFigma 的Instance≈ Java/TS 中的object属性可被实例覆盖通过“标记属性marker property”来区分组件类型例如Figma 中 Card 组件的属性type SDSCardImporter 仅匹配带有此属性的组件。4. 目标 Java / React 组件示例以 SDSCard 与 SDSButton 为例若手动创建 Java 组件var sdscard new SDSCard(); sdscard.setTitle(Great news!); sdscard.setBody(sayHello); var sdsbutton new SDSButton(); sdsbutton.setLabel(Sure!); sdscard.add(sdsbutton);若使用 ReactSDSCard titleGreat news! span slotbodyDid you know that Vaadin Copilot can import Figma components?/span SDSButton labelSure! / /SDSCard示例工程已包含这些组件。5. 编写 Importer从 Figma 转换为 Java/ReactImporter 是一个TypeScript 函数输入 FigmaNode输出 ComponentDefinition根据目标语言Java 或 React生成代码结构5.1 SDSCard Java Importer 示例文件frontend/sdscard-java-importer.tsfunction sdsCardJavaImporter(node, metadata) { if (node.properties.type SDSCard metadata.target java) { return { tag: SDSCard, props: { title: node.properties.title, body: { tag: Span, props: { text: node.properties.body }, javaClass: com.vaadin.flow.component.html.Span, }, }, children: createChildrenDefinitions(node, metadata, n { return n.properties.type SDSButton; }), javaClass: test.vaadin.copilot.flow.testviews.ui.customcomponents.components.SDSCard, }; } } registerImporter(sdsCardJavaImporter);核心点说明匹配type: SDSCard生成 Java 组件结构子组件过滤以查找 Button5.2 SDSButton Java Importer文件frontend/sdsbutton-java-importer.tsfunction sdsButtonJavaImporter(node, metadata) { if (node.properties.type SDSButton metadata.target java) { return { tag: SDSButton, props: { label: node.properties.label }, children: [], javaClass: test.vaadin.copilot.flow.testviews.ui.customcomponents.components.SDSButton, }; } } registerImporter(sdsButtonJavaImporter);5.3 SDSCard React Importer文件frontend/sdscard-react-importer.tsfunction sdsCardReactImporter(node, metadata) { if (node.properties.type SDSCard metadata.target react) { return { tag: SDSCard, props: { title: node.properties.title }, children: [ { tag: span, props: { slot: body }, children: [node.properties.body.toString()], }, ...createChildrenDefinitions(node, metadata, n n.properties.type SDSButton), ], reactImports: { SDSCard: Frontend/components/SDSCard }, }; } } registerImporter(sdsCardReactImporter);5.4 SDSButton React Importer文件frontend/sdsbutton-react-importer.tsfunction sdsButtonReactImporter(node, metadata) { if (node.properties.type SDSButton metadata.target react) { return { tag: SDSButton, props: { label: node.properties.label }, children: [], reactImports: { SDSButton: Frontend/components/SDSButton }, }; } } registerImporter(sdsButtonReactImporter);6. 在项目中启用 ImporterFlowJava项目中SpringBootApplication JsModule(value ./sdscard-java-importer.ts, developmentOnly true) JsModule(value ./sdsbutton-java-importer.ts, developmentOnly true) JsModule(value ./sdscard-react-importer.ts, developmentOnly true) JsModule(value ./sdsbutton-react-importer.ts, developmentOnly true) public class Application implements AppShellConfigurator {}React 项目index.tsxif (import.meta.env.DEV) { import(./sdscard-java-importer); import(./sdsbutton-java-importer); import(./sdscard-react-importer); import(./sdsbutton-react-importer); }7. 自动生成的代码示例