Yoga是一套定位于仅提供后台服务的多租户管理平台。平台基于Spring boot+Mybatis+Redis实现,页面采用freemarker后端渲染技术。 本框架的目的用于快速开发轻量级后台服务,比如库存管理、政务OA管理、直播回传管理、订单系统等等,并需要开发与之对应的APP、微信等前端产品。
1、如果下载源码后直接依赖使用,请修改根目录下的budild.gradle文件中的两处:
bootJar {
enabled = false
}
为
bootJar {
enabled = true
}
2、配置文件请复制yoga-business/yoga-admin中的application-example.yml为application.yml
一些基础类库,常用的工具类
- base: BaseCache, BaseController, BaseDto, BaseEnum, BaseService, BaseVo
- data: ApiResult, ApiResults, ChainMap, CommonMessage, CommonPage, MapConverter, PropertiesLoader, ResourceLoader, ResultConstants
- json: DateJsonDeserializer, DateJsonSerializer, DateTimeJsonDeserializer, DateTimeJsonSerializer, DateYMDHMJsonDeserializer, DateYMDHMJsonSerializer, TimeJsonDeserializer, TimeJsonSerializer
- mybatis: IntListTypeHandler, LocalDateTypeHandler, LongArrayTypeHandler, MapperQuery, MyMapper, StringListTypeHandler
- utils: AesUtil, ArrayUtil, AssertUtil, ClassUtil, CryptUtil, DateUtil, DateUtils, DefaultUtil, EnumUtil, FreemarkerUtil, HttpUtils, JsonUtil, MapUtil, NumberUtil, ObjectUtil, PinyinUtil, SqlBuilder, StringUtil, TypeCastUtil
基于AOP的日志库,通过注解方式自动记录业务日志
日志注解:
@Logging(module = ModuleName, description = "添加租户", primaryKeyIndex = -1, excludeArgs = 5, argNames = "租户名称,租户编码,租户描述,模板ID,管理员账号,,管理员昵称,管理员手机号")
public long add(String name, String code, String remark, Long templateId, String username, String password, String nickname, String mobile) throws Exception {
...
日志显示主键查询:
@LoggingPrimary(module = TenantService.ModuleName, name = "租户管理")
public class TenantService extends BaseService implements LoggingPrimaryHandler {
public final static String ModuleName = "gbl_tenant";
@Override
public String getPrimaryInfo(Object primaryId) {
Tenant tenant = tenantMapper.selectByPrimaryKey(primaryId);
if (tenant == null) return null;
return tenant.getName();
}
...
基于Spring扫描的系统配置项管理,通过注解方式自动发现注解
单行(无配置界面)增加设置项:
@Settable(module = WxmpService.ModuleName, key = WxmpService.Key_AppState, name = "微信小程序-小程序发布状态", placeHolder = "developer为开发版;trial为体验版;formal为正式版")
public class WeixinappController extends BaseController {
有配置页面的设置项:
@Settable
public class CaptchaController extends BaseController {
@ApiIgnore
@Settable(module = CaptchaService.ModuleName, key = CaptchaService.Key_Config, name = "通知设置-短信验证码设置")
@GetMapping("")
public String showSetting(ModelMap model, @Valid BaseDto dto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) throw new IllegalArgumentException(bindingResult);
CaptchaSetting setting = captchaService.getSetting(dto.getTid());
if (setting == null) setting = new CaptchaSetting();
if (StringUtil.isBlank(setting.getFormat())) setting.setFormat("您的验证码是:#code#,#time#内有效。如非您本人操作,请忽略本消息。");
model.put("setting", setting);
return "/admin/utility/captcha/setting";
}
...
保存读取配置项:
@Autowired
private SettingService settingService;
public final static String ModuleName = "gcf_weixinapp";
public final static String Key_Setting = "weixinapp.setting";
public final static String Key_AppState = "weixinapp.state";
public SettingConfig getSetting(long tenantId) {
return settingService.get(tenantId, ModuleName, Key_Setting, SettingConfig.class);
}
public void saveSetting(long tenantId, SettingConfig config) {
settingService.save(tenantId, ModuleName, Key_Setting, JSONObject.toJSONString(config), config.getAppId());
}
public String getAppState(long tenantId) {
return settingService.get(tenantId, ModuleName, Key_AppState, "formal");
}
工具模块库
-
aliyun 阿里云身份证识别模块
-
captcha 短信验证码模块,可通过系统配置项设置发送短信模板,以及是否模拟发送(不真实发送,直接返回验证码)
-
district 中国省市区三级地理信息模块
-
feie 飞鹅云打印机对接模块
-
image 基于image magick的图片压缩模块
-
mail 邮件发送模块,通过系统配置项配置发送邮箱信息
-
push 推送框架模块,插件模型,可对接极光等推送
-
qr 二维码模块,生成二维码图形,事件关联二维码等,扫码执行任务
-
quartz 定时器模块,有独立的配置页面
注册定时任务方法:
@PostConstruct public void addQuartz() { quartzService.add(new QuartzTask(SendSubscribeService.class, ModuleName, "发送微信通知", "*/10 * * * * ?")); }
-
sms 短信发送模块,插件模型,可以对接阿里云短信、云片短信平台等
-
uploader 文件上传模块,文件上传建议全部通过该接口,在业务部分只需要记录文件的ID即可
Excel导出模块,基于https://github.com/wenzewoo/ExcelKit,致谢
UEditor网页编辑工具,基于百度UEditor并做了相应适配,致谢
静态资源模块,后端管理系统中的各种静态资源,含:echarts、jquery、pdf.js、zui、zTree以及一些自定义样式定义,致谢各开源机构和个人
在实际部署中,可以将该模块中的静态资源通过独立nginx服务器进行部署,并做好跨域配置,实现动静态分离
基于域名映射的多租户框架模块
-
menu 菜单管理
应用启动时,将自动扫描包内所有的menu.xml文件,然后对所有的菜单进行合并,形成管理后台左侧的菜单输,然后根据每个租户开通的模块,自动筛选具有的菜单进行展示
-
TemplateService 租户模板
通过租户模块可以快速建立租户,租户模板将保存租户具有的模块、自定义菜单,基本配置信息等
-
TenantService 租户管理
可以全手动建立租户或者通过租户模板建立租户,租户建立将确定租户具有的模块、自定菜单、通过租户全局配置可以配置租户的管理网站登录页面样式(更换背景图、标题图、登录框左侧图、网站名称等)。
当导入数据库后第一次启动时,将自动建立0租户(系统租户),只有系统租户中可以增减配置其他租户。
管理账户配置模块
- role 角色管理模块
- 系统基本基于RBAC权限框架,创建租户后,首先建立角色,角色中将配置角色可以访问的模块(管理页面是否显示对应的菜单)以及模块中的各个权限(操作时鉴权,基于Shiro权限框架)
- 可以将角色绑定到部门、职级、用户三个对象上,最终登录用户将拥有上述三者的权限并集,本系统暂不支持权限黑名单机制
- branch 部门管理
- 部门按照树形结构进行管理,无层级限制
- duty 职级管理
- 从设计意图上说,职级和部门属于两个维度的管理体制(横向、纵向)
- 职级存在级别(level),添加职级时按照level进行排序添加,业务系统可使用level进行某些场景的排序(领导排在前面啦)
- user 用户管理
- 用户可以数据部门和职级,也可以不属于,而直接赋予角色。
在yoga-core/yoga-common中对freemarker进行了一系列封装,以减少编写freemarker时的代码量,同时规格化页面中各个页面的显示风格。
管理页面基础框架,包含页面头定义,脚注定义,框架定义等,典型页面使用:
<#include "/paging.component.ftl">
<#include "/form.component.ftl">
<#include "/table.component.ftl">
<#include "/page.component.ftl">
<#include "/input.component.ftl">
<#include "/modal.component.ftl">
<#setting number_format="##0.##">
<@html>
<@head includeDate=true includeUploader=true>
</@head>
<@bodyFrame>
<@crumbRoot name="权限管理" icon="icon-user">
<@crumbItem href="#" name="用户列表" />
</@crumbRoot>
<@bodyContent>
<@panel>
<@panelHeading "用户列表" />
<@panelBody>
<@inlineForm class="margin-b-15">
<@formLabelGroup class="margin-r-15" label="所属部门">
</@formLabelGroup>
<@inputSubmit text="搜索" icon="icon icon-search" class="btn btn-success"/>
<@rightAction>
<@shiro.hasPermission name="admin_user.add" >
<@inputButton text="添加" icon="icon-plus" class="btn btn-primary" onclick="doAdd();" />
</@shiro.hasPermission>
</@rightAction>
</@inlineForm>
<@table>
<@thead>
<@tr>
<@th 7>用户名</@th>
...
<@th 15 true>操作</@th>
</@tr>
</@thead>
<@tbody>
<#list users as operator>
<@tr>
<@td>${operator.username!}</@td>
...
<@td true>
<@shiro.hasPermission name="admin_user.update" >
<a href="javascript:void(0)" onclick="doEdit(${(operator.id?c)!})" class="btn btn-sm btn-info">
<i class="icon icon-edit"></i>
编辑
</a>
</@shiro.hasPermission>
</@td>
</@tr>
</#list>
</@tbody>
</@table>
</@panelBody>
<@panelPageFooter action="/admin/operator/user/list" />
</@panel>
</@bodyContent>
</@bodyFrame>
<script>
...
</script>
</@html>
表单封装,比如inputForm
常用表单控件,包含input和form两种前缀,比如inputDate代表一个单纯的日期输入空间,而formDate则代表Label和控件的单行组合,通过栅格布局实现了各个控件的显示统一
弹出框(比如信息编辑)控件,典型使用:
<@modal title="用户编辑" showId="userAddButton" onOk="saveOperator" width=75>
<@inputHidden name="id" id="edit_id"/>
<div class="form-group">
<label class="col-sm-offset-1 col-sm-1 control-label">用户名:</label>
<div class="col-sm-4">
<@inputText name="username"/>
</div>
<label class="col-sm-1 control-label">真实姓名:</label>
<div class="col-sm-4">
<@inputText name="nickname"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-offset-1 col-sm-1 control-label">手机号:</label>
<div class="col-sm-4">
<@inputText name="mobile"/>
</div>
<label class="col-sm-1 control-label">Email:</label>
<div class="col-sm-4">
<@inputText name="email"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-offset-1 col-sm-1 control-label">设置密码:</label>
<div class="col-sm-4">
<@inputPassword name="password" id="password" />
</div>
<#if branches?? && (((branches?size)!0) gt 0)>
<label class="col-sm-1 control-label">所属部门:</label>
<div class="col-sm-4">
<select class="form-control" name="branchId" id="branchId">
<option value="0">未指定</option>
<#list branches! as root>
<@m1_columns root 0 root_index/>
</#list>
</select>
</div>
</#if>
</div>
<div class="form-group">
<label class="col-sm-offset-1 col-sm-1 control-label">确认密码:</label>
<div class="col-sm-4">
<@inputPassword class="col-sm-4" id="repwd" />
</div>
<#if roles?? && (((branches?size)!0) gt 0)>
<label class="col-sm-1 control-label">所属职级:</label>
<div class="col-sm-4">
<@inputList options=duties! name="dutyId" blank="未指定" blankValue="0"/>
</div>
</#if>
</div>
<div class="form-group">
<label class="col-sm-offset-1 col-sm-1 control-label">赋予角色:</label>
<input type="hidden" name="roleIds" value="0">
<div class="col-sm-8">
<@inputCheckboxGroup options=roles! name="roleIds"/>
</div>
</div>
</@modal>
<script>
function doAdd() {
$("#add_form")[0].reset();
$("#add_form input[name='id']").val(0);
$("#add_modal").modal("show");
}
function doEdit(id) {
$("#add_form")[0].reset();
$.get(
"/admin/operator/user/get.json?id=" + id,
function (data) {
if (parseInt(data.code) < 0) {
alertShow("danger", data.message, 3000);
} else {
$("#add_form input[name='id']").val(id);
$("#add_modal").modal("show");
}
}
);
}
</script>
分页控件,列表分页,固定格式:
<@panelPageFooter action="/admin/operator/user/list" />
只需要替换action中的路径为controller的列表RequstMpping,controller中的ModelMap中需要增加page,形如:
model.put("users", users.getList());
model.put("page", new CommonPage(users));
表格控件,定义了tr th td宏,典型的如:
<@table>
<@thead>
<@tr>
<@th 7>用户名</@th>
...
<@th 15 true>操作</@th>
</@tr>
</@thead>
<@tbody>
<#list users as operator>
<@tr>
<@td>${operator.username!}</@td>
...
<@td true>
<@shiro.hasPermission name="admin_user.update" >
<a href="javascript:void(0)" onclick="doEdit(${(operator.id?c)!})" class="btn btn-sm btn-info">
<i class="icon icon-edit"></i>
编辑
</a>
</@shiro.hasPermission>
</@td>
</@tr>
</#list>
</@tbody>
后端管理网站模块
其实就是yoga-core各个模块的管理网站controller
-
aggregate:信息聚合,通过类接口注册每个模块中哪些信息需要聚合,聚合信息将用于后台管理网站首屏显示以及API中通过接口获取(比如平台订单总量,订单总额等等信息)
-
branch:部门管理,配合yoga-operator
-
duty:职级管理,配合yoga-operator
-
role:角色管理,配合yoga-operator
-
user:用户管理,配合yoga-operator
-
frame:管理网站框架,包含登录页以及iframe模式的主页(左侧菜单,菜单树显示)
-
freemarker:Freemarker标签页组件,实现管理网站脚注正确显示租户的名称,提供以下标签支撑:
<center><strong><@tenantTags tag="footer"/></strong></center> <center><strong><@tenantTags tag="footer"/></strong></center> <@tenantTags tag="resource"/>
特别的,resource前缀用于实现静态资源分离,配合yoga-resource的移除,实现动静分离
-
logging:系统业务日志管理页面,配合yoga-logging
-
quartz:定时任务管理页面,配合yoga-utility
-
setting:系统配置管理页面,配合yoga-setting
-
template:租户模版管理页面,配合yoga-tenant
-
tenant:租户管理管理页面,配合yoga-tenant
-
config:基于Swagger API管理的配置,配合shiro模块实现API页面的鉴权
-
shiro:基于Shiro的后台管理网站权限框架
-
freemarker:针对freemarker使用的shiro扩展,在freemarker页面中使用形如:
<@shiro.hasPermission name="admin_branch.add" > <a href="javascript:void(0)" onclick="doAdd(${(branch.id?c)!})" class="btn btn-sm btn-info"> <i class="icon icon-plus"></i>添加 </a> </@shiro.hasPermission>
页面将自动根据当前账号是否具有admin_branch.add而显示隐藏添加按钮
-
OperatorRealm、OperatorPrincipal、OperatorToken、ShiroXMLReader、ShiroConfiguration:Shiro基础框架
-
OperatorShiroFilter:Shiro全局过滤器,实现通过域名映射到租户(也可以通过http header中tid明确指定租户,但当用户登录后,将优先使用当前登录用户信息中的租户ID),租户ID将在BaseDto中统一传递到controller,在controller中可以通过如下代码获取当前登录用户:
User user = User.getLoginUser();
如果系统中存在多个shiro框架(比如前台一个,后台一个),则可以通过如下代码获取统一的LoginUser:
LoginUser loginUser = (LoginUser) subject.getSession().getAttribute("user");
需要注意,自定义shiro(比如前台)中需要在Realm中配置user属性,形如:
Session session = SecurityUtils.getSubject().getSession(); session.setAttribute("user", user); session.setAttribute("permissions", info.getStringPermissions());
-
MultiRealmAuthenticator:为框架支持多realm shiro提供支撑
-
内容管理模块
基于mongodb进行文档管理的内容管理模块,主要提供栏目结构化(字段设置)和在线文档编辑以及发布模板(不怎么好用)。
- article:基于Mongodb以及mysql字段设置的文档管理
- column:栏目管理,定义栏目树
- comment:评论管理
- property:选项字段选项值管理,比如地区列表
- template:模板管理,每个栏目中的文章结构(包含哪些字段,字段类型)
朋友圈模块
微信小程序接口
积分管理,但是这个积分是针对后台用户的,通过类接口方式注册系统的积分项目
极光推送,实现了yoga-utility中的push,可以按照此例子支持更多的推送方案
阿里云短信发送平台
亿美短信平台
中国移动短信网关平台
云片短信平台
可通过如下maven以来快速使用上边的模块(未列举部分自行替换即可)
implementation 'com.github.yunsean.yoga:yoga-common:1.4.0.1'
implementation 'com.github.yunsean.yoga:yoga-logging:1.4.0.1'
implementation 'com.github.yunsean.yoga:yoga-operator:1.4.0.1'
implementation 'com.github.yunsean.yoga:yoga-resource:1.4.0.1'
implementation 'com.github.yunsean.yoga:yoga-tenant:1.4.0.1'
implementation 'com.github.yunsean.yoga:yoga-utility:1.4.0.1'
implementation 'com.github.yunsean.yoga:yoga-push-jiguang:1.4.0.1'
implementation 'com.github.yunsean.yoga:yoga-sms-aliyun:1.4.0.1'
implementation 'com.github.yunsean.yoga:yoga-sms-yunpian:1.4.0.1'
implementation 'com.github.yunsean.yoga:yoga-admin:1.4.0.1'
master分支为新拆分出的v1.4版本,老版本请切换到v1.0分支。
若有问题,欢迎联系:yunsean@163.com