1 jfinal cms简介
1 jfinal cms简介
jfinal cms,采用了简洁强大的JFinal作为web框架,模板引擎用的是beetl,数据库用mysql,前端bootstrap框架。 支持多站点、oauth2认证、帐号注册、密码加密、评论及回复,消息提示,网站访问量统计,文章评论数和浏览量统计,回复管理,支持权限管理。
后台模块包含:栏目管理,栏目公告,栏目滚动图片,文章管理,回复管理,意见反馈,我的相册,相册管理,图片管理,专辑管理、视频管理、缓存更新,友情链接,访问统计,联系人管理,模板管理,组织机构管理,用户管理,角色管理,菜单管理,数据字典管理,站点管理。
建站截图:
2 jfinal cms部署与配置
2.1 maven方式部署配置
maven部署方式的好处在于不依赖于任何IDE,并且跨平台。
1) 下载项目源代码,安装jdk、maven、mysql。
2) 在项目目录下运行mvn install,提示BUILD SUCCESS即可。
3) 创建mysql用户和数据库,运行/jfinal_cms/sql下对应jfinal_cms_v4.sql。
4) 数据库配置文件:/jfinal_cms/src/main/resources/conf/db.properties,修改相应配置即可。
db_type=mysql
mysql.jdbcUrl =jdbc:mysql://127.0.0.1:3306/jfinal_cms2?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
mysql.user = root
mysql.password = 123456
mysql.driverClass = com.mysql.jdbc.Driver
5) 运行:mvn tomcat:run。看到“系统启动完成”说明运行成功。
6) 网站效果访问:http://127.0.0.1/jfinal_cms。
7) 管理地址:http://${ip:port}/${project_name}/admin,管理账号: admin/admin123
8) 各个模板的切换已通过系统中“站点管理”模块进行操作。站点管理是通过域名解析实现各个模板的对应。
2.2 eclipse部署配置
因为java开发,用eclipse的比较多~这里再介绍一下eclipse导入,部署和配置。
2.2.1 获取源码地址
源码:http://git.oschina.net/flyfox/jfinal_cms复制git地址。
2.2.2 下载git源码
然后我们打开eclipse,导入git项目。(这里也可以用git工具先将源码下载下来,或者直接下载zip包)
选择git导入:
点击URI导入方式:
输入复制的git地址:
选择下载master主分支:
选择导入到本地的目录:
点击下一步获取git源代码。
2.2.3 构建maven项目
如果git获取后,我们选择了作为一个普通项目导入,那么我们需要将其转换为maven项目,项目上面邮件选择Configure->Convert to MavenProject:
如果我们通过其他git工具或者下载zip,那么我们需要导入maven项目
选择项目路径即可:
这样我们项目就导入完成了。
2.2.4 运行项目代码
到这里我们需要安装好mysql,运行完初始化脚本,并且配置好数据库连接串以及账号密码。这些上面maven方式部署有介绍,就不再重复了。
现在我们就可以进行maven项目构建了,运行maveninstall,安装,并下载依赖包:
如下提示说明构建成功
[INFO]------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO]------------------------------------------------------------------------
[INFO] Total time: 22.565s
[INFO] Finished at: Sat Jun 11 07:41:04 GMT 2016
[INFO] Final Memory: 6M/12M
[INFO]------------------------------------------------------------------------
然后我们运行项目,项目右键,Run As-> Maven build...
然后运行tomcat:run,点击运行按钮:
看到如下提示,并且没有报错,说明启动成功。
##################################
############系统启动完成##########
##################################
2.2.5 项目访问
网站效果访问:http://127.0.0.1/jfinal_cms/
管理地址:http://127.0.0.1/jfinal_cms/admin,管理账号: admin/admin123
到这里jfinal cms部署就大功告成了~!~
2.3 实时编译源码配置
上面部署方式有一个小问题就是,每次修改都需要进行maven install后,再maven tomcat:run才能看到修改后的效果。可以进行如下配置解决这个问题。
修改pom,删除warSourceDirectory配置。
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>tomcat-maven-plugin</artifactId>
<version>1.1</version>
<configuration>
<port>80</port>
<path>/${project.artifactId}</path>
<warSourceDirectory>${basedir}/target/${artifactId}</warSourceDirectory>
</configuration>
</plugin>
项目右键,选择构建路径(BuildPath->Configure Build Path...):
删除源码路径,重新配置,再修改默认输出路径(Default output folder):
这是再运行就可以进行实时编辑查看了。
2.4 其他配置说明
1) 如需要oauth2的,设置src/conf/oauth.properties
2) 如果只使用单站点,可以将sites.properties文件中SITE.MULTI.FLAG = true改为false。然后通过config.properties的ATTR.PATH_PC=/template/mtg配置模板。
3 相关技术说明
3.1 java框架jfinal
jfinal官网:http://www.jfinal.com/
下面我在网上截了一张jfinal架构图,个人觉得梳理的很清楚。jfina官网上面的文档在使用方面介绍的简单明了,上手很容易,个人感觉没有必要在进行其他介绍了。
3.2 java模板beetl
beetl文档:http://ibeetl.com/guide/beetl.html
beetl的有点和好处可以看上面文档第一章节,作者介绍的已经很清楚了。详细介绍大家可以看上面的文档,我下面制作一些简单介绍。
l 变量引用
变量引用与其他模板相同,可以通过${name}调用。
这里要特殊说明的是beetl支持获取map属性,即调用get(Objectkey)。如果对象既有具体属性getName(),又有get(Object key),则以具体属性优先级高。如${user.name}先进行调用getName()方法如果没有再调用get(Objectkey)方法。
l list循环
代码片段如下:
总共 ${list.~size}
<%
for(user inlist){
%>
hello,${user.name};
<%}%>
当前页${pageMap['page']},总共${pageMap["total"]}
l 函数调用
可以直接调用registerFunctionPackage(namespace,yourJavaObject),这时候yourJavaObject里的所有public方法都将注册为Beetl方法,方法名是namespace+"."+方法名
jfinalcms中有许多方法和前台接口都是用了这种方式,所以这里也别说明一下。后续会进行详细介绍。
l 其他
剩下的大家就参考项目代码进行理解就可以了~如果不明白可以看beetl官方文档。
4 jfinal cms技术
源码依赖于作者的其他两个开源项目jflyfox_base和jflyfox_jfinal两个项目,相关介绍在jfinal cms项目依赖章节有所介绍。
4.1 项目目录结构介绍
首先我们先来熟悉一下项目的目录结构,以下为该项目目录结构说明,java代码:
项目文件目录:
4.2 后台模块开发
4.2.1 java代码
首先后台代码主要分为Controller,Model和Service。Controller负责参数传递和页面跳转;Model负责数据承载;Service负责复杂业务处理。
这里我们用联系人模块进行简单讲解。
下面是controller示例代码,首先需要继承BaseProjectController类,这样方便项目统一扩展,现在里面已经有一些公共方法了,请自行查看源码。
页面跳转主要包含list(列表页)、add(添加页)、view(查看页)、delete(删除数据)、edit(编辑页)和save(保存方法)。这几个是基本操作,常规增删改查均以此规范进行编写。Controller遵循jfinal设置规则,并加入了注解功能。以下列表访问地址就是/admin/contact/list;编辑页面地址就是/admin/contact/edit/${id}。其他地址不再列举。
以下黄色背景字体需要特别注意。
packagecom.jflyfox.modules.admin.contact;
importcom.jfinal.plugin.activerecord.Page;
importcom.jflyfox.component.base.BaseProjectController;
importcom.jflyfox.jfinal.component.annotation.ControllerBind;
importcom.jflyfox.jfinal.component.db.SQLUtils;
importcom.jflyfox.util.StrUtils;
/**
* 联系人管理
*
* @author flyfox 2014-2-11
*/
@ControllerBind(controllerKey= "/admin/contact")
publicclass ContactController extendsBaseProjectController {
privatestaticfinal String path = "/pages/admin/contact/contact_";
publicvoid list() {
TbContactmodel = getModelByAttr(TbContact.class);
// 这里是拼接查询sql并且获取前台传递的参数
SQLUtilssql = new SQLUtils(" from tb_contact t where 1=1 ");
if (model.getAttrValues().length != 0){
sql.setAlias("t");
sql.whereLike("name", model.getStr("name"));
sql.whereEquals("type", model.getStr("type"));
}
// 这里是统一做了排序功能
StringorderBy = getBaseForm().getOrderBy();
if (StrUtils.isEmpty(orderBy)) {
sql.append(" order by id desc ");
} else {
sql.append(" order by ").append(orderBy);
}
Page<TbContact>page = TbContact.dao.paginate(getPaginator(), "select t.* ", //
sql.toString().toString());
// 下拉框
setAttr("page", page);
setAttr("attr", model);
render(path + "list.html");
}
publicvoid add() {
render(path + "add.html");
}
publicvoid view() {
TbContactmodel = TbContact.dao.findById(getParaToInt());
setAttr("model", model);
render(path + "view.html");
}
publicvoid delete() {
TbContactmodel = new TbContact();
Integeruserid= getSessionUser().getUserID();
Stringnow = getNow();
model.put("update_id", userid);
model.put("update_time", now);
model.deleteById(getParaToInt());
list();
}
publicvoid edit() {
TbContactmodel = TbContact.dao.findById(getParaToInt());
setAttr("model", model);
render(path + "edit.html");
}
publicvoid save() {
Integerpid = getParaToInt();
TbContactmodel = getModel(TbContact.class);
Integeruserid= getSessionUser().getUserID();
Stringnow = getNow();
// 这两个字段设置为了日志收集
model.put("update_id", userid);
model.put("update_time", now);
if (pid != null && pid >0) { // 更新
model.update();
} else { // 新增
model.remove("id");
model.put("create_id", userid);
model.put("create_time", now);
model.save();
}
renderMessage("保存成功");
}
}
下面Model示例代码,Model需要继承BaseProjectModel,方便业务扩展。里面已经加入了缓存、日志等扩展功能,请自行查看源码。jfinal Model可以不添加get、set方法,这里添加主要是为了代码规范。Model遵循jfinal设置规则,并加入了注解功能。通过ModelBind可以直接设置对应的表和主键,主键默认是ID。
get、set方法可以通过作者的AutoCreate组件自动生成。这里不再做多介绍。
packagecom.jflyfox.modules.admin.contact;
importcom.jflyfox.component.base.BaseProjectModel;
importcom.jflyfox.jfinal.component.annotation.ModelBind;
@ModelBind(table= "tb_contact")
publicclass TbContact extendsBaseProjectModel<TbContact> {
privatestaticfinallongserialVersionUID = 1L;
publicstaticfinal TbContact dao = new TbContact();
// columns START
private String ID = "id"; // 主键
private String NAME = "name"; // 姓名
public TbContactsetId(java.lang.Integer value) {
set(ID,value);
returnthis;
}
public java.lang.Integer getId() {
return get(ID);
}
public TbContact setName(java.lang.String value) {
set(NAME, value);
returnthis;
}
public java.lang.String getName() {
return get(NAME);
}
……………………………………
}
Service集成BaseService,主要是Controller中复杂业务处理,将Controller中大量代码或者重复代码进行封装,统一调用。由于联系人模块相对简单,这里就没有Service代码了。
4.2.2 页面代码
后台模板主要是增删改查,jfinalcms使用beetl模板,这里就不多介绍了。
页面主要以${module}_名开头,包含add(添加)、edit(编辑)、list(列表)、view(查看)几个页面。各个页面页格式相对统一,可以进行参考。
以下为add页面代码,添加页面可以与edit页面公用~只是传递参数均为空。
<%
include("contact_edit.html"){}
%>
以下为edit页面代码,首先edit页面采用了beetl安全模式,保证参数为空不报异常,这样能够让add很好的引用。其次后端页面都采用了公共的头部标签head.html,列表和查看页面也是如此,不在赘述。
在编辑页面对js验证进行了封装,只需要添加valid和validname属性就可以实现js验证功能,提交时调用 validForm()方法即可。
<%
DIRECTIVE SAFE_OUTPUT_OPEN;
var headContent = {
include("/pages/template/head.html"){}
%>
<script type="text/javascript">
var oper= {
save:function(id){
if(!validForm()) {
returnfalse;
}
id =id || '0';
var url = 'admin/contact/save/'+id;
form1.action= url;
form1.submit();
returntrue;
}
};
</script>
<%
};
var bodyContent = {
%>
<form name="form1"action="" method="post"class="form-horizontal" role="form">
<input type="hidden"name="model.id" value="${model.id}"/>
<table class="table">
<%// 列表头部 %>
<tr>
<td>姓名</td>
<td>
<input class="form-control"type="text" name="model.name"value="${model.name}"
valid="vtext" validname="名称" />
</td>
</tr>
………………………………
<tr>
<td>说明</td>
<td>
<textarea class="form-control"rows="3" cols="30"name="model.remark">${model.remark}</textarea>
</td>
</tr>
</table>
<div style="height: 50px;clear: both;"> </div>
<nav class="navbarnavbar-default navbar-fixed-bottom">
<div class="container" style="padding: 5px 0px 5px 0px;text-align: center;">
<button class="btnbtn-primary" onclick="returnoper.save(${model.id!'0'});">保 存</button>
<button class="btnbtn-default" onclick="closeIframe();returnfalse;">关 闭</button>
</div>
</nav>
</form>
<%}; %>
<%layout("/pages/template/_layout.html",{head:headContent,body:bodyContent}){%>
DIRECTIVE SAFE_OUTPUT_CLOSE;
<%}%>
下面是列表页代码, 方法调用以oper对象调用形式封装。增加、修改、查看页面以弹框形式展示,这样可以很好的保存列表页查询条件。查看页面关闭不会刷新列表,添加和修改页面关闭后均会刷新列表,均已封装为Iframe方法。
paginator属性为列表分页查询调用方法。
showMenu方法为菜单选中展现,具体参数在菜单管理进行配置。
menu.html为菜单栏,各页面直接引用即可。paginator.html为分页栏,固定获取page参数进行处理。
列表头部设置设置class="sorting"为支持排序功能,name字段为传到后台的参数值。
<%
varheadContent = {
include("/pages/template/head.html"){}
%>
<script type="text/javascript">
var oper;
jQuery(function($) {
// 页面方法
oper = {
width: 400,
height: 430,
form :document.form1,
list :function() {
var url = 'admin/contact/list';
this.form.action = url;
this.form.submit();
},
view :function(id) {
var url = 'admin/contact/view/'+id;
var title = '查看联系人';
Iframe(url,this.width, this.height, title, false, false, false, EmptyFunc);
},
add : function() {
var url = 'admin/contact/add';
var title = '添加联系人';
Iframe(url,this.width, this.height, title);
},
edit :function(id) {
var url = 'admin/contact/edit/'+id;
var title = '修改联系人';
Iframe(url,this.width, this.height, title);
},
del : function(id) {
var url = 'admin/contact/delete/'+id;
var title = '确认要删除该联系人信息?';
Confirm(title,function() {
form1.action= url;
form1.submit();
});
}
};
//显示Menu索引
showMenu('page_contact');
});
//分页
varpaginator = function(page) {
oper.list();
};
</script>
<%
};
var bodyContent = {
%>
<form name="form1"action="" method="post" class="form-inline"role="form">
<!-- 菜单 -->
<%include("/pages/template/menu.html"){} %>
<div class="tableSearch">
<% //查询列表 %>
<div class="form-group">
<input class="form-control"type="text" name="attr.name"value="${attr.name!''}"
placeholder="请输入姓名" required='required' />
</div>
<button type="button"class="btn btn-default" onclick="oper.list();"name="search">
<span class="glyphiconglyphicon-search"></span>查 询
</button>
<button type="button"class="btn btn-default" onclick="resetForm();">
<span class="glyphiconglyphicon-refresh"></span>重 置
</button>
<button type="button"class="btn btn-default" onclick="oper.add();">
<span class="glyphiconglyphicon-plus"></span>新 增
</button>
</div>
<!-- 数据列表 -->
<table class="tabletable-striped table-bordered table-hover">
<thead>
<tr>
<th>序号</th>
<% // 列表头部 %>
<th name="name"class="sorting">姓名</th>
<th name="phone"class="sorting">手机号</th>
<th name="email"class="sorting">Email</th>
<th>说明</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<%for(item in page.list){ %>
<tr>
<td>${itemLP.index }</td>
<% // 列表内容 %>
<td>${item.name}</td>
<td>${item.phone}</td>
<td>${item.email}</td>
<td title="${item.remark}">
<%if (strutil.length(item.remark) > 6) { %>
${strutil.subStringTo(item.remark,0, 6)}...
<% } else { %>
${item.remark}
<% } %>
</td>
<td>
<a href="javascript:void(0);"class="btn btn-sm btn-success" onclick="oper.view(${item.id});">查看</a>
<a href="javascript:void(0);"class="btn btn-sm btn-primary" onclick="oper.edit(${item.id});">修改</a>
<a href="javascript:void(0);"class="btn btn-sm btn-danger" onclick="oper.del(${item.id});">删除</a>
</td>
</tr>
<%} %>
</tbody>
</table>
<%include("/pages/includes/paginator.html"){}%>
</form>
<%};%>
<% layout("/pages/template/_layout.html",{head:headContent,body:bodyContent}){%>
<%}%>
view页面相对简单,只是一个数据展示,这里就不多做介绍了。
如果你看到了这里,有了后台代码和前台页面,联系人模块就完成了,配置一下菜单就能看到效果了~!~
温馨提示:列表分页、菜单栏、js验证和列表排序功能封装实现这里先不做说明,有兴趣的可以查看源码。
4.3 前台模板开发
4.3.1 java代码
前台CommonController为整个项目默认入口,项目根目录、登陆、登出等。
TemplateService为前台模板接口,这里的TemplateService通过beetl方法注册实现接口调用。再config中进行注册,代码如下groupTemplate.registerFunctionPackage("temp",TemplateService.class);这个不明白可以看下beetl文档,这样就实现了页面调用temp.article(1)返回文章对象。
后台service封装实现具体方法,template进行service调用提供接口。这样前台页面直接访问template接口,很好的实现了代码前后端代码分离。当然前台的java还进行了公共Controller的封装以及公共参数的传递。
如文章、评论、消息、个人信息、注册、标签等公共Controller以及统一前台参数传递拦截器FrontInterceptor。截图如下:
有一些比较个性化的东西,我这里就没有封装接口,直接创建了一个Controller。
4.3.2 模板代码
前台模板现在有几个了~这里我们以最简单的website单页滚动网站为例。
这里主要得模板页面就是home.html。项目栏目通过后端管理进行配置,默认是home目录下的${urlkey}.html文件,website配置的是home。当然也可以直接通过栏目管理配置模板路径。
这里我们主要看顶部的接口调用,temp.articlePage返回当前站点的前20条文章列表,进行列表数据展示。temp.article返回当前站点设置的默认文章,进行一些属性展示。
<%
// 获取前20个文章
var articlePage = temp.articlePage(1, 20, session.site.model.site_folder_id);
var homeArticle =temp.article(session.site.model.site_article_id);
%>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>${HEAD_TITLE }</title>
<meta http-equiv="Content-Type"content="text/html; charset=UTF-8">
<link rel="icon"href="${BASE_PATH }favicon.ico" />
<link rel="shortcuticon" href="${BASE_PATH }favicon.ico"/>
<meta http-equiv="pragma"content="no-cache">
<meta http-equiv="cache-control"content="no-cache">
<meta http-equiv="expires"content="0">
<meta name="keywords"content="${HEAD_KEYWORDS }">
<meta name="description"content="${HEAD_DESCRIPTION }">
<!-- IE兼容处理 -->
<meta http-equiv="X-UA-Compatible"content="IE=edge">
<meta name="viewport"content="width=device-width, initial-scale=1">
<link rel="stylesheet"type="text/css" href="http://cdn.bootcss.com/fullPage.js/2.7.6/jquery.fullPage.min.css"/>
<script src="http://cdn.bootcss.com/jquery/1.8.3/jquery.min.js"></script>
<script type="text/javascript"src="http://cdn.bootcss.com/fullPage.js/2.7.6/jquery.fullPage.min.js"></script>
<link href="http://cdn.bootcss.com/bootstrap/3.2.0/css/bootstrap.min.css"rel="stylesheet">
<style>
……………………
</style>
<script type="text/javascript">
varanchorsArray = [];
varnavigationTooltipsArray = [];
<% for(item in articlePage.list){ %>
anchorsArray.push('j${item.id}');
navigationTooltipsArray.push('${item.title}');
<% }%>
$(document).ready(
function() {
$('#fullpage').fullpage({
anchors: anchorsArray, //[ 'j0','j1','j2','j3','j4','j5','j6' ],
navigation: true,
navigationPosition: 'right',
navigationTooltips: navigationTooltipsArray // [ '英雄联盟','德玛西亚皇子', '德邦总管','黑暗之女', '皎月女神','寒冰射手','联系我们']
,afterLoad:function(anchorLink, index){
$('ul.nav li').removeClass('active');
$('ul.nav li#nav_'+anchorLink).addClass('active');
}
});
});
</script>
</head>
<body>
<nav class="navbarnavbar-default navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand"href="#"><img alt="logo"src="${flyfox.getImage(homeArticle)}" width="24" height="24">
${homeArticle.title}
</a>
</div>
<div class="container">
<ul class="nav navbar-navnavbar-right">
<%for(item in articlePage.list){ %>
<li id="nav_j${item.id}"><a href="#j${item.id}">${item.title}</a></li>
<%} %>
</ul>
</div>
</div>
</nav>
<div id="fullpage">
<%for(item in articlePage.list){ %>
<div class="section"id="section${itemLP.index}" style="background-image: url(${flyfox.getImage(item)});">
${item.content!}
</div>
<%} %>
</div>
</body>
</html>
到这里简单的前台模板功能就介绍完了。
4.4 前台模板接口
4.4.1 前台默认入口
首先上一章节提到了默认入口,这里我要详细说明下默认访问路径规则。FrontInterceptor前台拦截器会传递友情链接、关于我们、推荐文章和栏目信息数据。然后根目录会默认访问当前站点设置的栏目,如果未设置那么展示TbFolder.ROOT栏目,如果url传递了目录Id或者目录urlKey那么会访问对应的栏目。跳转规则是优先处理跳转jumpUrl,其次是配置的模板路径,如果都没有按照默认home/${urlKey}.html的规则跳转。从这里可以看出,简单的栏目规则就是每个站点下面栏目是和urlKey一一对应的。
如我们添加一条栏目,id:101 urlKey:jflyfox那么我们可以通过“项目地址/101.html”访问,也可以通过“项目地址/jflyfox.html”访问。但是默认的栏目菜单生成规则是优先使用urlKey。具体可以看模板下includes/ header_menu.html实现。
@ControllerBind(controllerKey= "/")
publicclass CommonController extendsBaseProjectController {
…………………………
/**
* 首页,菜单
*
* 2015年5月25日 下午11:00:28 flyfox330627517@qq.com
*/
@Before(FrontInterceptor.class)
publicvoid index() {
// new FrontService().menu(this);
int folderRoot = TbFolder.ROOT;
SessionSitesite = getSessionSite();
IntegersiteFolderId = site.getModel().getSiteFolderId();
if (siteFolderId != null&& siteFolderId > 0) {
folderRoot= siteFolderId;
}
StringfolderStr = getPara();
IntegerfolderId = folderRoot;
if (folderStr != null) {
if (NumberUtils.parseInt(folderStr)> 0) {
folderId= NumberUtils.parseInt(folderStr);
}else {
folderId= NumberUtils.parseInt(FolderService.getMenu(folderStr));
}
}
if (folderId == null ||folderId <= 0) {
folderId= folderRoot;
}
// 活动目录
setAttr("folders_selected", folderId);
TbFolderfolder = new FolderService().getFolder(folderId);
setAttr("folder", folder);
setAttr("paginator", getPaginator());
// seo:title优化
StringfolderName = (folder == null ? "" : folder.getStr("name") + "- ");
setAttr(JFlyFoxUtils.TITLE_ATTR, folderName + getAttr(JFlyFoxUtils.TITLE_ATTR));
// 栏目跳转规则
StringjumpUrl = folder.getJumpUrl();
Stringpath = folder.getPath();
StringurlKey = folder.getKey();
if (StrUtils.isNotEmpty(jumpUrl)) {
redirect(jumpUrl);
} elseif (StrUtils.isNotEmpty(path)) {
renderAuto(path);
} else {
renderAuto(Home.PATH + urlKey + ".html");
}
}
……………………………………
}
其次是FrontAlbumImageController和FrontAlbumVideoController分别是图片和视频模块统一入口,现在功能相对简单,这里就不详细介绍了。
4.4.2 前台接口
前台接口主要是TemplateService这个类,提供方式是利用beetl函数注册方式,如groupTemplate.registerFunctionPackage("temp",TemplateService.class);代码,这样我们就可以通过temp.${method}进行调用了。
下面是接口代码,例如通过temp.countView(100),我们就能获取到文章id为100的浏览量,temp. articlePage(23,1,20)我们就能够获取到栏目id为23下面的前20条文章。
接口主要提供了文章列表、文章内容、浏览量、评论量,栏目标签,栏目、滚屏图片列表和提醒列表等信息。
packagecom.jflyfox.modules.front.template;
……………………
/**
* 模板方法接口
*
* 2016年1月18日 下午6:05:54 flyfox 330627517@qq.com
*/
publicclass TemplateService extendsBaseService {
privatefinalstatic FrontCacheService service = new FrontCacheService();
privatefinalstatic ArticleLikeCache articleLikeservice = newArticleLikeCache();
/**
* 获取浏览数
*
* 2015年6月2日 下午6:30:56 flyfox330627517@qq.com
*
* @paramarticleId
* @return
*/
publicstaticint countView(int articleId) {
TbArticlearticle = service.getArticleCount(articleId);
return article == null ? 0 :article.getCountView();
}
/**
* 获取评论数
*
* 2015年6月2日 下午6:30:56 flyfox330627517@qq.com
*
* @param articleId
* @return
*/
publicstaticint countComment(int articleId) {
TbArticlearticle = service.getArticleCount(articleId);
return article == null ? 0 :article.getCountComment();
}
/**
* 获取浏览数
*
* 2015年6月2日 下午6:30:56 flyfox330627517@qq.com
*
* @param articleId
* @return
*/
publicstaticboolean isLike(int userId, int articleId) {
returnarticleLikeservice.isLike(userId, articleId);
}
/**
* 获取标签信息
*
* 2015年5月25日 下午11:49:58 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* 目录
* @return
*/
public List<TbTags> tagsListByArticle(int articleId) {
returnservice.getTagsByArticle(articleId);
}
/**
* 获取标签信息
*
* 2015年5月25日 下午11:49:58 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* 目录
* @return
*/
public Page<TbTags> tagsPageByFolder(int pageNo, int pageSize, int folderId) {
returnservice.getTagsByFolder(newPaginator(pageNo, pageSize), folderId);
}
/**
* 获取标签信息
*
* 2015年5月25日 下午11:49:03 flyfox330627517@qq.com
*
* @param paginator
* @return
*/
public Page<TbTags> tagsPage(intpageNo, int pageSize, intsiteId) {
returnservice.getTags(newPaginator(pageNo, pageSize), siteId);
}
/**
* 查询文章,展示的和类型为11,12的推荐文件,前10个
*
* 2015年4月29日 下午4:48:24 flyfox330627517@qq.com
*
* @param paginator
* @param folder_id
* @return
*/
public Page<TbArticle> articlePageRecommend(int pageNo, int pageSize, int siteId) {
returnservice.getRecommendArticle(new Paginator(pageNo, pageSize), siteId);
}
/**
* 返回最新文章
*
* 2015年5月24日 下午10:52:05 flyfox330627517@qq.com
*
* @param paginator
* @return
*/
public Page<TbArticle> articlePageTop(int pageNo, int pageSize, int siteId) {
returnservice.getNewArticle(newPaginator(pageNo, pageSize), siteId);
}
/**
* 返回文章列表
*
* 2015年5月24日 下午10:52:05 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* @return
*/
public Page<TbArticle> articlePageSite(int pageNo, int pageSize, int siteId) {
returnservice.getArticleBySiteId(new Paginator(pageNo, pageSize), siteId);
}
/**
* 返回文章列表
*
* 2015年5月24日 下午10:52:05 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* @return
*/
public Page<TbArticle> articlePage(int pageNo, int pageSize, int folderId) {
returnservice.getArticle(newPaginator(pageNo, pageSize), folderId);
}
/**
* 返回文章列表不走缓存
*
* 2015年5月24日 下午10:52:05 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* @return
*/
public Page<TbArticle> articlePageNoCache(int pageNo, int pageSize, int folderId) {
returnservice.getArticleByNoCache(new Paginator(pageNo, pageSize), folderId);
}
/**
* 返回对应文章
*
* 2015年5月24日 下午10:52:05 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* @return
*/
public TbArticle article(intarticleId) {
returnservice.getArticle(articleId);
}
/**
* 获取栏目滚动图片
*
* 2016年1月28日 下午5:28:25 flyfox330627517@qq.com
*
* @param folderId
* @return
*/
public List<TbFolderRollPicture> rollPicture(int folderId) {
returnservice.getRollPicture(folderId);
}
/**
* 获取公告信息
*
* 2016年1月28日 下午5:29:47 flyfox330627517@qq.com
*
* @param folderId
* @return
*/
public List<TbFolderNotice> notice(int folderId) {
returnservice.getNotice(folderId);
}
}
除了默认出口和上述接口外,我们还提供了一些公共Controller和个性Controller,如下图所示:
这些Controller大致实现了关于我们、建议、文章查看、评论、消息、Oauth2登陆、个人信息、注册信息和标签功能。
4.5 项目工具类
base下包含了Model和Controller的父类:Model实现了增删改统一日志,自定义缓存查询(类似jfinal自带缓存查询)以及查询方法扩展,部分实现在BaseModel中。Controller实现了获取和设置用户session、站点session获取、自动识别移动和PC端跳转、带提示跳转,获取分页、获取form等功能,部分实现在BaseController中。
beetl下包含了beetl公共方法封装:数据字典获取、图片路径转换、菜单地址获取以及常规字符串处理方法。
config下是jfinal配置文件入口,里面有大量的jfinal初始化配置,这里就不一一说明了。
controller下是ueditor和umeditor的后台实现代码。
interceptor是通用拦截器:包含SEO属性设置,页面访问量统计,站点Session处理、缓存更新以及用户Session唯一键设置。在jflyfox_jfinal中还包含登陆session拦截、Session属性设置、form获取统一处理。
util下是一些公共配合及方法:包含文章访问量和评论量缓存,验证码生成、图片属性获取、缓存管理统一入口、上传组件封装以及常量类等。
项目内部工具类大致如下图所示:
5 jfinal cms项目依赖
jfinal cms采用maven结构,依赖与作者的其他两个项目。
l jflyfox_base项目
jfinal_base提供了java的工具类,如StrUtils,NumberUtils,DateUtils 等
github:https://github.com/jflyfox/jflyfox_base
osc@git:http://git.oschina.net/flyfox/jflyfox_base
l jflyfox_jfinal项目
jflyfox_jfinal是对Jfinal等组件的封装。
包含controller,model,form,service基础类封装。
对分页进行了后台和前台的实现。
加入了自动扫描model和controller以及注解支持。
实现了ueditor后台Controller代码。
加入了页面增删改查代码自动生成功能,可通过beetl模板进行配置。
实现了SessionAttrInterceptor、页面和手机设备判断拦截器以及BasePathHandler。
github:https://github.com/jflyfox/jflyfox_jfinal
osc@git:http://git.oschina.net/flyfox/jflyfox_jfinal
6 联系作者
作者:FLY的狐狸
jfinal cms交流群:568909653
QQ:369191470
源码地址:http://git.oschina.net/flyfox/jfinal_cms
7 开源赞助
支付宝支付
微信支付
jfinal cms,采用了简洁强大的JFinal作为web框架,模板引擎用的是beetl,数据库用mysql,前端bootstrap框架。 支持多站点、oauth2认证、帐号注册、密码加密、评论及回复,消息提示,网站访问量统计,文章评论数和浏览量统计,回复管理,支持权限管理。
后台模块包含:栏目管理,栏目公告,栏目滚动图片,文章管理,回复管理,意见反馈,我的相册,相册管理,图片管理,专辑管理、视频管理、缓存更新,友情链接,访问统计,联系人管理,模板管理,组织机构管理,用户管理,角色管理,菜单管理,数据字典管理,站点管理。
建站截图:
2 jfinal cms部署与配置
2.1 maven方式部署配置
maven部署方式的好处在于不依赖于任何IDE,并且跨平台。
1) 下载项目源代码,安装jdk、maven、mysql。
2) 在项目目录下运行mvn install,提示BUILD SUCCESS即可。
3) 创建mysql用户和数据库,运行/jfinal_cms/sql下对应jfinal_cms_v4.sql。
4) 数据库配置文件:/jfinal_cms/src/main/resources/conf/db.properties,修改相应配置即可。
db_type=mysql
mysql.jdbcUrl =jdbc:mysql://127.0.0.1:3306/jfinal_cms2?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
mysql.user = root
mysql.password = 123456
mysql.driverClass = com.mysql.jdbc.Driver
5) 运行:mvn tomcat:run。看到“系统启动完成”说明运行成功。
6) 网站效果访问:http://127.0.0.1/jfinal_cms。
7) 管理地址:http://${ip:port}/${project_name}/admin,管理账号: admin/admin123
8) 各个模板的切换已通过系统中“站点管理”模块进行操作。站点管理是通过域名解析实现各个模板的对应。
2.2 eclipse部署配置
因为java开发,用eclipse的比较多~这里再介绍一下eclipse导入,部署和配置。
2.2.1 获取源码地址
源码:http://git.oschina.net/flyfox/jfinal_cms复制git地址。
2.2.2 下载git源码
然后我们打开eclipse,导入git项目。(这里也可以用git工具先将源码下载下来,或者直接下载zip包)
选择git导入:
点击URI导入方式:
输入复制的git地址:
选择下载master主分支:
选择导入到本地的目录:
点击下一步获取git源代码。
2.2.3 构建maven项目
如果git获取后,我们选择了作为一个普通项目导入,那么我们需要将其转换为maven项目,项目上面邮件选择Configure->Convert to MavenProject:
如果我们通过其他git工具或者下载zip,那么我们需要导入maven项目
选择项目路径即可:
这样我们项目就导入完成了。
2.2.4 运行项目代码
到这里我们需要安装好mysql,运行完初始化脚本,并且配置好数据库连接串以及账号密码。这些上面maven方式部署有介绍,就不再重复了。
现在我们就可以进行maven项目构建了,运行maveninstall,安装,并下载依赖包:
如下提示说明构建成功
[INFO]------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO]------------------------------------------------------------------------
[INFO] Total time: 22.565s
[INFO] Finished at: Sat Jun 11 07:41:04 GMT 2016
[INFO] Final Memory: 6M/12M
[INFO]------------------------------------------------------------------------
然后我们运行项目,项目右键,Run As-> Maven build...
然后运行tomcat:run,点击运行按钮:
看到如下提示,并且没有报错,说明启动成功。
##################################
############系统启动完成##########
##################################
2.2.5 项目访问
网站效果访问:http://127.0.0.1/jfinal_cms/
管理地址:http://127.0.0.1/jfinal_cms/admin,管理账号: admin/admin123
到这里jfinal cms部署就大功告成了~!~
2.3 实时编译源码配置
上面部署方式有一个小问题就是,每次修改都需要进行maven install后,再maven tomcat:run才能看到修改后的效果。可以进行如下配置解决这个问题。
修改pom,删除warSourceDirectory配置。
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>tomcat-maven-plugin</artifactId>
<version>1.1</version>
<configuration>
<port>80</port>
<path>/${project.artifactId}</path>
<warSourceDirectory>${basedir}/target/${artifactId}</warSourceDirectory>
</configuration>
</plugin>
项目右键,选择构建路径(BuildPath->Configure Build Path...):
删除源码路径,重新配置,再修改默认输出路径(Default output folder):
这是再运行就可以进行实时编辑查看了。
2.4 其他配置说明
1) 如需要oauth2的,设置src/conf/oauth.properties
2) 如果只使用单站点,可以将sites.properties文件中SITE.MULTI.FLAG = true改为false。然后通过config.properties的ATTR.PATH_PC=/template/mtg配置模板。
3 相关技术说明
3.1 java框架jfinal
jfinal官网:http://www.jfinal.com/
下面我在网上截了一张jfinal架构图,个人觉得梳理的很清楚。jfina官网上面的文档在使用方面介绍的简单明了,上手很容易,个人感觉没有必要在进行其他介绍了。
3.2 java模板beetl
beetl文档:http://ibeetl.com/guide/beetl.html
beetl的有点和好处可以看上面文档第一章节,作者介绍的已经很清楚了。详细介绍大家可以看上面的文档,我下面制作一些简单介绍。
l 变量引用
变量引用与其他模板相同,可以通过${name}调用。
这里要特殊说明的是beetl支持获取map属性,即调用get(Objectkey)。如果对象既有具体属性getName(),又有get(Object key),则以具体属性优先级高。如${user.name}先进行调用getName()方法如果没有再调用get(Objectkey)方法。
l list循环
代码片段如下:
总共 ${list.~size}
<%
for(user inlist){
%>
hello,${user.name};
<%}%>
当前页${pageMap['page']},总共${pageMap["total"]}
l 函数调用
可以直接调用registerFunctionPackage(namespace,yourJavaObject),这时候yourJavaObject里的所有public方法都将注册为Beetl方法,方法名是namespace+"."+方法名
jfinalcms中有许多方法和前台接口都是用了这种方式,所以这里也别说明一下。后续会进行详细介绍。
l 其他
剩下的大家就参考项目代码进行理解就可以了~如果不明白可以看beetl官方文档。
4 jfinal cms技术
源码依赖于作者的其他两个开源项目jflyfox_base和jflyfox_jfinal两个项目,相关介绍在jfinal cms项目依赖章节有所介绍。
4.1 项目目录结构介绍
首先我们先来熟悉一下项目的目录结构,以下为该项目目录结构说明,java代码:
项目文件目录:
4.2 后台模块开发
4.2.1 java代码
首先后台代码主要分为Controller,Model和Service。Controller负责参数传递和页面跳转;Model负责数据承载;Service负责复杂业务处理。
这里我们用联系人模块进行简单讲解。
下面是controller示例代码,首先需要继承BaseProjectController类,这样方便项目统一扩展,现在里面已经有一些公共方法了,请自行查看源码。
页面跳转主要包含list(列表页)、add(添加页)、view(查看页)、delete(删除数据)、edit(编辑页)和save(保存方法)。这几个是基本操作,常规增删改查均以此规范进行编写。Controller遵循jfinal设置规则,并加入了注解功能。以下列表访问地址就是/admin/contact/list;编辑页面地址就是/admin/contact/edit/${id}。其他地址不再列举。
以下黄色背景字体需要特别注意。
packagecom.jflyfox.modules.admin.contact;
importcom.jfinal.plugin.activerecord.Page;
importcom.jflyfox.component.base.BaseProjectController;
importcom.jflyfox.jfinal.component.annotation.ControllerBind;
importcom.jflyfox.jfinal.component.db.SQLUtils;
importcom.jflyfox.util.StrUtils;
/**
* 联系人管理
*
* @author flyfox 2014-2-11
*/
@ControllerBind(controllerKey= "/admin/contact")
publicclass ContactController extendsBaseProjectController {
privatestaticfinal String path = "/pages/admin/contact/contact_";
publicvoid list() {
TbContactmodel = getModelByAttr(TbContact.class);
// 这里是拼接查询sql并且获取前台传递的参数
SQLUtilssql = new SQLUtils(" from tb_contact t where 1=1 ");
if (model.getAttrValues().length != 0){
sql.setAlias("t");
sql.whereLike("name", model.getStr("name"));
sql.whereEquals("type", model.getStr("type"));
}
// 这里是统一做了排序功能
StringorderBy = getBaseForm().getOrderBy();
if (StrUtils.isEmpty(orderBy)) {
sql.append(" order by id desc ");
} else {
sql.append(" order by ").append(orderBy);
}
Page<TbContact>page = TbContact.dao.paginate(getPaginator(), "select t.* ", //
sql.toString().toString());
// 下拉框
setAttr("page", page);
setAttr("attr", model);
render(path + "list.html");
}
publicvoid add() {
render(path + "add.html");
}
publicvoid view() {
TbContactmodel = TbContact.dao.findById(getParaToInt());
setAttr("model", model);
render(path + "view.html");
}
publicvoid delete() {
TbContactmodel = new TbContact();
Integeruserid= getSessionUser().getUserID();
Stringnow = getNow();
model.put("update_id", userid);
model.put("update_time", now);
model.deleteById(getParaToInt());
list();
}
publicvoid edit() {
TbContactmodel = TbContact.dao.findById(getParaToInt());
setAttr("model", model);
render(path + "edit.html");
}
publicvoid save() {
Integerpid = getParaToInt();
TbContactmodel = getModel(TbContact.class);
Integeruserid= getSessionUser().getUserID();
Stringnow = getNow();
// 这两个字段设置为了日志收集
model.put("update_id", userid);
model.put("update_time", now);
if (pid != null && pid >0) { // 更新
model.update();
} else { // 新增
model.remove("id");
model.put("create_id", userid);
model.put("create_time", now);
model.save();
}
renderMessage("保存成功");
}
}
下面Model示例代码,Model需要继承BaseProjectModel,方便业务扩展。里面已经加入了缓存、日志等扩展功能,请自行查看源码。jfinal Model可以不添加get、set方法,这里添加主要是为了代码规范。Model遵循jfinal设置规则,并加入了注解功能。通过ModelBind可以直接设置对应的表和主键,主键默认是ID。
get、set方法可以通过作者的AutoCreate组件自动生成。这里不再做多介绍。
packagecom.jflyfox.modules.admin.contact;
importcom.jflyfox.component.base.BaseProjectModel;
importcom.jflyfox.jfinal.component.annotation.ModelBind;
@ModelBind(table= "tb_contact")
publicclass TbContact extendsBaseProjectModel<TbContact> {
privatestaticfinallongserialVersionUID = 1L;
publicstaticfinal TbContact dao = new TbContact();
// columns START
private String ID = "id"; // 主键
private String NAME = "name"; // 姓名
public TbContactsetId(java.lang.Integer value) {
set(ID,value);
returnthis;
}
public java.lang.Integer getId() {
return get(ID);
}
public TbContact setName(java.lang.String value) {
set(NAME, value);
returnthis;
}
public java.lang.String getName() {
return get(NAME);
}
……………………………………
}
Service集成BaseService,主要是Controller中复杂业务处理,将Controller中大量代码或者重复代码进行封装,统一调用。由于联系人模块相对简单,这里就没有Service代码了。
4.2.2 页面代码
后台模板主要是增删改查,jfinalcms使用beetl模板,这里就不多介绍了。
页面主要以${module}_名开头,包含add(添加)、edit(编辑)、list(列表)、view(查看)几个页面。各个页面页格式相对统一,可以进行参考。
以下为add页面代码,添加页面可以与edit页面公用~只是传递参数均为空。
<%
include("contact_edit.html"){}
%>
以下为edit页面代码,首先edit页面采用了beetl安全模式,保证参数为空不报异常,这样能够让add很好的引用。其次后端页面都采用了公共的头部标签head.html,列表和查看页面也是如此,不在赘述。
在编辑页面对js验证进行了封装,只需要添加valid和validname属性就可以实现js验证功能,提交时调用 validForm()方法即可。
<%
DIRECTIVE SAFE_OUTPUT_OPEN;
var headContent = {
include("/pages/template/head.html"){}
%>
<script type="text/javascript">
var oper= {
save:function(id){
if(!validForm()) {
returnfalse;
}
id =id || '0';
var url = 'admin/contact/save/'+id;
form1.action= url;
form1.submit();
returntrue;
}
};
</script>
<%
};
var bodyContent = {
%>
<form name="form1"action="" method="post"class="form-horizontal" role="form">
<input type="hidden"name="model.id" value="${model.id}"/>
<table class="table">
<%// 列表头部 %>
<tr>
<td>姓名</td>
<td>
<input class="form-control"type="text" name="model.name"value="${model.name}"
valid="vtext" validname="名称" />
</td>
</tr>
………………………………
<tr>
<td>说明</td>
<td>
<textarea class="form-control"rows="3" cols="30"name="model.remark">${model.remark}</textarea>
</td>
</tr>
</table>
<div style="height: 50px;clear: both;"> </div>
<nav class="navbarnavbar-default navbar-fixed-bottom">
<div class="container" style="padding: 5px 0px 5px 0px;text-align: center;">
<button class="btnbtn-primary" onclick="returnoper.save(${model.id!'0'});">保 存</button>
<button class="btnbtn-default" onclick="closeIframe();returnfalse;">关 闭</button>
</div>
</nav>
</form>
<%}; %>
<%layout("/pages/template/_layout.html",{head:headContent,body:bodyContent}){%>
DIRECTIVE SAFE_OUTPUT_CLOSE;
<%}%>
下面是列表页代码, 方法调用以oper对象调用形式封装。增加、修改、查看页面以弹框形式展示,这样可以很好的保存列表页查询条件。查看页面关闭不会刷新列表,添加和修改页面关闭后均会刷新列表,均已封装为Iframe方法。
paginator属性为列表分页查询调用方法。
showMenu方法为菜单选中展现,具体参数在菜单管理进行配置。
menu.html为菜单栏,各页面直接引用即可。paginator.html为分页栏,固定获取page参数进行处理。
列表头部设置设置class="sorting"为支持排序功能,name字段为传到后台的参数值。
<%
varheadContent = {
include("/pages/template/head.html"){}
%>
<script type="text/javascript">
var oper;
jQuery(function($) {
// 页面方法
oper = {
width: 400,
height: 430,
form :document.form1,
list :function() {
var url = 'admin/contact/list';
this.form.action = url;
this.form.submit();
},
view :function(id) {
var url = 'admin/contact/view/'+id;
var title = '查看联系人';
Iframe(url,this.width, this.height, title, false, false, false, EmptyFunc);
},
add : function() {
var url = 'admin/contact/add';
var title = '添加联系人';
Iframe(url,this.width, this.height, title);
},
edit :function(id) {
var url = 'admin/contact/edit/'+id;
var title = '修改联系人';
Iframe(url,this.width, this.height, title);
},
del : function(id) {
var url = 'admin/contact/delete/'+id;
var title = '确认要删除该联系人信息?';
Confirm(title,function() {
form1.action= url;
form1.submit();
});
}
};
//显示Menu索引
showMenu('page_contact');
});
//分页
varpaginator = function(page) {
oper.list();
};
</script>
<%
};
var bodyContent = {
%>
<form name="form1"action="" method="post" class="form-inline"role="form">
<!-- 菜单 -->
<%include("/pages/template/menu.html"){} %>
<div class="tableSearch">
<% //查询列表 %>
<div class="form-group">
<input class="form-control"type="text" name="attr.name"value="${attr.name!''}"
placeholder="请输入姓名" required='required' />
</div>
<button type="button"class="btn btn-default" onclick="oper.list();"name="search">
<span class="glyphiconglyphicon-search"></span>查 询
</button>
<button type="button"class="btn btn-default" onclick="resetForm();">
<span class="glyphiconglyphicon-refresh"></span>重 置
</button>
<button type="button"class="btn btn-default" onclick="oper.add();">
<span class="glyphiconglyphicon-plus"></span>新 增
</button>
</div>
<!-- 数据列表 -->
<table class="tabletable-striped table-bordered table-hover">
<thead>
<tr>
<th>序号</th>
<% // 列表头部 %>
<th name="name"class="sorting">姓名</th>
<th name="phone"class="sorting">手机号</th>
<th name="email"class="sorting">Email</th>
<th>说明</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<%for(item in page.list){ %>
<tr>
<td>${itemLP.index }</td>
<% // 列表内容 %>
<td>${item.name}</td>
<td>${item.phone}</td>
<td>${item.email}</td>
<td title="${item.remark}">
<%if (strutil.length(item.remark) > 6) { %>
${strutil.subStringTo(item.remark,0, 6)}...
<% } else { %>
${item.remark}
<% } %>
</td>
<td>
<a href="javascript:void(0);"class="btn btn-sm btn-success" onclick="oper.view(${item.id});">查看</a>
<a href="javascript:void(0);"class="btn btn-sm btn-primary" onclick="oper.edit(${item.id});">修改</a>
<a href="javascript:void(0);"class="btn btn-sm btn-danger" onclick="oper.del(${item.id});">删除</a>
</td>
</tr>
<%} %>
</tbody>
</table>
<%include("/pages/includes/paginator.html"){}%>
</form>
<%};%>
<% layout("/pages/template/_layout.html",{head:headContent,body:bodyContent}){%>
<%}%>
view页面相对简单,只是一个数据展示,这里就不多做介绍了。
如果你看到了这里,有了后台代码和前台页面,联系人模块就完成了,配置一下菜单就能看到效果了~!~
温馨提示:列表分页、菜单栏、js验证和列表排序功能封装实现这里先不做说明,有兴趣的可以查看源码。
4.3 前台模板开发
4.3.1 java代码
前台CommonController为整个项目默认入口,项目根目录、登陆、登出等。
TemplateService为前台模板接口,这里的TemplateService通过beetl方法注册实现接口调用。再config中进行注册,代码如下groupTemplate.registerFunctionPackage("temp",TemplateService.class);这个不明白可以看下beetl文档,这样就实现了页面调用temp.article(1)返回文章对象。
后台service封装实现具体方法,template进行service调用提供接口。这样前台页面直接访问template接口,很好的实现了代码前后端代码分离。当然前台的java还进行了公共Controller的封装以及公共参数的传递。
如文章、评论、消息、个人信息、注册、标签等公共Controller以及统一前台参数传递拦截器FrontInterceptor。截图如下:
有一些比较个性化的东西,我这里就没有封装接口,直接创建了一个Controller。
4.3.2 模板代码
前台模板现在有几个了~这里我们以最简单的website单页滚动网站为例。
这里主要得模板页面就是home.html。项目栏目通过后端管理进行配置,默认是home目录下的${urlkey}.html文件,website配置的是home。当然也可以直接通过栏目管理配置模板路径。
这里我们主要看顶部的接口调用,temp.articlePage返回当前站点的前20条文章列表,进行列表数据展示。temp.article返回当前站点设置的默认文章,进行一些属性展示。
<%
// 获取前20个文章
var articlePage = temp.articlePage(1, 20, session.site.model.site_folder_id);
var homeArticle =temp.article(session.site.model.site_article_id);
%>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>${HEAD_TITLE }</title>
<meta http-equiv="Content-Type"content="text/html; charset=UTF-8">
<link rel="icon"href="${BASE_PATH }favicon.ico" />
<link rel="shortcuticon" href="${BASE_PATH }favicon.ico"/>
<meta http-equiv="pragma"content="no-cache">
<meta http-equiv="cache-control"content="no-cache">
<meta http-equiv="expires"content="0">
<meta name="keywords"content="${HEAD_KEYWORDS }">
<meta name="description"content="${HEAD_DESCRIPTION }">
<!-- IE兼容处理 -->
<meta http-equiv="X-UA-Compatible"content="IE=edge">
<meta name="viewport"content="width=device-width, initial-scale=1">
<link rel="stylesheet"type="text/css" href="http://cdn.bootcss.com/fullPage.js/2.7.6/jquery.fullPage.min.css"/>
<script src="http://cdn.bootcss.com/jquery/1.8.3/jquery.min.js"></script>
<script type="text/javascript"src="http://cdn.bootcss.com/fullPage.js/2.7.6/jquery.fullPage.min.js"></script>
<link href="http://cdn.bootcss.com/bootstrap/3.2.0/css/bootstrap.min.css"rel="stylesheet">
<style>
……………………
</style>
<script type="text/javascript">
varanchorsArray = [];
varnavigationTooltipsArray = [];
<% for(item in articlePage.list){ %>
anchorsArray.push('j${item.id}');
navigationTooltipsArray.push('${item.title}');
<% }%>
$(document).ready(
function() {
$('#fullpage').fullpage({
anchors: anchorsArray, //[ 'j0','j1','j2','j3','j4','j5','j6' ],
navigation: true,
navigationPosition: 'right',
navigationTooltips: navigationTooltipsArray // [ '英雄联盟','德玛西亚皇子', '德邦总管','黑暗之女', '皎月女神','寒冰射手','联系我们']
,afterLoad:function(anchorLink, index){
$('ul.nav li').removeClass('active');
$('ul.nav li#nav_'+anchorLink).addClass('active');
}
});
});
</script>
</head>
<body>
<nav class="navbarnavbar-default navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand"href="#"><img alt="logo"src="${flyfox.getImage(homeArticle)}" width="24" height="24">
${homeArticle.title}
</a>
</div>
<div class="container">
<ul class="nav navbar-navnavbar-right">
<%for(item in articlePage.list){ %>
<li id="nav_j${item.id}"><a href="#j${item.id}">${item.title}</a></li>
<%} %>
</ul>
</div>
</div>
</nav>
<div id="fullpage">
<%for(item in articlePage.list){ %>
<div class="section"id="section${itemLP.index}" style="background-image: url(${flyfox.getImage(item)});">
${item.content!}
</div>
<%} %>
</div>
</body>
</html>
到这里简单的前台模板功能就介绍完了。
4.4 前台模板接口
4.4.1 前台默认入口
首先上一章节提到了默认入口,这里我要详细说明下默认访问路径规则。FrontInterceptor前台拦截器会传递友情链接、关于我们、推荐文章和栏目信息数据。然后根目录会默认访问当前站点设置的栏目,如果未设置那么展示TbFolder.ROOT栏目,如果url传递了目录Id或者目录urlKey那么会访问对应的栏目。跳转规则是优先处理跳转jumpUrl,其次是配置的模板路径,如果都没有按照默认home/${urlKey}.html的规则跳转。从这里可以看出,简单的栏目规则就是每个站点下面栏目是和urlKey一一对应的。
如我们添加一条栏目,id:101 urlKey:jflyfox那么我们可以通过“项目地址/101.html”访问,也可以通过“项目地址/jflyfox.html”访问。但是默认的栏目菜单生成规则是优先使用urlKey。具体可以看模板下includes/ header_menu.html实现。
@ControllerBind(controllerKey= "/")
publicclass CommonController extendsBaseProjectController {
…………………………
/**
* 首页,菜单
*
* 2015年5月25日 下午11:00:28 flyfox330627517@qq.com
*/
@Before(FrontInterceptor.class)
publicvoid index() {
// new FrontService().menu(this);
int folderRoot = TbFolder.ROOT;
SessionSitesite = getSessionSite();
IntegersiteFolderId = site.getModel().getSiteFolderId();
if (siteFolderId != null&& siteFolderId > 0) {
folderRoot= siteFolderId;
}
StringfolderStr = getPara();
IntegerfolderId = folderRoot;
if (folderStr != null) {
if (NumberUtils.parseInt(folderStr)> 0) {
folderId= NumberUtils.parseInt(folderStr);
}else {
folderId= NumberUtils.parseInt(FolderService.getMenu(folderStr));
}
}
if (folderId == null ||folderId <= 0) {
folderId= folderRoot;
}
// 活动目录
setAttr("folders_selected", folderId);
TbFolderfolder = new FolderService().getFolder(folderId);
setAttr("folder", folder);
setAttr("paginator", getPaginator());
// seo:title优化
StringfolderName = (folder == null ? "" : folder.getStr("name") + "- ");
setAttr(JFlyFoxUtils.TITLE_ATTR, folderName + getAttr(JFlyFoxUtils.TITLE_ATTR));
// 栏目跳转规则
StringjumpUrl = folder.getJumpUrl();
Stringpath = folder.getPath();
StringurlKey = folder.getKey();
if (StrUtils.isNotEmpty(jumpUrl)) {
redirect(jumpUrl);
} elseif (StrUtils.isNotEmpty(path)) {
renderAuto(path);
} else {
renderAuto(Home.PATH + urlKey + ".html");
}
}
……………………………………
}
其次是FrontAlbumImageController和FrontAlbumVideoController分别是图片和视频模块统一入口,现在功能相对简单,这里就不详细介绍了。
4.4.2 前台接口
前台接口主要是TemplateService这个类,提供方式是利用beetl函数注册方式,如groupTemplate.registerFunctionPackage("temp",TemplateService.class);代码,这样我们就可以通过temp.${method}进行调用了。
下面是接口代码,例如通过temp.countView(100),我们就能获取到文章id为100的浏览量,temp. articlePage(23,1,20)我们就能够获取到栏目id为23下面的前20条文章。
接口主要提供了文章列表、文章内容、浏览量、评论量,栏目标签,栏目、滚屏图片列表和提醒列表等信息。
packagecom.jflyfox.modules.front.template;
……………………
/**
* 模板方法接口
*
* 2016年1月18日 下午6:05:54 flyfox 330627517@qq.com
*/
publicclass TemplateService extendsBaseService {
privatefinalstatic FrontCacheService service = new FrontCacheService();
privatefinalstatic ArticleLikeCache articleLikeservice = newArticleLikeCache();
/**
* 获取浏览数
*
* 2015年6月2日 下午6:30:56 flyfox330627517@qq.com
*
* @paramarticleId
* @return
*/
publicstaticint countView(int articleId) {
TbArticlearticle = service.getArticleCount(articleId);
return article == null ? 0 :article.getCountView();
}
/**
* 获取评论数
*
* 2015年6月2日 下午6:30:56 flyfox330627517@qq.com
*
* @param articleId
* @return
*/
publicstaticint countComment(int articleId) {
TbArticlearticle = service.getArticleCount(articleId);
return article == null ? 0 :article.getCountComment();
}
/**
* 获取浏览数
*
* 2015年6月2日 下午6:30:56 flyfox330627517@qq.com
*
* @param articleId
* @return
*/
publicstaticboolean isLike(int userId, int articleId) {
returnarticleLikeservice.isLike(userId, articleId);
}
/**
* 获取标签信息
*
* 2015年5月25日 下午11:49:58 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* 目录
* @return
*/
public List<TbTags> tagsListByArticle(int articleId) {
returnservice.getTagsByArticle(articleId);
}
/**
* 获取标签信息
*
* 2015年5月25日 下午11:49:58 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* 目录
* @return
*/
public Page<TbTags> tagsPageByFolder(int pageNo, int pageSize, int folderId) {
returnservice.getTagsByFolder(newPaginator(pageNo, pageSize), folderId);
}
/**
* 获取标签信息
*
* 2015年5月25日 下午11:49:03 flyfox330627517@qq.com
*
* @param paginator
* @return
*/
public Page<TbTags> tagsPage(intpageNo, int pageSize, intsiteId) {
returnservice.getTags(newPaginator(pageNo, pageSize), siteId);
}
/**
* 查询文章,展示的和类型为11,12的推荐文件,前10个
*
* 2015年4月29日 下午4:48:24 flyfox330627517@qq.com
*
* @param paginator
* @param folder_id
* @return
*/
public Page<TbArticle> articlePageRecommend(int pageNo, int pageSize, int siteId) {
returnservice.getRecommendArticle(new Paginator(pageNo, pageSize), siteId);
}
/**
* 返回最新文章
*
* 2015年5月24日 下午10:52:05 flyfox330627517@qq.com
*
* @param paginator
* @return
*/
public Page<TbArticle> articlePageTop(int pageNo, int pageSize, int siteId) {
returnservice.getNewArticle(newPaginator(pageNo, pageSize), siteId);
}
/**
* 返回文章列表
*
* 2015年5月24日 下午10:52:05 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* @return
*/
public Page<TbArticle> articlePageSite(int pageNo, int pageSize, int siteId) {
returnservice.getArticleBySiteId(new Paginator(pageNo, pageSize), siteId);
}
/**
* 返回文章列表
*
* 2015年5月24日 下午10:52:05 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* @return
*/
public Page<TbArticle> articlePage(int pageNo, int pageSize, int folderId) {
returnservice.getArticle(newPaginator(pageNo, pageSize), folderId);
}
/**
* 返回文章列表不走缓存
*
* 2015年5月24日 下午10:52:05 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* @return
*/
public Page<TbArticle> articlePageNoCache(int pageNo, int pageSize, int folderId) {
returnservice.getArticleByNoCache(new Paginator(pageNo, pageSize), folderId);
}
/**
* 返回对应文章
*
* 2015年5月24日 下午10:52:05 flyfox330627517@qq.com
*
* @param paginator
* @param folderId
* @return
*/
public TbArticle article(intarticleId) {
returnservice.getArticle(articleId);
}
/**
* 获取栏目滚动图片
*
* 2016年1月28日 下午5:28:25 flyfox330627517@qq.com
*
* @param folderId
* @return
*/
public List<TbFolderRollPicture> rollPicture(int folderId) {
returnservice.getRollPicture(folderId);
}
/**
* 获取公告信息
*
* 2016年1月28日 下午5:29:47 flyfox330627517@qq.com
*
* @param folderId
* @return
*/
public List<TbFolderNotice> notice(int folderId) {
returnservice.getNotice(folderId);
}
}
除了默认出口和上述接口外,我们还提供了一些公共Controller和个性Controller,如下图所示:
这些Controller大致实现了关于我们、建议、文章查看、评论、消息、Oauth2登陆、个人信息、注册信息和标签功能。
4.5 项目工具类
base下包含了Model和Controller的父类:Model实现了增删改统一日志,自定义缓存查询(类似jfinal自带缓存查询)以及查询方法扩展,部分实现在BaseModel中。Controller实现了获取和设置用户session、站点session获取、自动识别移动和PC端跳转、带提示跳转,获取分页、获取form等功能,部分实现在BaseController中。
beetl下包含了beetl公共方法封装:数据字典获取、图片路径转换、菜单地址获取以及常规字符串处理方法。
config下是jfinal配置文件入口,里面有大量的jfinal初始化配置,这里就不一一说明了。
controller下是ueditor和umeditor的后台实现代码。
interceptor是通用拦截器:包含SEO属性设置,页面访问量统计,站点Session处理、缓存更新以及用户Session唯一键设置。在jflyfox_jfinal中还包含登陆session拦截、Session属性设置、form获取统一处理。
util下是一些公共配合及方法:包含文章访问量和评论量缓存,验证码生成、图片属性获取、缓存管理统一入口、上传组件封装以及常量类等。
项目内部工具类大致如下图所示:
5 jfinal cms项目依赖
jfinal cms采用maven结构,依赖与作者的其他两个项目。
l jflyfox_base项目
jfinal_base提供了java的工具类,如StrUtils,NumberUtils,DateUtils 等
github:https://github.com/jflyfox/jflyfox_base
osc@git:http://git.oschina.net/flyfox/jflyfox_base
l jflyfox_jfinal项目
jflyfox_jfinal是对Jfinal等组件的封装。
包含controller,model,form,service基础类封装。
对分页进行了后台和前台的实现。
加入了自动扫描model和controller以及注解支持。
实现了ueditor后台Controller代码。
加入了页面增删改查代码自动生成功能,可通过beetl模板进行配置。
实现了SessionAttrInterceptor、页面和手机设备判断拦截器以及BasePathHandler。
github:https://github.com/jflyfox/jflyfox_jfinal
osc@git:http://git.oschina.net/flyfox/jflyfox_jfinal
6 联系作者
作者:FLY的狐狸
jfinal cms交流群:568909653
QQ:369191470
源码地址:http://git.oschina.net/flyfox/jfinal_cms
7 开源赞助
支付宝支付
微信支付