yexiu
-
使用Mapper方式进行联表查询
有些业务场景需要查询两张表的数据,这时候就需要用到联表查询。下面将介绍两种方式进行联表查询。 场景:A模型页面,查询条件中包含B模型字段 模型A @Model.model(YesOne.MODEL_MODEL) @Model(displayName = "YesOne", summary = "YesOne") public class YesOne extends IdModel { public static final String MODEL_MODEL = "top.YesOne"; @Field.Integer @Field(displayName = "YesId") private Long yesId; @Field.String @Field(displayName = "名字") private String name; @Field.String @Field(displayName = "科目名字") private String professionalName; @Field(displayName = "关联YesTwo") @Field.many2one @Field.Relation(relationFields = {"yesId"},referenceFields = {"id"}) private YesTwo yesTwo; } 模型B @Model.model(YesTwo.MODEL_MODEL) @Model(displayName = "YesTwo", summary = "YesTwo") public class YesTwo extends IdModel { public static final String MODEL_MODEL = "top.YesTwo"; @Field.Integer @Field(displayName = "科目id") private Long professionalId; @Field.String @Field(displayName = "科目名字") private String professionalName; } 1. 使用in的方式查询 通过B模型的查询条件查询出符合条件的所有数据ID,再根据这个ID去A模型里面查询出所需的数据。 @Function.Advanced(displayName = "查询列表", type = FunctionTypeEnum.QUERY, category = FunctionCategoryEnum.QUERY_PAGE, managed = true) @Function(openLevel = {FunctionOpenEnum.LOCAL, FunctionOpenEnum.REMOTE, FunctionOpenEnum.API}) public Pagination<YesOne> queryPage(Pagination<YesOne> page, IWrapper<YesOne> queryWrapper) { String professionalName = (String) queryWrapper.getQueryData().get("professionalName"); if (StringUtils.isNotBlank(professionalName)) { List<Long> yesTwoId = new YesTwo().queryList(Pops.<YesTwo>lambdaQuery() .from(YesTwo.MODEL_MODEL) .eq(YesTwo::getProfessionalName, professionalName)) .stream().map(YesTwo::getId) .collect(Collectors.toList()); LambdaQueryWrapper<YesOne> wq = Pops.<YesOne>lambdaQuery().from(YesOne.MODEL_MODEL); if (CollectionUtils.isNotEmpty(yesTwoId)) { wq.in(YesOne::getYesId, yesTwoId); } return new YesOne().queryPage(page, wq); } return new YesOne().queryPage(page, queryWrapper); } 2. 使用mapper的方式查询 利用sql的方式去直接查询出结果。使用联表查询的方式查询 @Autowired private YesOneQueryMapper yesOneQueryMapper; @Function.Advanced(displayName = "查询列表", type = FunctionTypeEnum.QUERY, category = FunctionCategoryEnum.QUERY_PAGE, managed = true) @Function(openLevel = {FunctionOpenEnum.LOCAL,…
-
验证服务是否启动成功
验证服务是否启动成功可以调用****/ruok接口,端口是服务器实际运行端口返回imok是启动成功,返回no是启动失败 @Order(Ordered.LOWEST_PRECEDENCE) @RestController public class HealthCheckController implements ApplicationListener<ApplicationStartedEvent> { private static boolean IM_OK = false; private static final String SUCCESS_STRING = "imok"; private static final String FAILURE_STRING = "no"; @GetMapping("/ruok") public String ruok() { if (IM_OK) { return SUCCESS_STRING; } return FAILURE_STRING; } @Override public void onApplicationEvent(ApplicationStartedEvent event) { IM_OK = true; } public static boolean imok() { return IM_OK; } }
-
调试工具在业务场景的使用-后端
这篇文档主要介绍在业务场景中,使用调试工具解决后端问题的思路。 调试工具的使用见文档:问题排查调试工具使用手册 调试工具的页面内容介绍:Oinone平台可视化调试工具 例1:模型配置不存在 现象:点击详情报模型配置不存在错误 排查路径: 将报错请求复制到接口调试出,查看调试信息 在调试信息页面,可以看到全部堆栈,利用堆栈信息找报错问题。 可以看到执行了StudentAction的queryOne方法在42行有问题 检查代码发现传进queryList的wrapper参数没有指定模型编码。添加模型编码问题解决 例2:无权限问题排查 现象:用户前端自定义跳转工作流审批页面,提示无权限 排查路径: 将报错请求复制到接口调试处,查看调试信息 查看debug信息中权限上下文中角色携带的权限是否正确。复制debug信息中的path路径,去权限上下文中搜索查看该路径下所有的权限。 根据上面的path路径搜索权限信息: ~~~ "getRoleActionPermissionsByViewAction:workbench.WorkBenchWorkflowUserTaskActive:WorkflowMenus_WorkBenchMenu_ActiveUserTaskMenu": { "630732547466232342": { "/workflow/WorkflowMenus_WorkBenchMenu_ActiveUserTaskMenu/ACTION#workbench.WorkBenchWorkflowUserTaskActive#workflow_write/ACTION#workflow.WorkflowUserTask#workflow_writeturnon": 1, "/workflow/WorkflowMenus_WorkBenchMenu_ActiveUserTaskMenu/ACTION#workbench.WorkBenchWorkflowUserTaskActive#workflow_wait/ACTION#workflow.WorkflowUserTask#workflow_agree": 1, } }, ~~~ 参数介绍: 630732547466232342:角色id为630732547466232342拥有的所有权限信息 /workflow/WorkflowMenus_WorkBenchMenu_ActiveUserTaskMenu:path路径 /ACTION#workbench.WorkBenchWorkflowUserTaskActive#workflow_write:此path路径下面的ACTION,模型为workbench.WorkBenchWorkflowUserTaskActive的workflow_write动作。 对比无权限页面和以上参数是否对应。可在页面url上查看模型,动作。常见问题有模型不匹配(更换为正常有权限的模型)、角色下无动作权限。 经查看调试信息发现,设置的该角色下并无所需动作的权限信息,考虑由于前端自定义跳转页面没有配置sessionPath所致。 由于5.0版本权限是根据路径进行鉴权的,请求载荷中variables需要携带path路径。 如果是用户自定义跳转页面,需要配置sessionPath:/workflow/WorkflowMenus_WorkBenchMenu_ActiveUserTaskMenu,值为url中的path路径。
-
使用GraphQL生成API文档
GraphiQL 是一个非常流行的交互式开发环境(IDE),专门用于浏览、编写和测试 GraphQL 查询。它不仅可以帮助你查询 API,还可以自动生成文档。以下是如何使用 GraphiQL 的具体步骤: 仓库地址:https://github.com/anvilco/spectaql?tab=readme-ov-file#yaml-options 一、使用GraphiQL 工具生成API文档。 1. GraphiQL 安装与配置 方式 : 本地或全局安装 GraphiQL 如果你的 GraphQL API 服务器没有内置 GraphiQL,你可以使用独立的 GraphiQL 框架或包。 全局安装 GraphiQL如果你想在本地环境使用 GraphiQL,你可以通过 npm 或 yarn 安装:如果下载不成功可以使用淘宝镜像源 npm install -g graphiql 通过 npm 或 Yarn 安装为开发依赖你也可以将 GraphiQL 作为开发依赖安装到项目中: npm install graphiql 生成您的文档! npx spectaql config.yml 运行此命令你需要一份config.yml文件。具体使用参考https://github.com/anvilco/spectaql?tab=readme-ov-file#yaml-options 2. 使用JSON格式生成文档 生成或导出 schema 文件 自动生成 schema如果你使用的是 Java 类和注解方式定义的 GraphQL API(使用 @GraphQLQuery 等注解),GraphQL schema 通常在运行时生成。你可以使用 Spring Boot 启动后访问 GraphQL endpoint 来手动导出 schema。 访问 GraphQL Endpoint:在应用运行时,GraphQL API 通常暴露在 /graphql 路径下。你可以通过 introspection 查询导出完整的 GraphQL schema。 下面是一个 introspection 查询的示例,它可以帮助你获取 schema:这个查询可根据文档需要动态调整。查询http://127.0.0.1:8091/pamirs/graphql,注意保证工程yml配置文件pamirs.framework.gateway.show-doc: true为开启状态。 query IntrospectionQuery { __schema { queryType { …FullType } mutationType { …FullType } subscriptionType { name } types { …FullType } directives { name description locations args { …InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { …InputValue } type { …TypeRef } isDeprecated deprecationReason } inputFields { …InputValue } interfaces { …TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { …TypeRef } } fragment InputValue on __InputValue { name description type { …TypeRef } defaultValue } fragment…
-
Excel添加水印功能
实现ExcelWriteHandlerExtendApi接口从而实现对Excel增加复杂功能的操作。如添加水印。具体实现请自行百度 /** * 根据上下文判断是否执行 * * @param context Excel定义上下文 * @return 是否执行该扩展 */ boolean match(ExcelDefinitionContext context); /** * 构建出一个WriteWorkbook对象,即一个工作簿对象,对应的是一个Excel文件; * * @param builder 可用于设置inMemory=true实现复杂功能(如添加水印) */ default void extendBuilder(ExcelWriterBuilder builder) { } 例:Excel添加水印 本例参考文章:Java使用EasyExcel导出添加水印 实现ExcelWriteHandlerExtendApi接口,并添加@Component注解 添加依赖包 <!– eaysexcel –> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.2</version> </dependency> <!– poi 添加水印 –> <dependency> <groupId>org.apache.poi</groupId> <artifactId>ooxml-schemas</artifactId> <version>1.4</version> </dependency> <!– 使用了hutool的工具类 –> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.20</version> </dependency> package pro.shushi.pamirs.top.core.temp; import cn.hutool.core.img.ImgUtil; import com.alibaba.excel.write.builder.ExcelWriterBuilder; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; import org.apache.poi.openxml4j.opc.PackagePartName; import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.openxml4j.opc.TargetMode; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFPictureData; import org.apache.poi.xssf.usermodel.XSSFRelation; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Component; import pro.shushi.pamirs.file.api.context.ExcelDefinitionContext; import pro.shushi.pamirs.file.api.easyexcel.ExcelWriteHandlerExtendApi; import java.awt.*; import java.awt.image.BufferedImage; @Component public class CustomWaterMarkHandler implements ExcelWriteHandlerExtendApi { private final WaterMark watermark; public CustomWaterMarkHandler() { this.watermark = new WaterMark().setContent("ABC"); } @Override public void extendBuilder(ExcelWriterBuilder builder) { builder.inMemory(true); } @Override public boolean match(ExcelDefinitionContext context) { return DemoTemplate.TEMPLATE_NAME.equals(context.getName()); } @Override public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { try { BufferedImage bufferedImage = createWatermarkImage(); setWaterMarkToExcel((XSSFWorkbook) writeWorkbookHolder.getWorkbook(), bufferedImage); } catch (Exception e) { throw new RuntimeException("添加水印出错",e); } } private BufferedImage createWatermarkImage() { final Font font = watermark.getFont(); final int width = watermark.getWidth(); final int height = watermark.getHeight(); String[] textArray…
-
序列化工具使用问题
后端使用的JSON序列化JsonUtils.toJSONString(nodes);前端使用的JSON序列化PamirsJsonUtils.toJSONString(nodes, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteDateUseDateFormat, SerializerFeature.BrowserCompatible);注意:用什么工具序列化,就用什么工具反序列化、parse 的时候,要保持一致, 使用PamirsJsonUtils序列化工具主要是为了解决枚举取值错误的问题以及id精度丢失问题。当出现这两个问题的时候,需要使用PamirsJsonUtils 进行序列化。
-
保存多值字段SQL执行报错
定义多值类型时,字段类型应该设置为List类型。 @Field.String @Field(displayName ="经费证明", multi = true, serialize = Field.serialize.JSON) private List<String> matchFund; 场景复现
-
函数如何跳过权限拦截
跳过登录直接调用接口 示例: 跳过queryTea的权限验证 @Action(displayName = "queryTea", bindingType = ViewTypeEnum.FORM) @Action.Advanced(type = FunctionTypeEnum.UPDATE) public Teacher queryTea(Teacher data) { } 在yaml文件里面配置上该函数的namespace(模型编码)以及函数名字 pamirs: auth: fun-filter: – namespace: user.PamirsUserTransient fun: login #登录 – namespace: top.Teacher fun: queryTea 不跳过登录直接调用接口 示例: 在yaml文件里面配置上该函数的namespace(模型编码)以及函数名字 pamirs: auth: fun-filter-only-login: #登录后不再校验该函数的权限 – namespace: top.Teacher fun: queryTea 按包设置权限过滤 如何批量跳过权限验证?以上两种方式提供了在yml文件里面配置权限过滤的方式,但如果需要大量过滤权限,配置就变得很繁琐,所以下面主要介绍通过代码扩展的方式去控制权限。 示例: 以下示例通过控制包路径来跳过权限。 继承pro.shushi.pamirs.auth.api.spi.AuthFilterService接口 import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Order(88) @Component public class CustomAuthFilterService implements AuthFilterService { public static final String skipClass = "pro.shushi.pamirs.top.core.action"; @Override public Boolean isAccessAction(String model, String name) { //从缓存中取函数 Action cacheAction = PamirsSession.getContext().getExtendCache(ActionCacheApi.class).get(model, name); if (cacheAction instanceof ServerAction) { ServerAction serverAction = (ServerAction) cacheAction; Function function = PamirsSession.getContext().getFunction(serverAction.getModel(), serverAction.getFun()); String clazz = function.getClazz(); //返回true就代表通过验证 if (clazz != null && clazz.startsWith(skipClass)) { return true; } } return null; } } 请求pro.shushi.pamirs.top.core.action路径下的动作可以通过验证。
-
Function、Action函数使用规范
Function的定义需要严格遵循Oinone的规范, @Action是指页面上有按钮展示的方法,所有@Action注解的方法都是需要在权限处设置权限才能访问的。Action背后都对应一个Function。 @Function是Oinone的可管理的执行逻辑,是无处不在的。 如果只是查询,不需要在页面有按钮,定义为@Function就可以了。 覆写常用默认数据管理器定义标准 @Action.Advanced(name = FunctionConstants.create, managed = true)//默认取的是方法名 @Action(displayName = "确定", summary = "添加", bindingType = ViewTypeEnum.FORM) public AuthRole create(AuthRole data) { @Action.Advanced(type = FunctionTypeEnum.UPDATE, managed = true) @Action(displayName = "更新", label = "确定", summary = "修改", bindingType = ViewTypeEnum.FORM) public AuthRole update(AuthRole data) { @Action.Advanced(type = FunctionTypeEnum.DELETE, managed = true) @Action(displayName = "删除", label = "删除", contextType = ActionContextTypeEnum.SINGLE_AND_BATCH) @Function.fun(FunctionConstant.deleteWithFieldBatch) public List<AuthRole> delete(List<AuthRoe> dataList) { @Function.Advanced(displayName = "查询角色列表", type = FunctionTypeEnum.QUERY, category = FunctionCategoryEnum.QUERY_PAGE, managed = true) @Function(openLevel = {FunctionOpenEnum.LOCAL, FunctionOpenEnum.REMOTE, FunctionOpenEnum.API}) public Pagination<AuthRole> queryPage(Pagination<AuthRole> page, IWrapper<AuthRole> queryWrapper) {//注意方法名和入参名称必须和平台保持一致 @Function.Advanced(displayName = "查询指定角色", type = FunctionTypeEnum.QUERY, category = FunctionCategoryEnum.QUERY_ONE, managed = true) @Function.fun(FunctionConstants.queryByEntity) @Function(openLevel = {FunctionOpenEnum.LOCAL, FunctionOpenEnum.REMOTE, FunctionOpenEnum.API}) public AuthRole queryOne(AuthRole query) {//注意方法名和入参名称必须和平台保持一致 自定义函数定义标准 @Action(displayName = "启用") @Action.Advanced(type = FunctionTypeEnum.UPDATE) public Teacher dataStatus(Teacher data) { } @Function @Function.Advanced(displayName = "构造", type = FunctionTypeEnum.QUERY) public Teacher constructAll(Teacher data) { } 注意事项: 覆写常用默认数据管理器Function定义需要严格按照以上函数定义,包括出入参名字定义、注解定义。定义错误会导致gql请求报错或者找不到函数。 定义@Action或者@Function时,函数出入参必须是当前类注解定义的@Model.model()的模型,或者被该模型字段全包含的的模型,比如它的父模型。 页面调用使用的@Action或者@Function方法,出入参必须是oinone的对象,且不能是基础的java类型,因为oinone的对象有元数据信息,这样才能完成前后端之间的自动交互 managed = true定义当前函数为数据管理函数。它只有在重写平台默认数据管理器时需要使用。 @Function.fun()代表定义函数编码,不可更改,默认与方法名称相同。同一个模型Action内不允许有两个相同的函数编码。 不要使用set、get、unset作为函数方法名的开头,不要使用toString作为函数方法名。 传输模型没有默认的数据管理器,所以不能定义数据管理函数。 @Action和@Function注解使用约定 重写内置数据管理器动作和函数的,应与平台注册方式完全保持一致。以下属性可根据需要进行修改:(必须) @Function.Advanced#displayName @Function#openLevel 自定义方法不要与内置数据管理器中定义的动作和函数重名。(必须) @Action和@Function注解不要混合使用。(自定义方法必须) 如无特殊必要,请不要使用如下属性修改函数定义:(自定义方法必须) @Function#name @Function.fun#value @Function.Advanced#managed @Function.Advanced#builtin @Function.Advanced#group @Function.Advanced#version @Action.Advanced#name @Action.Advanced#args @Action.Advanced#managed @Action.Advanced#language 自定义方法在选择注册动作或函数时,应按照如下规则进行判断:(必须) 若该方法通过用户行为触发的,应注册为动作。 若该方法通过“入口”进行控制的,应注册为函数。 自定义方法若注册为动作时,应按照如下规则进行定义: 使用@Action.Advanced#type属性定义函数类型,默认为UPDATE。混合操作的动作应明确列出所有类型。(必须) 使用@Action#displayName属性定义动作功能名称。如无特殊必要,同一模型下的所有动作名称不要重复。页面展示名称重复的,可使用@Action#label属性定义展示名称。(必须) 使用@Action#summary属性定义动作功能简要描述。 使用@Action#contextType属性定义动作上下文类型,默认为SINGLE。(必须) 使用@Action#bindingType属性定义动作所在视图类型,默认为TABLE。(必须) 自定义方法若注册为函数时,应按照如下规则进行定义: 使用@Function.Advanced#type属性定义函数类型,默认为UPDATE。混合操作的函数应明确列出所有类型。(必须)…
-
推送自定义消息
项目中添加消息依赖 pro.shushi.pamirs.core pamirs-message-api 调用pro.shushi.pamirs.message.engine.message.MessageSender#sendSystemMail发送系统消息示例: @Action(displayName = "发送消息") public Student sendMessage(Student data){ MessageSender mailSender = (MessageSender) MessageEngine.get(MessageEngineTypeEnum.MAIL_SEND).get(null); String content = "发送自定义消息"; String subject = null; List<Long> userIds = new ArrayList<>(); userIds.add(PamirsSession.getUserId()); PamirsMessage message = new PamirsMessage() .setName(subject) .setSubject(subject) .setBody(content) .setMessageType(MessageTypeEnum.NOTIFICATION); List<PamirsMessage> messages = new ArrayList<>(); messages.add(message); SystemMessage systemMessage = new SystemMessage(); systemMessage.setPartners(userIds.stream().map(i -> (PamirsUser) new PamirsUser().setId(i)).collect(Collectors.toList())) .setType(MessageGroupTypeEnum.SYSTEM_MAIL) .setMessages(messages); mailSender.sendSystemMail(systemMessage); return data; }