GQL GET 请求生成图片文件流 — 技术实现方案

GQL GET 请求生成图片文件流 — 技术实现方案

基于 Oinone/Pamirs GQL GET 文件流机制,参考 FileAction#downloadFormData(查询型)与 ExcelExportTaskAction#createExportTask(同步写流),实现「按业务 ID 动态生成图片并输出文件流」的通用指南。


1. 背景与目标

1.1 需求

在业务场景中,需要根据业务 ID(如订单 ID、模板 ID、单据 ID)在后端动态渲染图片(条码、标签、证书、预览图等),并将结果以文件流形式返回给前端。

1.2 约束

  • 复用 Oinone 既有鉴权、会话、模块路由体系,不新增独立 REST 下载接口
  • 与 Excel 同步导出保持一致的调用方式:通过 GET 访问 /pamirs/{module}?query=...&variables=...
  • 前端仅两种消费方式:
    • 页面预览<img :src="imageUrl" />
    • 触发下载window.open(imageUrl, '_blank')

2. 参考实现

2.1 查询型文件流(图片查看应对齐此模式)

图片查看属于只读查询,应使用 query + @Function(type = QUERY),与 CDN 文件下载一致:

// @oinone/kunlun-vue-ui-common/.../UploadService.ts
const gql = `query{resourceFileFormQuery{downloadFormData(resourceFileForm:{downloadUrl:"${url}"}){filename}}}`;
window.open(UrlHelper.appendBasePath(`/pamirs/file?query=${encodeURIComponent(gql)}`), '_blank');
// pamirs-core/pamirs-file2/.../FileAction.java
@Function.Advanced(displayName = "前端下载数据", type = FunctionTypeEnum.QUERY)
public ResourceFileForm downloadFormData(ResourceFileForm resourceFileForm) {
    // 直接写 HttpServletResponse 输出流
    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition", "attachment; filename=" + ...);
    IOUtils.copy(inputStream, response.getOutputStream());
    return new ResourceFileForm();
}

图片查看用 GQL.query,不用 GQL.mutation

2.2 同步写流机制(共用)

无论 query 还是 mutation,文件流输出的底层机制相同:在 Function/Action 执行过程中直接写 HttpServletResponse

2.2.1 HTTP 网关:RequestController

GET  /pamirs/{moduleName}?query={gql}&variables={json}
POST /pamirs/{moduleName}  body: { query, variables }

GET 与 POST 走同一执行链路

// pamirs-framework/pamirs-gateways-graph-java/.../RequestController.java
@RequestMapping(value = "/pamirs/{moduleName}", method = RequestMethod.GET)
public DeferredResult<String> pamirsGet(
    @PathVariable("moduleName") String moduleName,
    @RequestParam("query") String gql,
    @RequestParam(value = "variables", required = false) String variables,
    ...) {
    PamirsClientRequestParam gqlRequest = new PamirsClientRequestParam();
    gqlRequest.setQuery(gql);
    if (!StringUtils.isBlank(variables)) {
        gqlRequest.setVariables(JsonUtils.parseMap(variables));
    }
    return pamirsPost(moduleName, gqlRequest, request, response);
}

variables.pathRequestHelper.preparePamirsRequestParam 中经 SessionPrepareApi.prepare 注入会话上下文,与前端 getSessionPath() 对应。

2.2.2 Excel 导出对照:ExcelFileServiceImpl#doExportSync

Excel 同步导出使用 mutationcreateExportTask),因为会创建导出任务记录;图片查看无此副作用,故用 query

同步导出的关键:劫持 FileClient.upload,将字节直接写入当前 HTTP 响应:

// pamirs-core/pamirs-file2/.../ExcelFileServiceImpl.java
public void doExportSync(ExcelExportTask exportTask, ExcelDefinitionContext context) {
    FileClient fileClient = new FileClient() {
        @Override
        public CdnFile upload(String fileName, byte[] data) {
            HttpServletResponse response = ...; // 从 RequestContextHolder 获取
            response.addHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(data.length));
            response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(fileName, UTF_8));
            ServletOutputStream sos = response.getOutputStream();
            sos.write(data);
            sos.flush();
            return internalFileClient.upload(fileName, data);
        }
    };
    doExport0(exportTask, context, fileClient);
}

核心模式:GQL Function 执行业务逻辑 → 生成 byte[] → 通过 HttpServletResponse.getOutputStream() 直接输出,不再返回 JSON

2.3 mutation vs query 选型

场景 GQL 类型 后端注解 原因
图片查看 / 下载 query @Function(type = QUERY) 只读,按 ID 渲染并输出流
Excel 同步导出 mutation @Action 创建导出任务记录,附带写流
CDN 文件下载 query @Function(type = QUERY) 只读,代理已有文件流

2.4 端到端时序

GQL GET 请求生成图片文件流 — 技术实现方案


3. 通用图片流方案

3.1 调用方式

图片生成通过 GQL query 发起,一次 GET 请求完成渲染并输出文件流。前端通过同一 URL,配合后端 Content-Disposition 区分两种用途:

前端用法 后端响应头 效果
<img :src="imageUrl" /> Content-Disposition: inline 页面内预览
window.open(imageUrl, '_blank') Content-Disposition: attachment 新标签触发下载

请求参数中增加 download 字段控制响应头(见 §4、§5)。

3.2 模块选择

moduleName 说明
file 走 file 子系统,与 Excel 导出同模块
业务模块(如 order Action 挂在业务模型上,适合强业务耦合的图片生成

4. 后端实现指南

4.1 数据模型

@Model.model("order.BusinessImageRequest")
@Model(displayName = "业务图片请求")
public class BusinessImageRequest extends IdModel {

    @Field.String
    @Field(displayName = "业务ID")
    private String businessId;

    @Field.String
    @Field(displayName = "图片类型", summary = "如 BARCODE / LABEL / CERTIFICATE")
    private String imageType;

    @Field.Boolean
    @Field(displayName = "下载模式", summary = "true=attachment 下载,false=inline 预览")
    private Boolean download;

    @Field.Integer
    @Field(displayName = "宽度")
    private Integer width;

    @Field.Integer
    @Field(displayName = "高度")
    private Integer height;
}

4.2 Function 实现(查询型)

@Component
@Model.model(BusinessImageRequest.MODEL_MODEL)
public class BusinessImageAction {

    @Autowired
    private BusinessImageService businessImageService;

    @Function.Advanced(displayName = "渲染业务图片", type = FunctionTypeEnum.QUERY)
    @Function(openLevel = {FunctionOpenEnum.API, FunctionOpenEnum.LOCAL, FunctionOpenEnum.REMOTE})
    public BusinessImageRequest renderBusinessImage(BusinessImageRequest businessImageRequest) {
        businessImageService.renderImage(businessImageRequest);
        return businessImageRequest;
    }
}

4.3 Service:写图片流

@Service
public class BusinessImageServiceImpl implements BusinessImageService {

    public void renderImage(BusinessImageRequest request) {
        byte[] imageBytes = generateImage(request);

        HttpServletResponse response = Optional.ofNullable(RequestContextHolder.getRequestAttributes())
            .map(a -> (ServletRequestAttributes) a)
            .map(ServletRequestAttributes::getResponse)
            .orElseThrow(() -> new RuntimeException("未获取到 Http 响应"));

        try {
            String fileName = buildFileName(request);
            boolean download = Boolean.TRUE.equals(request.getDownload());
            String disposition = download ? "attachment" : "inline";

            response.setContentType("image/png");
            response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(imageBytes.length));
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                disposition + ";filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));

            ServletOutputStream out = response.getOutputStream();
            out.write(imageBytes);
            out.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private byte[] generateImage(BusinessImageRequest request) {
        // 1. 按 businessId 查业务数据
        // 2. 调用渲染引擎生成图片
        // 3. 返回 PNG/JPEG byte[]
        return ...;
    }
}

实现模式与 FileAction#downloadFormData 一致:在 GQL Query Function 调用链内直接写 HttpServletResponse,无需额外 Controller。


5. 前端实现指南

5.1 构造图片 URL(公共方法)

import { GQL, getSessionPath } from '@oinone/kunlun-request';
import { UrlHelper } from '@oinone/kunlun-shared';

interface BuildImageUrlOptions {
  moduleName: string;      // 如 'order'
  queryModel: string;      // 如 'businessImageRequest'(GQL 自动拼接为 businessImageRequestQuery)
  businessId: string;
  imageType: string;
  download?: boolean;      // false=预览(inline),true=下载(attachment)
}

async function buildImageUrl(options: BuildImageUrlOptions): Promise<string> {
  const { moduleName, queryModel, businessId, imageType, download = false } = options;

  const gql = await GQL.query(queryModel, 'renderBusinessImage')
    .buildRequest((builder) => {
      builder.buildObjectParameter('businessImageRequest', (b) => {
        b.stringParameter('businessId', businessId);
        b.stringParameter('imageType', imageType);
        b.booleanParameter('download', download);
      });
    })
    .buildResponse((b) => b.parameter('id'))
    .toString();

  const variables = { path: getSessionPath() };
  return UrlHelper.appendBasePath(
    `/pamirs/${moduleName}?query=${encodeURIComponent(gql)}&variables=${encodeURIComponent(
      JSON.stringify(variables)
    )}`
  );
}

5.2 方式一:<img src> 页面预览

<template>
  <img :src="imageUrl" alt="业务图片预览" />
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';

const props = defineProps<{ businessId: string; imageType: string }>();
const imageUrl = ref('');

watch(
  () => [props.businessId, props.imageType],
  async () => {
    imageUrl.value = await buildImageUrl({
      moduleName: 'order',
      queryModel: 'businessImageRequest',
      businessId: props.businessId,
      imageType: props.imageType,
      download: false   // inline,供 img 内联渲染
    });
  },
  { immediate: true }
);
</script>

5.3 方式二:window.open 下载图片

async function downloadBusinessImage(businessId: string, imageType: string) {
  const url = await buildImageUrl({
    moduleName: 'order',
    queryModel: 'businessImageRequest',
    businessId,
    imageType,
    download: true    // attachment,触发浏览器下载
  });
  window.open(url, '_blank');
}

6. GQL 请求示例

6.1 预览(download: false

query {
  businessImageRequestQuery {
    renderBusinessImage(businessImageRequest: {
      businessId: "ORD-20260702-001"
      imageType: "BARCODE"
      download: false
    }) {
      id
    }
  }
}

6.2 下载(download: true

query {
  businessImageRequestQuery {
    renderBusinessImage(businessImageRequest: {
      businessId: "ORD-20260702-001"
      imageType: "BARCODE"
      download: true
    }) {
      id
    }
  }
}

6.3 完整 GET URL(解码后示意)

/{basePath}/pamirs/order?query=query{businessImageRequestQuery{renderBusinessImage(businessImageRequest:{businessId:"ORD-20260702-001",imageType:"BARCODE",download:false}){id}}}&variables={"path":"order.OrderList#search"}

7. 注意事项

7.1 URL 长度

浏览器与网关对 GET URL 长度有限制(通常 2KB~8KB)。图片请求参数应保持精简(businessIdimageType 等标量字段),复杂渲染逻辑在后端按 ID 自行查数。

7.2 响应类型

场景 download Content-Type Content-Disposition
<img src> 预览 false image/png inline;filename=...
window.open 下载 true image/png attachment;filename=...

7.3 鉴权

GET 请求依赖同源 Cookievariables.path 用于恢复视图会话;若 Function 不依赖会话上下文,可按需简化。

7.4 本地开发代理

本地开发时 /pamirs 需代理到后端,例如在 Vue CLI vue.config.js 中:

proxy: {
  '/pamirs': { target: 'http://your-backend-host:port', changeOrigin: true }
}

8. 实施检查清单

  • [ ] 后端:BusinessImageRequest 模型 + renderBusinessImage Query Function(写 HttpServletResponse
  • [ ] 后端:download 字段控制 inline / attachment
  • [ ] 前端:公共 buildImageUrl 方法(GQL.query 构造 + encodeURIComponent
  • [ ] 前端:variables.path = getSessionPath()
  • [ ] 前端:UrlHelper.appendBasePath 拼接 BASE_PATH
  • [ ] 联调:dev 代理 /pamirs 到后端
  • [ ] 验证:<img> 内联预览、window.open 下载、未登录拦截

9. 源码索引

9.1 前端(Oinone 框架)

说明 路径
查询型文件下载 URL @oinone/kunlun-vue-ui-common/.../UploadService.ts (generatorDownloadUrl)
GQL Builder @oinone/kunlun-request/.../gql.ts
URL 工具 @oinone/kunlun-shared/.../UrlHelper.ts

9.2 后端(oinone-pamirs)

说明 路径
GET/POST 网关 pamirs-framework/pamirs-gateways-graph-java/.../RequestController.java
会话准备 pamirs-framework/pamirs-gateways-graph-java/.../RequestHelper.java
查询型文件流(首选参考) pamirs-core/pamirs-file2/pamirs-file2-api/.../FileAction.java (downloadFormData)
Excel 同步写流(机制参考) pamirs-core/pamirs-file2/pamirs-file2-api/.../ExcelFileServiceImpl.java (doExportSync)
GET URL 编码测试 pamirs-core/pamirs-file2/pamirs-file2-core/.../RequestGetUrlTest.java

10. 相关文档

Oinone社区 作者:nation原创文章,如若转载,请注明出处:https://doc.oinone.top/kai-fa-shi-jian/25587.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

Like (0)
nation's avatarnation数式员工
Previous 2026年3月31日 pm11:21
Next 2023年12月18日 am11:37

相关推荐

  • 如何使用GQL工具正确发起请求

    简介 本文将讲解一下如何正确发起GQL请求和GQL工具使用过程中的常见问题。 参数介绍 请求url和请求方法在浏览器的请求标头里面可以查到,保持一致就可以向服务正常发送请求,但还缺少请求体确定请求哪个接口。 每个请求都包括两部分内容:1. Query 2. Variables右键Query和Variables复制值直接拷贝到工具中就可以正常请求了。 注意:如果使用admin账号登录,请求的时候可以不携带Variables参数,因为admin没有权限控制,如果使用其他用户,就必须携带Variables参数,否则会被权限拦截。

    2024年10月25日
    1.8K00
  • Oinone协同开发使用手册

    概述 Oinone平台为开发人员提供了本地环境 – 测试环境之间的协同开发模式,可以使得开发人员在本地环境中设计的模型、函数等元数据实时被测试环境使用并设计。开发人员开发完成对应页面和功能后,可以部署至测试环境直接进行测试。 本篇文章将详细介绍协同开发模式在实际开发中的应用及相关内容。 名词解释 本地环境: 开发人员的本地启动环境 测试环境: 在测试服务器上部署的业务测试环境,业务工程服务和设计器服务共用中间件 业务工程服务:在测试服务器上部署的业务工程 设计器服务: 在测试服务器上部署的设计器镜像 一套环境:以测试环境为例,业务工程服务和设计器服务共同组成一套环境 生产环境: 在生产服务器上部署的业务生产环境 环境准备 部署了一个可用的设计器服务,并能正常访问。(需参照下文启动设计器环境内容进行相应修改) 准备一个用于开发的java工程。 准备一个用于部署测试环境的服务器。 协同参数介绍 用于测试环境的参数 -PmetaProtected=${value} 启用元数据保护,只有配置相同启动参数的服务才允许对元数据进行更新。通常该命令用于设计器服务和业务工程服务,并且两个环境需使用相同的元数据保护标记(value)进行启动。本地环境不使用该命令,以防止本地环境在协同开发时意外修改测试环境元数据,导致元数据混乱。 用法 java -jar boot.jar -PmetaProtected=pamirs 用于本地环境的配置 使用命令配置ownSign(推荐) java -jar boot.jar –pamirs.distribution.session.ownSign=demo 使用yaml配置ownSign pamirs: distribution: session: allMetaRefresh: false # 启用元数据全量刷新(备用配置,如遇元数据错误或混乱,启用该配置可进行恢复,使用一次后关闭即可) ownSign: demo # 协同开发元数据隔离标记,用于区分不同开发人员的本地环境,其他环境不允许使用 启动设计器环境 docker-run启动 -e PROGRAM_ARGS=-PmetaProtected=pamirs docker-compose启动 services: backend: container_name: designer-backend image: harbor.oinone.top/oinone/designer-backend-v5.0 restart: always environment: # 指定spring.profiles.active ARG_ENV: dev # 指定-Plifecycle ARG_LIFECYCLE: INSTALL # jvm参数 JVM_OPTIONS: "" # 程序参数 PROGRAM_ARGS: "-PmetaProtected=pamirs" PS: java [JVM_OPTIONS?] -jar boot.jar [PROGRAM_ARGS?] 开发流程示例图 具体使用步骤详见协同开发支持

    2024年7月24日
    2.5K00
  • DsHint(指定数据源)和BatchSizeHint(指定批次数量)

    概述和使用场景 DsHintApi ,强制指定数据源, BatchSizeHintApi ,强制指定查询批量数量 API定义 DsHintApi public static DsHintApi model(String model/**模型编码*/) { // 具体实现 } public DsHintApi(Object dsKey/***数据源名称*/) { // 具体实现 } BatchSizeHintApi public static BatchSizeHintApi use(Integer batchSize) { // 具体实现 } 使用示例 1、【注意】代码中使用 try-with-resources语法; 否则可能会出现数据源错乱 2、DsHintApi使用示例包裹在try里面的所有查询都会强制使用指定的数据源 // 使用方式1: try (DsHintApi dsHintApi = DsHintApi.model(PetItem.MODEL_MODEL)) { List<PetItem> items = demoItemDAO.customSqlDemoItem(); PetShopProxy data2 = data.queryById(); data2.fieldQuery(PetShopProxy::getPetTalents); } // 使用方式2: try (DsHintApi dsHintApi = DsHintApi.use("数据源名称")) { List<PetItem> items = demoItemDAO.customSqlDemoItem(); PetShopProxy data2 = data.queryById(); data2.fieldQuery(PetShopProxy::getPetTalents); } 3、BatchSizeHintApi使用示例包裹在try里面的所有查询都会按照指定的batchSize进行查询 // 查询指定每次查询500跳 try (BatchSizeHintApi batchSizeHintApi = BatchSizeHintApi.use(500)) { PetShopProxy data2 = data.queryById(); data2.fieldQuery(PetShopProxy::getPetTalents); } // 查询指定不分页(batchSize=-1)查询。 请注意,你必须在明确不需要分页查询的情况下使用;如果数据量超大不分页可能会卡死。默认不指定分页数的情况下下平台会进行分页查询 try (BatchSizeHintApi batchSizeHintApi = BatchSizeHintApi.use(-1)) { PetShopProxy data2 = data.queryById(); data2.fieldQuery(PetShopProxy::getPetTalents); }

    2024年5月18日
    2.0K00
  • 环境运行时Jar版本控制

    环境运行时Jar版本控制 前景 为了避免基于低代码定义产生的元数据错乱。因此产生了运行时Jar版本检查功能。 现象 如果当前运行时依赖的Ja版本低于已安装版本,启动时会有如下类似信息提示: 解决 按照提示升级依赖Jar版本 通过启动参数 -PgoBack=true 强制覆盖安装当前运行时版本 java -jar 方式 java -jar xxx.jar -PgoBack=true [其他参数] mvn spring-boot run 方式 mvn clean compile spring-boot:run -Dspring-boot.run.arguments=”-PgoBack=true [其他参数]”

    2025年3月10日
    1.5K00
  • OSS(CDN)配置和文件系统的一些操作

    目前Oinone支持的OSS类型 类型 服务 OSS 阿里云OSS UPYUN 又拍云 MINIO MinIO HUAWEI_OBS 华为云OBS LOCAL 本地NGINX文件存储 TENCENT_COS 腾讯云COS CTYUN_ZOS 天翼云ZOS OSS通用yaml配置 cdn: oss: name: # 名称 type: # 类型 bucket: uploadUrl: # 上传URL downloadUrl: # 下载URL accessKeyId: accessKeySecret: mainDir: # 主目录 validTime: 3600000 timeout: 600000 active: true referer: localFolderUrl: others: [key]: name: # 名称 type: # 类型 bucket: uploadUrl: # 上传URL downloadUrl: # 下载URL accessKeyId: accessKeySecret: mainDir: # 主目录 validTime: 3600000 timeout: 600000 active: true referer: localFolderUrl: PS:others中使用自定义key来指定OSS服务进行文件上传/下载功能。上传/下载必须匹配,否则无法正常使用。 OSS配置示例 阿里云OSS cdn: oss: name: 阿里云 type: OSS bucket: pamirs(根据实际情况修改) uploadUrl: oss-cn-hangzhou.aliyuncs.com downloadUrl: oss-cn-hangzhou.aliyuncs.com accessKeyId: 你的accessKeyId accessKeySecret: 你的accessKeySecret # 根据实际情况修改 mainDir: upload/ validTime: 3600000 timeout: 600000 active: true imageResizeParameter: referer: 华为云OBS cdn: oss: name: 华为云 type: HUAWEI_OBS bucket: pamirs(根据实际情况修改) uploadUrl: obs.cn-east-2.myhuaweicloud.com downloadUrl: obs.cn-east-2.myhuaweicloud.com accessKeyId: 你的accessKeyId accessKeySecret: 你的accessKeySecret # 根据实际情况修改 mainDir: upload/ validTime: 3600000 timeout: 600000 active: true allowedOrigin: http://192.168.95.31:8888,https://xxxx.xxxxx.com referer: 华为云OBS需要在启动工程增加以下依赖 <okhttp3.version>4.9.3</okhttp3.version> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp3.version}</version> </dependency> 注意事项华为云OBS的防盗链配置,仅允许携带特定referer的才可以,而excel导入后端处理的逻辑匿名读的时候是不带referer的,所以会被拒绝 MINIO 文件系统,mino的配置: cdn: oss: name: minio type: MINIO bucket: pamirs(根据实际情况修改) uploadUrl: http://192.168.243.6:32190(根据实际情况修改) downloadUrl: http://192.168.243.6:9000(根据实际情况修改) accessKeyId: 你的accessKeyId accessKeySecret: 你的accessKeySecret # 根据实际情况修改 mainDir: upload/ validTime: 3600000 timeout: 600000 active: true referer: localFolderUrl: MINIO无公网访问地址下OSS的配置方式: https://doc.oinone.top/yun-wei-shi-jian/7112.html 又拍云 cdn: oss: name: 又拍云…

    后端 2023年11月1日
    2.1K00

Leave a Reply

Please Login to Comment