标品实施:从标品构建到定制(扩展)包的开发

总体描述

Oinone有一个非常重要的特性:通过平台承载标准化产品(标品)。针对不同客户的个性化需求,不再直接修改标准产品代码,而是以扩展包的形式进行扩展和定制化,通过继承和重写标准产品的能力来满足客户需求。

本文讲解述怎么通过标品构建扩展工程的过程。

构建标品

按照Oinone的规范构建标品工程

构建扩展包

在定制模块中指定上游模块

上游依赖模块upstreams,模块定义如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Module {

    // 显示名称
    @AliasFor("displayName")
    String value() default "";

    // 依赖模块名列表
    String[] dependencies() default ModuleConstants.MODULE_BASE;

    // 上游模块名列表
    String[] upstreams() default {};

    ……

扩展模块示例

@Component
@Module(
        name = SecondModule.MODULE_NAME,
        displayName = "DEMO扩展",
        version = "1.0.0",
        // 指定上游模块(标品模块,可以为多个)
        upstreams = DemoModule.MODULE_MODULE,
        priority = 1,
        dependencies = {ModuleConstants.MODULE_BASE,
                CommonModule.MODULE_MODULE,
                UserModule.MODULE_MODULE,
                AuthModule.MODULE_MODULE,
                BusinessModule.MODULE_MODULE,
                // 上游模块(标品模块,可以为多个)
                DemoModule.MODULE_MODULE,
        }
)
@Module.module(SecondModule.MODULE_MODULE)
@Module.Advanced(selfBuilt = true, application = true)
@UxHomepage(@UxRoute(model = WorkRecord.MODEL_MODEL))
public class SecondModule implements PamirsModule {

    public static final String MODULE_MODULE = "demo_core_ext";

    public static final String MODULE_NAME = "DemoCoreExt";

    @Override
    public String[] packagePrefix() {
        return new String[]{
                "pro.shushi.pamirs.second"
        };
    }
}

application.yml配置文件

pamirs:
   boot:
      modules:
          .....
          - demo_core  // 加标准工程
          - demo_core_ext

maven配置

父工程依赖

<dependencyManagement>
        <dependencies>
        .....
            <dependency>
                <groupId>pro.shushi.pamirs.demo</groupId>
                 <artifactId>pamirs-demo-api</artifactId>
                 <version>1.0.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>pro.shushi.pamirs.demo</groupId>
                <artifactId>pamirs-demo-core</artifactId>
                 <version>1.0.0-SNAPSHOT</version>
            </dependency>
            .....
  </dependencies>
</dependencyManagement>

api子工程加入依赖

 <dependency>
          <groupId>pro.shushi.pamirs.demo</groupId>
          <artifactId>pamirs-demo-api</artifactId>
</dependency>

boot子工程加入依赖

 <dependency>
          <groupId>pro.shushi.pamirs.demo</groupId>
          <artifactId>pamirs-demo-core</artifactId>
</dependency>

数据库设置

base数据库要跟标品工程一致

注意事项

标品工程的第三方依赖,在扩展工程都要有,否则启动会报错

扩展模块功能开发

  • 菜单扩展
    1、可以按需隐藏标品的菜单;
    2、可以根据扩展包的实际情况增加菜单;
  • 模型扩展
    1、扩展包可继承标品已有模型;

    • 新增字段、覆盖字段,不继承
      2、扩展包可根据实际情况新增自有模型;
  • 函数扩展
    1、扩展包可根据实际情况【覆写】标品中的函数;
    2、扩展包可根据实际情况【新增】自有函数;
    3、扩展包可通过Hook机制实现业务的个性化;
    4、扩展包可根据自身业务情况实现标品中的扩展点;
    5、……

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

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

Like (0)
望闲's avatar望闲数式管理员
Previous 2024年6月1日 am11:40
Next 2024年6月1日 pm6:57

相关推荐

  • 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,将字节直接写入当前…

    2026年7月2日
    12300
  • 如何跳过固定path路径下面所有的按钮权限

    场景: 业务上需要跳过弹窗打开里面的所有按钮权限。 实践: 实现AuthFilterService权限接口。 package pro.shushi.pamirs.top.api.spi; import org.apache.commons.lang3.StringUtils; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import pro.shushi.pamirs.auth.api.spi.AuthFilterService; import pro.shushi.pamirs.boot.web.session.AccessResourceInfoSession; import pro.shushi.pamirs.meta.common.spi.SPI; /** * @author Yexiu at 09:04 on 2024/9/27 */ @Order(88) @Component @SPI.Service public class CustomAuthFilterService implements AuthFilterService { public static final String skipPath = "/top_demo/uiMenuc6238c29bca44250a041691565056a63/ACTION#top.Teacher#uiView2b60cc6daa334c7280cb78207d41addc"; @Override public Boolean isAccessAction(String model, String name) { String path = AccessResourceInfoSession.getInfo().getOriginPath(); if (StringUtils.isNotEmpty(path) && path.startsWith(skipPath)) { //返回true就代表通过验证 return true; } return null; } @Override public Boolean isAccessAction(String path) { if (StringUtils.isNotEmpty(path) && path.startsWith(skipPath)) { //返回true就代表通过验证 return true; } return null; } } 可以看到弹窗下面的按钮都不需要权限控制了。

    2025年3月11日
    1.1K00
  • 如何给角色增加菜单权限

    对接第三方的权限时,第三方传过来菜单项,需要拿着这些菜单在平台这边进行授权,可以使用代码的方式给指定菜单创建权限代码示例: public class demo { @Autowired private PermissionNodeLoader permissionNodeLoader; @Autowired private AuthRbacRolePermissionServiceImpl authRbacRolePermissionService; public void roleAuthorization() { ArrayList<Menu> menus = new ArrayList<>(); menus.add(new Menu().queryOneByWrapper(Pops.<Menu>lambdaQuery() .from(Menu.MODEL_MODEL) .eq(Menu::getName, "uiMenu90dd10ae7cc4459bacd2845754b658a8") .eq(Menu::getModule, TopModule.MODULE_MODULE))); menus.add(new Menu().queryOneByWrapper(Pops.<Menu>lambdaQuery() .from(Menu.MODEL_MODEL) .eq(Menu::getName, "TopMenus_shoppMenu_Shop3Menu_ShopSayHello52eMenu") .eq(Menu::getModule, TopModule.MODULE_MODULE))); //加载指定角色的全部资源权限项 ResourcePermissionNodeLoader loader = permissionNodeLoader.getManagementLoader(); List<PermissionNode> nodes = loader.buildRootPermissions(); List<AuthRbacResourcePermissionItem> authRbacRolePermissionProxies = new ArrayList<>(); //给指定角色创建权限,如果需要多个角色,可以for循环依次执行authRbacRolePermissionService.update(authRbacRolePermissionProxy) AuthRole authRole = new AuthRole().queryOneByWrapper(Pops.<AuthRole>lambdaQuery() .from(AuthRole.MODEL_MODEL) .eq(AuthRole::getCode, "R003") .eq(AuthRole::getName, "R003")); AuthRbacRolePermissionProxy authRbacRolePermissionProxy = new AuthRbacRolePermissionProxy(); AuthRole.transfer(authRole, authRbacRolePermissionProxy); for (PermissionNode node : nodes) { traverse(node, authRbacRolePermissionProxies, menus); } authRbacRolePermissionProxy.setResourcePermissions(authRbacRolePermissionProxies); authRbacRolePermissionService.update(authRbacRolePermissionProxy); } private void traverse(PermissionNode node, List<AuthRbacResourcePermissionItem> authRbacRolePermissionProxies, ArrayList<Menu> menus) { if (node == null) { return; } //按照指定菜单进行过滤,如果不是指定菜单,则设置菜单项不可访问,如果是指定菜单,则设置可访问 Set<Long> menuIds = new HashSet<>(); for (Menu menu : menus) { menuIds.add(menu.getId()); } if (node instanceof MenuPermissionNode) { AuthRbacResourcePermissionItem item = new AuthRbacResourcePermissionItem(); if (menuIds.contains(Long.parseLong(node.getId()))) { item.setCanAccess(Boolean.TRUE); } else { item.setCanAccess(Boolean.FALSE); } item.setCanManagement(node.getCanManagement()); item.setPath(node.getPath()); item.setSubtype(node.getNodeType()); item.setType(AuthEnumerationHelper.getResourceType(node.getNodeType())); item.setDisplayName(node.getDisplayValue()); item.setResourceId(node.getResourceId()); authRbacRolePermissionProxies.add(item); } List<PermissionNode> childNodes = node.getNodes(); if (CollectionUtils.isNotEmpty(childNodes)) { for (PermissionNode child : childNodes) { traverse(child, authRbacRolePermissionProxies, menus); } } } } 执行看效果

    2024年11月14日
    1.2K00
  • 蓝绿发布

    背景 应用程序升级面临最大挑战是新旧业务切换,将软件从测试的最后阶段带到生产环境,同时要保证系统不间断提供服务。长期以来,业务升级渐渐形成了几个发布策略:蓝绿发布、灰度发布和滚动发布,目的是尽可能避免因发布导致的流量丢失或服务不可用问题。 本文主要介绍Oinone平台如何实现蓝绿发布。蓝绿发布:项目逻辑上分为AB组,在项目系统时,首先把A组从负载均衡中摘除,进行新版本的部署。B组仍然继续提供服务。 需求 统一权限统一登录信息不同业务数据 实现方案 首先需要两个环境并统一流量入口,这里使用Nginx配置负载均衡:nginx如何配置后端服务的负载均衡 统一权限配置 在蓝绿环境添加不同的redis前缀 spring: redis: prefix: xxx 在蓝绿环境的修改AuthRedisTemplate Bean,利用setKeySerializer去掉redis前缀。 可以使用@Profile注解指定仅线上环境生效。 @Configuration // @Profile("prod") public class AuthRedisTemplateConfig { @Bean(AuthConstants.REDIS_TEMPLATE_BEAN_NAME) public AuthRedisTemplate<?> authRedisTemplate( RedisConnectionFactory redisConnectionFactory, PamirsStringRedisSerializer pamirsStringRedisSerializer ) { AuthRedisTemplate<Object> template = new AuthRedisTemplate<Object>(redisConnectionFactory, pamirsStringRedisSerializer) { @Override public void afterPropertiesSet() { // 重写 key serializer,去掉前缀隔离 this.setKeySerializer(new PamirsStringRedisSerializer(null)); super.afterPropertiesSet(); } }; return template; } } 统一登录 在蓝绿环境自定义实现pro.shushi.pamirs.user.api.spi.UserCacheApi SPI,去除redis前缀 package pro.shushi.pamirs.top.core.spi; import com.alibaba.fastjson.JSON; import com.google.common.collect.Sets; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.SessionCallback; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import pro.shushi.pamirs.auth.api.cache.redis.AuthRedisTemplate; import pro.shushi.pamirs.meta.api.dto.model.PamirsUserDTO; import pro.shushi.pamirs.meta.api.dto.protocol.PamirsRequestVariables; import pro.shushi.pamirs.meta.api.session.PamirsSession; import pro.shushi.pamirs.meta.common.spi.SPI; import pro.shushi.pamirs.user.api.cache.UserCache; import pro.shushi.pamirs.user.api.configure.UserConfigure; import pro.shushi.pamirs.user.api.spi.UserCacheApi; import java.net.URI; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; @Order(1) @Component @SPI.Service("MyUserCache") public class MyUserCache implements UserCacheApi { private static final Set<String> DEFAULT_FILTER_URIS = Collections.singleton("/pamirs/message"); @Autowired private AuthRedisTemplate redisTemplate; @Override public PamirsUserDTO getSessionUser(String key) { String objectValue = getUserCacheAndRenewed(key); if (StringUtils.isNotBlank(objectValue)) { return JSON.parseObject(objectValue, PamirsUserDTO.class); } return null; } @Override public void setSessionUser(String key, PamirsUserDTO user, Integer expire) { user.setPassword(null); expire = getExpire(expire); redisTemplate.opsForValue().set(key.replace("'", " "), JSON.toJSONString(user), expire, TimeUnit.SECONDS); // 当前的实现是一个user可以在多个客户端登录,需要在管理端修改user权限后强制清除掉该用户已登录的session,所以需要记录uid对应所有已登录的sessionId String userRelSessionKey = UserCache.createUserRelSessionKey(user.getUserId()); redisTemplate.opsForSet().add(userRelSessionKey, key); redisTemplate.expire(userRelSessionKey,…

    2025年9月18日
    99200
  • 查询时自定义排序字段和排序规则

    指定字段排序 平台默认排序字段,参考IdModel,按创建时间和ID倒序(ordering = "createDate DESC, id DESC") 方法1:模型指定排序 模型定义增加排序字段。@Model.Advanced(ordering = "xxxxx DESC, yyyy DESC") @Model.model(PetShop.MODEL_MODEL) @Model(displayName = "宠物店铺",summary="宠物店铺",labelFields ={"shopName"}) @Model.Code(sequence = "DATE_ORDERLY_SEQ",prefix = "P",size=6,step=1,initial = 10000,format = "yyyyMMdd") @Model.Advanced(ordering = "createDate DESC") public class PetShop extends AbstractDemoIdModel { public static final String MODEL_MODEL="demo.PetShop"; // ………… } 方法2:Page查询中可以自定排序规则 API参考 pro.shushi.pamirs.meta.api.dto.condition.Pagination#orderBy public <G, R> Pagination<T> orderBy(SortDirectionEnum direction, Getter<G, R> getter) { if (null == getSort()) { setSort(new Sort()); } getSort().addOrder(direction, getter); return this; } 具体示例 @Function.Advanced(type= FunctionTypeEnum.QUERY) @Function.fun(FunctionConstants.queryPage) @Function(openLevel = {FunctionOpenEnum.API}) public Pagination<PetShop> queryPage(Pagination<PetShop> page, IWrapper<PetShop> queryWrapper){ page.orderBy(SortDirectionEnum.DESC, PetShop::getCreateDate); page = new PetShop().queryPage(page, queryWrapper); return page; } 方法3:查询的wapper中指定 API参考:pro.shushi.pamirs.framework.connectors.data.sql.AbstractWrapper#orderBy @Override public Children orderBy(boolean condition, boolean isAsc, R… columns) { if (ArrayUtils.isEmpty(columns)) { return typedThis; } SqlKeyword mode = isAsc ? ASC : DESC; for (R column : columns) { doIt(condition, ORDER_BY, columnToString(column), mode); } return typedThis; } 具体示例 public List<PetShop> queryList(String name) { List<PetShop> petShops = Models.origin().queryListByWrapper( Pops.<PetShop>lambdaQuery().from(PetShop.MODEL_MODEL) .orderBy(true, true, PetShop::getCreateDate) .orderBy(true, true, PetShop::getId) .like(PetShop::getShopName, name)); return petShops; } 设置查询不排序 方法1:关闭平台默认排序字段,设置模型的ordering,改成:ordering = "1=1" 模型定义增加排序字段。@Model.Advanced(ordering = "1=1") @Model.model(PetShop.MODEL_MODEL) @Model(displayName = "宠物店铺",summary="宠物店铺",labelFields ={"shopName"}) @Model.Code(sequence = "DATE_ORDERLY_SEQ",prefix = "P",size=6,step=1,initial = 10000,format = "yyyyMMdd") @Model.Advanced(ordering =…

    2024年5月25日
    2.6K00

Leave a Reply

Please Login to Comment