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.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 端到端时序

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)。图片请求参数应保持精简(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 模型 + 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 2026年7月27日

相关推荐

  • 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.8K00
  • Oinone连接外部数据源方案

    场景描述 在实际业务场景中,有是有这样的需求:链接外部数据进行数据的获取;通常的做法:1、【推荐】通过集成平台的数据连接器,链接外部数据源进行数据操作;2、项目代码中链接数据源,即通过程序代码操作外部数据源的数据; 本篇文章只介绍通过程序代码操作外部数据源的方式. 整体方案 Oinone管理外部数据源,即yml中配置外部数据源; 后端通过Mapper的方式进行数据操作(增/删/查/改); 调用Mapper接口的时候,指定到外部数据源; 详细步骤 1、数据源配置(application.yml), 与正常的数据源配置一样 out_ds_name(外部数据源别名): driverClassName: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # local环境配置调整 url: jdbc:mysql://ip(host):端口/数据库Schema?useSSL=false&allowPublicKeyRetrieval=true&useServerPrepStmts=true&cachePrepStmts=true&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true username: 用户名 password: 命名 initialSize: 5 maxActive: 200 minIdle: 5 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true asyncInit: true 2、外部数据源其他配置外部数据源限制创建表结构的执行,可以通过配置指定【不创建DB,不创建数据表】 persistence: global: auto-create-database: true auto-create-table: true ds: out_ds_name(外部数据源别名): # 不创建DB auto-create-database: false # 不创建数据表 auto-create-table: false 3、后端写Mapper SQL Mapper跟使用原生mybaits/mybaits-plus写法一样,无特殊限制; Mapper和SQL写到一起,或者分开两个文件都可以 4、Mapper被Service或者Action调用1)启动的Application中@MapperScan需要扫描到对应的包。2)调用是与普通bean一样(即调用方式跟传统的方式样),唯一的区别就是加上DsHintApi,即指定Mapper所使用的数据源。 @Autowired private ScheduleItemMapper scheduleItemMapper; public saveData(Object data) { ScheduleQuery scheduleQuery = new ScheduleQuery(); //scheduleQuery.setActionName(); try (DsHintApi dsHint = DsHintApi.use(“外部数据源名称”)) { List<ScheduleItem> scheduleItems = scheduleItemMapper.selectListForSerial(scheduleQuery); // 具体业务逻辑 } } 其他参考:如何自定义sql语句:https://doc.oinone.top/backend/4759.html

    2024年5月17日
    2.6K00
  • Oinone环境保护(v5.2.3以上)

    概述 Oinone平台为合作伙伴提供了环境保护功能,以确保在一套环境可以在较为安全前提下修改配置文件,启动多个JVM等部署操作。 本章内容主要介绍与环境保护功能相关的启动参数。 名词解释 本地开发环境:开发人员在本地启动业务工程的环境 公共环境:包含设计器镜像和业务工程的环境 环境保护参数介绍 【注意】参数是加在程序实参 (Program arguments)上,通常可能错误的加在Active Profiles上了 -PenvProtected=${value} 是否启用环境保护,默认为true。 环境保护是通过与最近一次保存在数据库的base_platform_environment表中数据进行比对,并根据每个参数的配置特性进行判断,在启动时将有错误的内容打印在启动日志中,以便于开发者进行问题排查。 除此之外,环境保护功能还提供了一些生产配置的优化建议,开发者可以在启动时关注这些日志,从而对生产环境的配置进行调优。 -PsaveEnvironments=${value} 是否将此次启动的环境参数保存到数据库,默认为true。 在某些特殊情况下,为了避免公共环境中的保护参数发生不必要的变化,我们可以选择不保存此次启动时的配置参数到数据库中,这样就不会影响其他JVM启动时发生校验失败而无法启动的问题。 -PstrictProtected=${value} 是否使用严格校验模式,默认为false 通常我们建议在公共环境启用严格校验模式,这样可以最大程度的保护公共环境的元数据不受其他环境干扰。 PS:在启用严格校验模式时,需避免内外网使用不同连接地址的场景。如无法避免,则无法启用严格校验模式。 常见问题 需要迁移数据库,并更换了数据库连接地址该如何操作? 将原有数据库迁移到新数据库。 修改配置文件中数据库的连接地址。 在启动脚本中增加-PenvProtected=false关闭环境保护。 启动JVM服务可以看到有错误的日志提示,但不会中断本次启动。 移除启动脚本中的-PenvProtected=false或将值改为true,下次启动时将继续进行环境保护检查。 可查看数据库中base_platform_environment表中对应数据库连接配置已发生修改,此时若其他JVM在启动前未正确修改,则无法启动。 本地开发时需要修改Redis连接地址到本地,但希望不影响公共环境的使用该如何操作? PS:由于Redis中的元数据缓存是根据数据库差量进行同步的,此操作会导致公共环境在启动时无法正确刷新Redis中的元数据缓存,需要配合pamirs.distribution.session.allMetaRefresh参数进行操作。如无特殊必要,我们不建议使用该形式进行协同开发,多次修改配置会导致出错的概率增加。 本地环境首次启动时,除了修改Redis相关配置外,还需要配置pamirs.distribution.session.allMetaRefresh=true,将本地新连接的Redis进行初始化。 在本地启动时,增加-PenvProtected=false -PsaveEnvironments=false启动参数,以确保本地启动不会修改公共环境的配置,并且可以正常通过环境保护检测。 本地环境成功启动并正常开发功能后,需要发布到公共环境进行测试时,需要先修改公共环境中业务工程配置pamirs.distribution.session.allMetaRefresh=true后,再启动业务工程。 启动一次业务工程后,将配置还原为pamirs.distribution.session.allMetaRefresh=false。

    2024年10月21日
    2.5K00
  • 首次登录修改密码和自定义密码规则等

    场景描述 在某些场景下,可能需要实现 用户首次登录强制修改密码的功能,或者存在修改平台默认密码等校验规则等需求;本文将讲解不改变平台代码的情况下,如何实现这些功能需求。 首次登录修改密码 方案概述 自定义User增加是否是第一次登录的属性,登录后执行一个扩展点。 判断是否是一次登录,如果是则返回对应的状态码,前端根据状态码重定向到修改密码的页面。修改完成则充值第一次登录的标识。 PS:首次登录的标识平台前端已默认实现 扩展PamirsUser(例如:DemoUser) /** * @author wangxian */ @Model.model(DemoUser.MODEL_MODEL) @Model(displayName = "用户", labelFields = {"nickname"}) @Model.Advanced(index = {"companyId"}) public class DemoUser extends PamirsUser { public static final String MODEL_MODEL = "demo.DemoUser"; @Field.Integer @Field.Advanced(columnDefinition = "bigint DEFAULT '0'") @Field(displayName = "公司ID", invisible = true) private Long companyId; /** * 默认true->1 */ @Field.Boolean @Field.Advanced(columnDefinition = "tinyint(1) DEFAULT '1'") @Field(displayName = "是否首次登录") private Boolean firstLogin; } 定义扩展点接口(实际项目按需要增加和删减接口的定义) import pro.shushi.pamirs.meta.annotation.Ext; import pro.shushi.pamirs.meta.annotation.ExtPoint; import pro.shushi.pamirs.user.api.model.tmodel.PamirsUserTransient; @Ext(PamirsUserTransient.class) public interface PamirsUserTransientExtPoint { @ExtPoint PamirsUserTransient loginAfter(PamirsUserTransient user); @ExtPoint PamirsUserTransient loginCustomAfter(PamirsUserTransient user); @ExtPoint PamirsUserTransient firstResetPasswordAfter(PamirsUserTransient user); @ExtPoint PamirsUserTransient firstResetPasswordBefore(PamirsUserTransient user); @ExtPoint PamirsUserTransient modifyCurrentUserPasswordAfter(PamirsUserTransient user); @ExtPoint PamirsUserTransient modifyCurrentUserPasswordBefore(PamirsUserTransient user); } 编写扩展点实现(例如:DemoUserLoginExtPoint) @Order(0) @Component @Ext(PamirsUserTransient.class) @Slf4j public class DemoUserLoginExtPoint implements PamirsUserTransientExtPoint { @Override @ExtPoint.Implement public PamirsUserTransient loginAfter(PamirsUserTransient user) { return checkFirstLogin(user); } private PamirsUserTransient checkFirstLogin(PamirsUserTransient user) { //首次登录需要修改密码 Long userId = PamirsSession.getUserId(); if (userId == null) { return user; } DemoUser companyUser = new DemoUser().queryById(userId); // 判断用户是否是第一次登录,如果是第一次登录,需要返回错误码,页面重新向登录 Boolean isFirst = companyUser.getFirstLogin(); if (isFirst) { //如果是第一次登录,返回一个标识给前端。 // 首次登录的标识平台已默认实现 user.setBroken(Boolean.TRUE); user.setErrorCode(UserExpEnumerate.USER_FIRST_LOGIN_ERROR.code()); return user; } return user; } @Override public PamirsUserTransient loginCustomAfter(PamirsUserTransient user) { return checkFirstLogin(user); } @Override…

    2024年5月25日
    6.2K00
  • Oinone开发实践-业务实现多租户方案

    总体方案 业务项目中,需要隔离的模型自定义增加租户字段进行数据隔离; 参考了Mybatis-Plus插件的TenantSqlParser进行的JPA实现,使用jsqlparser解析并修改SQL; 实现获取当前用户租户ID,SQL增删改查时处理租户字段,实现租户数据的隔离 参考项目: https://github.com/baomidou/mybatis-plus https://github.com/JSQLParser/JSqlParser 具体实现方式 1、业务上定义两个基础抽象模型包含租户字段 定义包含ID的基础抽象模型,且包含租户字段(如:公司编码, 用其他字段作为租户字段也可以,根据实际业务情况灵活修改)。 @Model.model(XXIdModel.MODEL_MODEL) @Model.Advanced(type = ModelTypeEnum.ABSTRACT) @Model(displayName = “带公司CODE的基础ID抽象模型”, summary = “带公司Code的Id模型”) public abstract class XXIdModel extends IdModel { public static final String MODEL_MODEL = “demo.biz.XXIdModel”; @Field.String @Field(displayName = “所属公司编码”, invisible = true, index = true) private String companyCode; } 定义包含Code的基础抽象模型,且包含租户字段(如:公司编码, 用其他字段作为租户字段也可以,根据实际业务情况灵活修改)。 @Model.model(XXCodeModel.MODEL_MODEL) @Model.Advanced(type = ModelTypeEnum.ABSTRACT) @Model(displayName = “带公司CODE的基础Code抽象模型”, summary = “带公司CODE的Code模型”) public abstract class XXCodeModel extends CodeModel { public static final String MODEL_MODEL = “demo.biz.XXCodeModel”; @Field.String @Field(displayName = “所属公司编码”, invisible = true, index = true) private String companyCode; } 2、业务模块的模型需租户隔离的都是继承上面这两个模型; @Model.model(PetPetCompany.MODEL_MODEL) @Model(displayName = “宠物公司”, labelFields = “name”) public class PetPetCompany extends AbstractCompanyCodeModel { public static final String MODEL_MODEL = “demo.PetPetCompany”; @Field.String @Field(displayName = “名称”) private String name; @Field.Text @Field(displayName = “简介”) private String introduction; } 3、自定义扩展Session,Session中设置租户信息 每次请求多把登录用户所属公司编码(companyCode)放到Session中;Session扩展参考:https://doc.oinone.top/oio4/9295.html 4、定义拦截器Interceptor进行数据隔离 数据创建和查询通过拦截器把Session中的中的公司编码(companyCode)设置到隔离字段中;拦截器的java示例代码参考: package pro.shushi.pamirs.demo.core.interceptor; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.ItemsListVisitor; import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList; import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.insert.Insert; import net.sf.jsqlparser.statement.select.*; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.values.ValuesStatement; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import…

    2024年4月6日
    1.6K00

Leave a Reply

Please Login to Comment