先看涉及到的表吧,提提神
o_app_api
:微服务接口表o_ui
:前端工程表o_ui_page
:前端页面表o_ui_page_button
:前端页面按钮表o_product
:产品表o_product_menu
:顶级/互斥式产品菜单表o_product_menu_relation
:共存式产品菜单表通常产品应该画个产品效果图出来,便于直观的表达他期望的效果。
原始设计图太丑了,我这直接放出最终效果图
产品树效果
产品菜单树效果
满足产品(・ω・)需求的产品效果有了,剩下的交给苦逼的程序员 {{{ (>_<) }}}
针对需求1,根据产品单独建个表o_product
,记录不同的产品数据。
针对需求2,用菜单来映射功能,建立产品菜单关联表。
针对需求3,产品根据产品类型划分顶级产品和子产品(共存产品、互斥产品),子产品还能继续挂接子产品形成树
针对需求4、5,菜单分为菜单分类和菜单页面,菜单分类下挂接菜单分类或菜单页面,形成菜单树
针对需求6,增加菜单按钮类型,挂接到菜单页面下
产品树很容易解决,产品表加个父级产品代码,返回树形的数据结构就OK了
下面主要讨论产品菜单树的设计和实现
根据需求和效果图,我们将菜单树分为为菜单分类、菜单页面、菜单查询按钮和菜单操作按钮这四种基本单元。
对于前端程序员来说,为了便于开发和前端渲染,菜单树数据应该类似如下机构
[{
"menuName": "产品A",
"menuType": "菜单分类",
"children": [{
"menuName": "菜单分类A",
"menuType": "菜单分类",
"childern": [{
"menuName": "菜单页面A",
"menuType": "菜单页面",
"childern": [
{ "menuName": "菜单按钮A", "menuType": "菜单查询按钮"},
{ "menuName": "菜单按钮B", "menuType": "菜单操作按钮"}
]
}]
}]
}]
菜单分类和菜单页面作为顶部和左侧菜单树的数据来源,页面内部按钮的显示隐藏由菜单按钮是否存在来控制。顶部菜单和左侧导航菜单的显示是所有产品线都公用的部分,我会单独写一篇文章来说明前端如何进行控制。
前端需要的数据结构有了,现在开始讨论菜单树如何进行构建。
传统的方式是提供一个菜单管理界面,手动新增修改删除菜单,手动配置页面的跳转地址,至于页面内的接口的访问地址,基本上不进行存储,因为没有必要,至于接口的访问权限的控制,通常是通过设置一些标志来进行控制。
做过前端的同学应该都清楚,每个前端工程里每个页面都是硬编码写出来的,它是实际存在的,只要输对正确的地址就能打开,前端页面里调用的后端接口也是实际存在的,输对正确的参数,也能正常的调用通过。
对于产品线来说,所有的实际页面和接口,这就是他的全部功能,但是对于产品来说,他需要的只是现阶段满足需求的页面和功能按钮,一些已经过时、或者弃用、或者这个时间段内暂时不能使用的功能页面和按钮,他不需要,小手在菜单管理界面一点删除,就OK了。这样长期以往会导致一个后果,弃用的前端页面和后端接口越来越多,当有一天开始进行系统优化、功能改造时,对苦逼的换了不知几届的程序员来说,想要知道代码里的这些页面和接口是否还在使用,就只能自己翻产品线和翻日志了。
从这开始,产品经理和程序员的矛盾出现了。
为了解决这个矛盾,我设计了微服务接口管理、前端工程管理这两个个功能,这两个功能不是给产品用的,是给程序员自己用的。
微服接口管理:
管理所有微服务的接口信息。这些接口信息都是通过微服务启动后,程序自动获取当前微服务所拥有的接口信息,并自动同步到数据表中 o_app_api
(微服务接口表)
@Order(-98) // 确保最先执行
@Component
public class AppApiApplicationRunner implements ApplicationRunner {
@Value("${spring.application.name:default}")
private String applicationName; // 获取微服务名称,避免不同微服务相同接口的情况
@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
@Autowired
private syncApiClient syncApiClient;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("初始化查询api信息!");
if ("operate".equals(applicationName)) return;
Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
List<OperateAppApi> apis = new ArrayList<>((int) Math.ceil(handlerMethods.size() / 0.75) + 1);
for (Map.Entry<RequestMappingInfo, HandlerMethod> m : handlerMethods.entrySet()) {
RequestMappingInfo info = m.getKey();
HandlerMethod method = m.getValue();
ApiAnnotation apiAnnotation = method.getMethodAnnotation(ApiAnnotation.class);
OperateAppApi api = new OperateAppApi();
api.setApplicationName(applicationName);
api.setControllerName(NCAop.getConciseMethodName(method.getMethod().getDeclaringClass().getName()));
api.setApiUrl(info.getPatternsCondition().getPatterns().iterator().next());
if (apiAnnotation != null) {
api.setModularName(apiAnnotation.modularName());
api.setDescription(apiAnnotation.description());
api.setState(apiAnnotation.state());
} else {
api.setModularName(api.getControllerName());
api.setDescription(api.getApiUrl());
api.setState(1);
}
log.info("api信息 {}", api.toString());
api.setAccessAuth("none");
apis.add(api);
}
syncApiClient.initApiList(apis); // 新增修改入库,旧接口删除标记
log.info("同步接口完成");
}
}
这时候接口数据就有了,新增修改的接口自动入库和修改,已删除的接口,标记成可删除状态,可以在微服务接口管理界面进行删除。
同时,基于这些接口信息,实现了接口访问权限设置和接口访问统计管理功能,这两个功能的实现逻辑单独写文章进行介绍。
前端工程管理:
管理前端工程及工程拥有的菜单和按钮。这个我之前也想弄成程序自动处理收集,方法就是在前端路由生成时,将页面及路由信息主动推送给后端接口,进行入库,但是呢,这样一点安全性都没有,而且没法添加接口信息,所以还是只能老老实实手动维护了。
前端工程和页面信息都手动进行录入,页面下的接口信息调用微服务接口信息,进行选取。
数据库表
o_ui
:前端工程表o_ui_page
:前端页面表o_ui_page_button
:前端页面按钮表产品管理:
现在菜单页面和菜单按钮都有了,产品需要做的事就是,在产品管理页面,按照产品需求,自己定义菜单分类,自己从前端工程里选取前端页面挂接到自己想挂到地方,自己想删哪个删哪个。
然后程序员需要做的事就是,每次功能迭代后,把微服务接口里已经删除的接口信息,手动进行删除,手动维护下前端工程里的信息,然后产品在进行产品线功能的调整。
一个完整的需求到上线逻辑完成:产品定义需求 -> 开发实现需求 -> 开发维护接口和前端工程信息 -> 产品维护产品线信息 -> 上线成功。
产品线功能更灵活了,产品也方便了,程序员加了点微末的工作量,后期系统优化更方便了,同时微服务接口和前端工程管理还直观的体现了系统的整体情况,向上汇报PPT
也有了,皆大欢喜。
产品用途:
作为一个SAAS
系统,产品线根据服务于那个用户体系,分成三个用途,运维/运营、C端用户、B端租户。
在进行角色授权菜单时,角色只能选取当前角色所处用户体系的产品菜单。
产品类型:
用于解决需求3,可以出售或试用产品线中的全部或部分功能。分为顶级产品、共存式产品、排斥式产品。
下面几个规则需要好好理解下
共存式产品和排斥式产品都属于顶级产品的子产品。
共存式产品和排斥式产品都可以作为对方的子产品。
顶级产品和排斥式产品能够手动新增、修改、删除菜单信息,共存式产品只能选取上级产品拥有的菜单信息。
授权时只能选取共存式产品的菜单进行授权。
共存式产品的归属于最近的父级(或者祖父级)顶级产品或者排斥式产品。
这四条规则解决的问题: