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.path 在 RequestHelper.preparePamirsRequestParam 中经 SessionPrepareApi.prepare 注入会话上下文,与前端 getSessionPath() 对应。
2.2.2 Excel 导出对照:ExcelFileServiceImpl#doExportSync
Excel 同步导出使用 mutation(
createExportTask),因为会创建导出任务记录;图片查看无此副作用,故用 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 端到端时序

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)。图片请求参数应保持精简(businessId、imageType 等标量字段),复杂渲染逻辑在后端按 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 请求依赖同源 Cookie。variables.path 用于恢复视图会话;若 Function 不依赖会话上下文,可按需简化。
7.4 本地开发代理
本地开发时 /pamirs 需代理到后端,例如在 Vue CLI vue.config.js 中:
proxy: {
'/pamirs': { target: 'http://your-backend-host:port', changeOrigin: true }
}
8. 实施检查清单
- [ ] 后端:
BusinessImageRequest模型 +renderBusinessImageQuery 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. 相关文档
- RSQL 占位符与 EXISTS 扩展 — 若图片生成依赖 RSQL 条件过滤可参考
Oinone社区 作者:nation原创文章,如若转载,请注明出处:https://doc.oinone.top/kai-fa-shi-jian/25587.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验