报警信息系统是司机系统研发团队微服务化的产物,它为司机业务涉及司机通知、警示牌、处罚单业务拆分奠定了基础。报警信息系统见证了专车业务快速发展以及司机研发团队的成长,因此我想围绕它做一篇技术研发回忆类文章。
一、导读
报警信息系统(ucaralarminfo
),最初于2015年年底开始搭建,最初是为一个司机通知的产品需求而诞生的。这个产品需求是对现有司机通知模块的升级优化,由最初的标题+文本内容的简约形式开始迈入通知分类频道+富文本内容时代,从最初的全量通知下发开始迈入多种场景策略下发时代。
这侧面反映专车业务的发展,恰好我入职的第二年2016年也是专车最辉煌的一年。本篇将重点讲述报警信息系统的架构与优化,希望读者可以从中收获,感谢大家。
二、项目背景
2015年10月8日,我从一家传统企业开始迈入一家互联网出行公司-神州专车,我所在的团队是神州专车业务的司机与订单研发团队。最初团队只有两个系统服务,司机系统(ucarintrdriver
)和订单系统(ucarorder
)。
这一年年底,我所在的研发团队评审通过一个司机通知业务版块的需求。后来我们团队研究分析,原有司机系统服务太重,司机通知业务与现有系统业务关联性不大,因此我们准备拆分新的应用服务,并使用独立的数据库,实现与原有系统的业务与系统解耦,为后续业务迭代奠定基础。
三、需求分析
这次产品需求上线后,具体要求有以下几点:
- 上线后,需要兼容司机客户端APP新老版本,上线后需要灰度一段,并不会全量升级。
- 上线后,运营后台司机通知功能菜单与老功能菜单需要并行一段时间,并不会下线原有功能。
该需求新功能,需要支持按照不同维度,包括:司机类别、大区、城市、指定人员等下发。司机通知会从属不同的通知频道,可以支持频道图片添加,司机已读未读区分。
四、技术设计
从系统架构角度分析,倘若我们拆分新的应用服务和数据库,我们需要面对两个问题:
- 上线时,应用服务并不会停机,这意味着新老服务数据需要双写。
- 上线后,司机历史通知怎么办,我们需要完成老数据库数据迁移到新库。
针对上述两个问题,既然思路已经有了,那就是技术方案实现的细节考虑了。
/**********************司机通知优化需求*******************************/
/*
* @author 石冬冬
* @date 2016/3/14
* @see 司机通知优化需求V1.0
*/
/*该表用途*/
/*1、通知类别表*/
/*建表语句*/
DROP TABLE IF EXISTS `t_notice_classify`;
create table t_notice_classify
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键自增id',
classify_name VARCHAR(20) NOT NULL COMMENT '类别名称',
status TINYINT NOT NULL COMMENT '状态(1:有效、0:无效)',
remark VARCHAR(256) DEFAULT NULL COMMENT '备注',
create_time DATETIME(3) NOT NULL COMMENT '创建时间',
create_emp BIGINT NOT NULL COMMENT '创建人',
modify_time DATETIME(3) NOT NULL COMMENT '修改时间',
modify_emp BIGINT NOT NULL COMMENT '修改人',
PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='通知类别表';
/*创建通知类别+状态唯一索引*/
create unique index udx_cname_status on t_notice_classify(classify_name,status);
/*该表用途*/
/*2、大区配置表*/
/*建表语句*/
DROP TABLE IF EXISTS `t_notice_region`;
create table t_notice_region
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键自增id',
region_name VARCHAR(20) NOT NULL COMMENT '大区名称',
status TINYINT NOT NULL COMMENT '状态(1:有效、0:无效)',
remark VARCHAR(256) DEFAULT NULL COMMENT '备注',
create_time DATETIME(3) NOT NULL COMMENT '创建时间',
create_emp BIGINT NOT NULL COMMENT '创建人',
modify_time DATETIME(3) NOT NULL COMMENT '修改时间',
modify_emp BIGINT NOT NULL COMMENT '修改人',
PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='大区配置表';
/*创建通知大区+状态唯一索引*/
create unique index udx_rname_status on t_notice_region(region_name,status);
/*该表用途*/
/*3、大区配置城市关联表*/
/*建表语句*/
DROP TABLE IF EXISTS `t_notice_region_res`;
create table t_notice_region_res
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键自增id',
region_id BIGINT NOT NULL COMMENT '大区ID',
city_id BIGINT NOT NULL COMMENT '城市ID',
PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='大区配置城市关联表';
create index idx_t_notice_region_res on t_notice_region_res(region_id,city_id);
/*该表用途*/
/*4、消息通知表 注意:(该表自动增长种子为7000000)*/
/*建表语句*/
DROP TABLE IF EXISTS `t_notice`;
create table t_notice
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键自增id',
classify_id BIGINT NOT NULL COMMENT '通知类别ID',
notice_no VARCHAR(20) NOT NULL COMMENT '通知编号',
notice_type TINYINT NOT NULL COMMENT '通知类型 1:全员通知 2:大区通知 3:城市通知 4:部分司机',
title_type TINYINT NOT NULL COMMENT '标题类型 1:文本标题 2:图文标题',
driver_type TINYINT NOT NULL COMMENT '司机类型 0:专职 1:兼职',
send_type TINYINT NOT NULL COMMENT '发送人类型 1:人工发送 2:系统发送',
title VARCHAR(100) NOT NULL COMMENT '通知标题',
summary VARCHAR(256) DEFAULT NULL COMMENT '通知概要',
link_url VARCHAR(256) DEFAULT NULL COMMENT 'ftp静态页面链接地址',
status TINYINT NOT NULL COMMENT '通知状态 0:已新建 1:已删除 2:已发送 3:已停发',
abandon TINYINT DEFAULT 1 COMMENT '数据是否废弃 0:废弃 1:保留',
remark VARCHAR(256) DEFAULT NULL COMMENT '备注',
create_time DATETIME(3) NOT NULL COMMENT '创建人',
create_emp BIGINT NOT NULL COMMENT '创建时间',
modify_time DATETIME(3) NOT NULL COMMENT '修改时间',
modify_emp BIGINT NOT NULL COMMENT '修改人',
PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='消息通知表';
/*创建通知数据的唯一索引,同一类别下专职、兼职有效存在的通知只能有一条*/
alter table t_notice AUTO_INCREMENT=8000000;
create index idx_cid_status_abandon_dtype on t_notice(classify_id,status,abandon,driver_type);
/*该表用途*/
/*5、消息通知内容表*/
/*建表语句*/
DROP TABLE IF EXISTS `t_notice_content`;
CREATE TABLE `t_notice_content` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`notice_id` BIGINT DEFAULT NULL COMMENT '消息通知ID',
`content` TEXT DEFAULT NULL COMMENT '消息通知内容',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='消息通知内容表';
create index idx_t_notice_content on t_notice_content(notice_id);
/*该表用途*/
/*6、消息附件表*/
/*建表语句*/
DROP TABLE IF EXISTS `t_notice_attach`;
create table t_notice_attach
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键自增id',
notice_id BIGINT NOT NULL COMMENT '消息通知ID',
attach_type TINYINT NOT NULL COMMENT '附件类型 1:图片 2:文档',
attach_url VARCHAR(256) DEFAULT NULL COMMENT '附件fpt存储地址',
attach_name VARCHAR(100) DEFAULT NULL COMMENT '附件名称',
PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='消息附件表';
create index idx_t_notice_attach on t_notice_attach(notice_id);
/*该表用途*/
/*7、消息通知下发配置表*/
/*建表语句*/
DROP TABLE IF EXISTS `t_notice_config`;
create table t_notice_config
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键自增id',
notice_id BIGINT DEFAULT NULL COMMENT '消息通知ID',
city_id BIGINT DEFAULT NULL COMMENT '城市ID',
driver_id BIGINT DEFAULT NULL COMMENT '司机ID',
PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='消息通知下发配置表';
create index idx_t_notice_config on t_notice_config(notice_id,driver_id,city_id);
/*该表用途*/
/*8、消息通知操作记录表*/
/*建表语句*/
DROP TABLE IF EXISTS `t_notice_operate_his`;
create table t_notice_operate_his
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键自增id',
notice_id BIGINT NOT NULL COMMENT '消息通知ID',
operate_type TINYINT NOT NULL COMMENT '操作类型 1:新建,2:修改,3:删除,4:下发',
operator BIGINT NOT NULL COMMENT '操作人ID',
operate_time DATETIME(3) NOT NULL COMMENT '操作时间',
PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='消息通知操作记录表';
create index idx_t_notice_operate_his on t_notice_operate_his(notice_id,operate_type);
/*该表用途*/
/*9、消息通知下发记录表 注意:(该表主键由系统生成)*/
/*建表语句*/
DROP TABLE IF EXISTS `t_notice_send`;
create table t_notice_send
(
id VARCHAR(32) NOT NULL COMMENT '主键自增id,使用UUID生成',
notice_id BIGINT NOT NULL COMMENT '消息通知ID',
classify_id BIGINT NOT NULL COMMENT '消息类别ID',
driver_id BIGINT NOT NULL COMMENT '配置司机ID',
read_status TINYINT NOT NULL COMMENT '阅读状态 0:未读 1:已读',
read_time DATETIME(3) DEFAULT NULL COMMENT '阅读时间',
send_time DATETIME(3) NOT NULL COMMENT '下发时间',
PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='消息通知下发记录表';
create index idx_t_notice_send on t_notice_send(driver_id,read_status,classify_id,notice_id);
4.1、数据双写
从司机系统分析,无论是对外提供的RPC服务还是运营端的通知下发,是代码实现来讲,都需要经过service层业务逻辑处理。
这意味着,我们可以选择在这个service层同步给新应用服务,最终我们选择MQ,而非RPC服务调用,是由于MQ业务解耦,不会因此服务提供方服务阻塞影响司机系统,同时定义出消息规范也能复用给其他业务系统对接司机通知业务版块。
与此同时,我们在新应用也要实现数据同步给司机系统,同样基于MQ实现。
一定会有读者朋友会提出疑问,为何没有基于binlog实现呢?我想告诉大家的是,原库只有两张表,而新库由于需求的复杂性,设计出来7、8张表,所以即便基于此方案,代码实现肯定没有基于MQ更简单。
4.2、数据同步
上线后,由于拆分一个独立的数据库,需要把历史通知同步到新库中。由于历史库,当时已经是百万数据量,这意味着倘若同步到新库中,新库表增长需要预留历史数据同步的空间,因此,我们考虑到新数据库表增长种子直接从百万开始。
老库增量同步
上线后,老库需要把增量数据同步到新库,我们采取在原来服务层,通过MQ,推送一个消息,然后新应用消费,并落库即可。
新库同步老库
由于老app并不会上线后就下线,意味着需要兼容老app,因此我们依然采用MQ,新应用写库时,同步推送MQ,然后老应用服务消费消息并写库即可。
4.3、通知下发
NoticeSendServiceImpl
/**
* Description:消息通知下发记录Service接口实现类 All Rights Reserved.
*
* @version 1.0 2016-3-16 上午11:09:57 by 石冬冬-Chris Suk 创建
*/
@Service("noticeSendService")
public class NoticeSendServiceImpl implements NoticeSendService {
private static Logger logger = LoggerFactory.getLogger(NoticeSendServiceImpl.class);
@Autowired
private DriverDao driverDao;
@Autowired
private NoticeSendDao noticeSendDao;
//
public static final Long BUS_TYPE = 19L;
@Override
public void send(HashCommands hashCmd,RedisLifeStrategyEnum redisLifeStrategyEnum,NoticeBean noticeBean,DriverQueryDTO driverQueryDTO){
boolean flag = true;
Long noticeId = null;
try {
final long start = System.currentTimeMillis();
int driverSendCount = 0;
if(null==noticeBean||null==driverQueryDTO){return;}
StringBuilder sb = new StringBuilder();
String sendTime = noticeBean.getSendTime();//通知下发时间
if(StringTools.isEmpty(sendTime)){
sendTime = TimeTools.format4YYYYMMDDHHMISS(new Date());
}
noticeId = noticeBean.getId();//通知ID
Long classifyId = noticeBean.getClassifyId();//通知类别ID
//modify by zhoumian
final boolean isPop = NumberUtil.isEquals(noticeBean.getIsPop(), DriverNoticeConstants.NoticeIsPopEnum.YES.getIndex());//获取通知是否弹屏
driverQueryDTO.setDriverStatus(DriverConstant.StatusEnum.VALID.getIndex());
driverQueryDTO.setIsDimission(DriverConstant.DimissionEnum.UNDIMISSIONENUM.getIndex());
//1、查询出通知对应要下发的相关司机
List<DriverQueryDTO> driverList = this.driverDao.queryListByCondition(driverQueryDTO);
if(CollectionUtils.isNotEmpty(driverList)){
//2、分组处理下发,每组500个司机 add by 石冬冬 on 2016/4/9
List<List<DriverQueryDTO>> groupList = CollectionsTools.group(driverList, CollectionsTools.GROUP_MAX_COUNT);
for(List<DriverQueryDTO> eachList : groupList){
//3、定义下发表对象集合
List<NoticeSendBean> sendList = new ArrayList<NoticeSendBean>();
final List<PushDriverMessage> messages = new ArrayList<PushDriverMessage>();
//4、迭代每组司机成员集合
for(DriverQueryDTO dto : eachList){
Long driverId = dto.getDriverId();
sb.append(driverId).append(StringTools.SYMBOL_SPLITOR);
NoticeSendBean noticeSendBean = new NoticeSendBean();
String sendId = StringTools.getUUID();
noticeSendBean.setId(sendId);
noticeSendBean.setNoticeId(noticeId);
noticeSendBean.setClassifyId(classifyId);
noticeSendBean.setDriverId(driverId);
noticeSendBean.setReadStatus(NoticeReadStatusEnum.UNREAD.getIndex());
noticeSendBean.setSendTime(sendTime);
sendList.add(noticeSendBean);
driverSendCount++;
if(isPop){
PushDriverMessage message = new PushDriverMessage();
DriverNoticePushPo po = new DriverNoticePushPo();
po.setPushType(19L);
String content = JSONObject.toJSONString(po);
message.setDriverId(driverId);
message.setContent(content);
message.setBusType(BUS_TYPE);
message.setRetryExpiry(0L);
messages.add(message);
}
buildPushDriverMessage(driverId, new PushBuilderCallback() {
@Override
public boolean judge() {
return isPop;
}
@Override
public void after(PushDriverMessage message) {
messages.add(message);
}
});
if(redisLifeStrategyEnum == RedisLifeStrategyEnum.REPLACE){
NoticeRedisUtil.setNoticeSendRedis(hashCmd,driverId, classifyId, sendId);
}
}
//5、每组500个元素批量插入通知下发记录表
this.noticeSendDao.insertByBatch(sendList);
//发送弹窗push
if(isPop && CollectionsTools.isNotEmpty(messages)){
pushNotice(messages);
}
}
}
String outDriverIds = StringTools.removeLastSymb(sb.toString());
HbaseLogUtil.sendHbaseLogMQ(noticeId, LogEnum.Alarminfo.LogTypeEnum.NOTICE_ISSUE, "【司机通知下发】通知下发,司机="+driverSendCount+"个,noticeBean="+JSON.toJSON(noticeBean), null,noticeBean,outDriverIds);
outDriverIds = StringTools.truncate(outDriverIds, StringTools.MAX_LENGTH);
//logger.error(TimeOuter.format(System.currentTimeMillis()-start,"【司机通知下发】通知下发,司机="+driverSendCount+"个,noticeBean="+JSON.toJSON(noticeBean)+",司机ID={"+outDriverIds+"}"));
} catch (Exception e) {
logger.error("【司机通知-下发】批量插入下发表异常",e);
HbaseLogUtil.sendHbaseLogMQ(noticeId, LogEnum.Alarminfo.LogTypeEnum.NOTICE_ISSUE, "【司机通知下发】批量插入下发表异常", e, redisLifeStrategyEnum,noticeBean,driverQueryDTO);
throw new BizException("【司机通知】批量下发异常,noticeBean={"+JSON.toJSON(noticeBean)+"}"+e);
}
}
@Override
public Long insert(NoticeSendBean noticeSendBean) {
return this.noticeSendDao.insert(noticeSendBean);
}
@Override
public void updateById(String noticeSendId) {
this.noticeSendDao.updateById(noticeSendId);
}
@Override
public Integer getCountByNoticeSendBean(Long driverId, Integer readStatus) {
NoticeSendBean noticeSendBean = new NoticeSendBean();
noticeSendBean.setReadStatus(readStatus);
noticeSendBean.setDriverId(driverId);
return this.noticeSendDao.getCountByNoticeSendBean(noticeSendBean);
}
@Override
public Integer getCountOfNoitceSend(Long driverId, NoticeDTO noticeDTO) {
NoticeSendBean bean = new NoticeSendBean();
bean.setClassifyId(noticeDTO.getClassifyId());
bean.setDriverId(driverId);
return this.noticeSendDao.getCountOfNoitceSend(bean);
}
/**
* 批量push信息
* @param messages
*/
private void pushNotice(List<PushDriverMessage> messages){
try {
logger.info("开发发送push:",JSON.toJSON(messages));
RemoteClientUtil.getTCPClient().executeToObject("ucarintrdriver.emptyDispatch.vehicle.V1.pushDriverListMessage", messages);
} catch (Exception e) {
logger.error("司机通知弹窗调用接口异常:messages={}",JSON.toJSON(messages),e);
}
}
/**
* Description: 构建司机Push<br>
* @version V1.0 2017/5/30 18:34 by 石冬冬-Seig Heil(dd.shi02@zuche.com)创建
* @param driverId
* @param callback
* @return
*/
private void buildPushDriverMessage(Long driverId,PushBuilderCallback callback){
if(callback.judge()){
PushDriverMessage message = new PushDriverMessage();
DriverNoticePushPo po = new DriverNoticePushPo();
po.setPushType(19L);
String content = JSONObject.toJSONString(po);
message.setDriverId(driverId);
message.setContent(content);
message.setBusType(BUS_TYPE);
message.setRetryExpiry(0L);
callback.after(message);
}
}
/**
* 构建发送Push的接口
*/
private interface PushBuilderCallback{
/**
* 是否需要添加到下发push中
* @return
*/
boolean judge();
/**
* 把构建好的对象暴露给调用发
* @param message
*/
void after(PushDriverMessage message);
}
}
4.4、通知详情
司机通知基于KindEditor富文本编辑器编辑内容,然后发布内容,基于Freemark模板引擎生成静态页面,然后上传到FTP服务器,司机app直接基于CDN访问静态页面即可。
富文本编辑
JSP页面
<!DOCTYPE html>
<%@ page contentType="text/html;charset=utf-8"%>
<%@include file="/inc/metaData.jsp"%>
<html xmlns="http://www.w3.org/1999/xhtml">
<c:set var="pageTitle" value="${act eq 'view' ? '查看' : '编辑' }"/>
<head>
<meta charset="utf-8" />
<title>${pageTitle}通知内容</title>
<link href="${basePath}/static/plugins/kindeditor-4.1.7/themes/default/default.css?v=${suzeVersion}" rel="stylesheet" type="text/css"/></link>
<script src="${basePath}/static/plugins/kindeditor-4.1.7/kindeditor-all.js?v=${suzeVersion}" type="text/javascript"></script>
<%@include file="/inc/metaDataResource.jsp" %>
<style type="text/css">
.Hint{background-color:#fff;display:block;height:25px;line-height:25px;padding:0px 5px;border:1px solid #2F7FC1;color:#ff0000;}
</style>
</head>
<body>
<p></p>
<div class="order_information pb10" style="min-width:455px;">
<form id="contentForm" method="post">
<div id="order_information_fixed" class="bg_white">
<div class="brumbtext_ucar gray_666">
<h1 class="bold fl f14">
${pageTitle}通知内容
<input type="hidden" id="act" name="act" value="${act}" />
<input type="hidden" id="id" name="id" value="${noticeBean.id}" />
<input type="hidden" id="title" name="title" value="${noticeBean.title}" />
<input type="hidden" id="titleType" name="titleType" value="${noticeBean.titleType}" />
<input type="hidden" id="createTime" name="createTime" value="${noticeBean.createTime}" />
<input type="hidden" id="contentId" name="contentId" value="${noticeBean.contentId}" />
<input type="hidden" id="classifyName" name="classifyName" value="${noticeBean.classifyName}" />
</h1>
<div class="fr p5">
<input name="clearBtn" id="clearBtn" type="button" class="blue_btn_bg" value="清除通知内容" onClick="Suze.doClear()" />
<input name="saveBtn" id="saveBtn" type="button" class="blue_btn_bg" value="保存" onClick="Suze.doSave()" />
<input name="exitBtn" id="exitBtn" type="button" class="blue_btn_bg" value="关闭" onClick="window.close();" />
</div>
</div>
</div>
<!--通知内容-->
<div style="padding-top: 35px;">
<div class="bg_gray_ED p10 m10">
<div class="bg_white disType1">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td><h2 class="bold f14 p5">通知内容</h2></td>
</tr>
</table>
<table class="tab_information" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td class="black" width="100%">
<div class="Hint">图片不大于1MB,通知内容支持最多30张图片。</div>
</td>
</tr>
<tr>
<td class="black" width="100%">
<textarea id="content" name="content" style="width:455px;height:100px;">${noticeBean.content}</textarea>
</td>
</tr>
</table>
</div>
</div>
</div>
</form>
</div>
</body>
</html>
<script type="text/javascript" src="${basePath }/static/driverNotice/notice/noticeContent.js?v=${suzeVersion}"></script>
JS
/**
* 通知内容编辑器JS类
* @author 石冬冬(ddshi@1010111.com)创建
* @version 1.0 2016/3/27
*/
var Suze={
/**
* 全局变量
*/
Cts:{
urlPrefix:"/notice/",
KE:null//编辑器对象
},
/**
* 初始化入口
*/
init:function(){
this.initBtns();
this.initKE();
},
/**
* 初始化按钮
*/
initBtns:function(){
var act = $('#act').val();
var contentLength = $('#contentLength').val();
if(contentLength=='true'){
$('#clearBtn').hide();
}
if(act=='view'){
$('#clearBtn').hide();
$('#saveBtn').hide();
}
},
/**
* 初始化编辑器,使用KindEditor
*/
initKE:function(){
var ke = null;
var noticeId = $('#id').val();
var urlType = 'cdn';
if(G.cloudSwitch==true){
urlType = 'cloud';
}
//默认插件
/*var items = [
'source', '|', 'undo', 'redo', '|', 'preview', 'print', 'template', 'code', 'cut', 'copy', 'paste',
'plainpaste', 'wordpaste', '|', 'justifyleft', 'justifycenter', 'justifyright',
'justifyfull', 'insertorderedlist', 'insertunorderedlist', 'indent', 'outdent', 'subscript',
'superscript', 'clearhtml', 'quickformat', 'selectall', '|', 'fullscreen', '/',
'formatblock', 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold',
'italic', 'underline', 'strikethrough', 'lineheight', 'removeformat', '|', 'image', 'multiimage',
'flash', 'media', 'insertfile', 'table', 'hr', 'emoticons', 'baidumap', 'pagebreak',
'anchor', 'link', 'unlink', '|', 'about'
];*/
var items = [
'source','clearhtml', '|', 'justifyleft', 'justifycenter', 'justifyright','justifyfull', 'insertorderedlist','insertunorderedlist',
'|','formatblock', 'fontname','fontsize', '|', 'forecolor', 'hilitecolor',
'bold','italic', 'underline', 'strikethrough', 'lineheight', 'removeformat', '|', 'image','link', 'unlink', '|', 'preview'
];
//图片上传的地址
var uploadJsonURL = CAR_PATH + "/imageUpload/upload.do_?temp="+Math.random();
KindEditor.ready(function(K) {
ke=K.create('#content', {
allowFileManager : true,
items:items,
resizeType:1,
filterMode : true,
height:'530px',
width:'100%',
extraFileUploadParams : {
classifyId:noticeId,//通知ID
modelType:14,
special:true,
cloud:G.cloud,
urlType:urlType,
imgMaxWidth:-1,//图片尺寸最大宽度
imgMaxHeight:-1,//图片尺寸最大高度
imgMaxSize:1024*1024//默认图片1MB
},
uploadJson:uploadJsonURL
});
Suze.Cts.KE = ke;
var url = location.search;
if (url.indexOf("act=view") > 0) {
ke.readonly(true);
}
});
},
/**
* 编辑器内容校验
* @param content
*/
verify:function(content) {
var msg = null;
var str = content;
//以下是为了检查编辑器中的空格,防止全敲入空格提交表单
str=str.replace(/ /g,"");
str=str.replace(/\s/g,"");
if(str.length==0){
return '请输入内容';
}
//限制图片数量
if((str.split('<img')).length-1>30)
{
return '通知内容图片数量不能大于30';
}
//去除图片标签,验证文字长度
if(str.replace(/<img(?:.|\s)*?>/g, '').length>4000){
return '通知内容字符不能大于4000个';
}
},
/**
* 保存通知内容
*/
doSave:function(){
var url = CAR_PATH + this.Cts.urlPrefix + "doModifyContent.do_?temp="+Math.random();
var content = $.trim(this.Cts.KE.html());
$('#content').val(content);
var classifyId = $('#id').val();
var text = $.trim(this.Cts.KE.text());
//text = text.replace(/<img(?:.|\s)*?>/g,'');//再次去掉里边所有的img标签元素 add by 石冬冬 on 2016/4/5
var verify = this.verify(text);
if(null!=verify){alert(verify);return;}
if(!classifyId){alert('对象ID为空');return;}
var data = $('#contentForm').serialize();
$.ajax({
url: url,
type:'POST',
data: data,
timeout: 6000,
async : false,
success: function(rs){
alert(rs.messages);
//window.opener.Suze.doSearch(); 无需刷新
window.close();
},
error: function(jqXHR, status, err){
alert(err);
}
});
},
/**
* 清除通知内容
*/
doClear:function(){
if(confirm('确定要清除通知内容吗?')){
var url = CAR_PATH + this.Cts.urlPrefix + "doClearContent.do_?temp="+Math.random();
var classifyId = $('#id').val();
if(!classifyId){alert('对象ID为空');return;}
var data = $('#contentForm').serialize();
$.ajax({
url: url,
type:'POST',
data: data,
timeout: 6000,
async : false,
success: function(rs){
alert(rs.messages);
window.opener.Suze.doSearch();
window.close();
},
error: function(jqXHR, status, err){
alert(err);
}
});
}
}
};
Suze.init();
静态页面生成
当时由于我们团队都是服务端开发,而且负责的后端运营系统基于SpringMVC+JSP,并没有前后端分离,这就意味着我们服务端承担着大量的前端开发工作,当时最流行的莫属jQuery了。我们为了给运营端发布司机通知内容,基于国内开源的富文本编辑器KindEditor定制开发了相关组件,同时为了发布司机通知,服务端通过模板引擎生成静态页面,我通过调研了技术栈,选用了基于jQueryMobile技术。事实上,期间为了考虑不通app的兼容性,由于app打开页面,基于手机自带的浏览器因此,我测试了许久才使得验证通过。
定义模板
基于FreeMark模板引擎技术。
<!--
@see 该页面用于司机消息通知富文本保存页面使用,基于jquery mobile
@author:石冬冬
@date 2016/3/15
@version 1.0
@email dd.shi02@zuche.com
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.css">
<script src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
<script src="http://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.js"></script>
<title>消息详情</title>
<!-- 让图片永远自适应任何手机屏幕尺寸 -->
<style type="text/css">
img{max-width:100%;}
body,p,div{background-color:#EFEFEF;word-wrap: break-word;}
.title,.subTitle,.attachList{text-align:center;}
</style>
</head>
<body style="background-color:#EFEFEF;">
<div data-role="page">
<div data-role="content">
<h2 class="title">${title}</h2>
<h3 class="subTitle">${classifyName} ${createTime}</h3>
<!-- 基于freemark表达式迭代,附件类型{1:图片 2:文档} add by 石冬冬 on 2016/03/27 -->
<#if attachList??>
<p class="attachList">
<#list attachList as attach>
<#if attach.attachType == 1>
<img alt="" src="${attach.attachUrl}">
</#if>
<#if attach.attachType == 2>
<a href="${attach.attachUrl}"/>
</#if>
</#list>
</p>
</#if>
<p>${content}</p>
</div>
</div>
</body>
</html>
通知详情模板
<!--
@see 该页面用于警示类型完整攻略富文本保存页面使用,基于jquery mobile
@author:石冬冬
@date 2015/12/25
@version 1.0
@email dd.shi02@zuche.com
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.css">
<script src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
<script src="http://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.js"></script>
<title>通知详情</title>
<!-- 让图片永远自适应任何手机屏幕尺寸 -->
<style type="text/css">
img{max-width:100%;}
body,p,div{background-color:#EFEFEF;word-wrap: break-word;}
.ui-content{padding:0px 15px 0px 15px;}
.container{padding-top:0px;margin:0px;}
.ui-body-c, .ui-overlay-c{color:#333;text-shadow:0 1px 0 #fff;background:#EFEFEF;background-image:linear-gradient(#EFEFEF,#EFEFEF);}
</style>
</head>
<body style="background-color:#EFEFEF;">
<div data-role="page">
<div data-role="content">
<p class="container">${content}</p>
</div>
</div>
</body>
</html>
HtmlUtils
package com.ucar.alarm.utils;
/**
* Description:使用Freemark生成静态页面,并上传到Ftp服务器
* All Rights Reserved.
* @version 1.0 2015-12-23 下午4:16:58 by 石冬冬-Chris Suk创建
*/
public class HtmlUtils {
private static final Logger logger = LoggerFactory.getLogger(HtmlUtils.class);
/**
* 静态页面文件后缀
*/
public static final String FILE_PREFIX = ".html";
/**
* 类文件路径
*/
public static final String CLASS_PATH = HtmlUtils.class.getResource("/").getPath();
/**
* 存储freemark模板目录
*/
public static final String TEMPLETE_PATH = (CLASS_PATH.indexOf("WEB-INF") > 0 ? CLASS_PATH.substring(0, CLASS_PATH.indexOf("WEB-INF")) : CLASS_PATH) + File.separator + "templete" + File.separator;
/**
* ftp服务器地址
*/
public static final String FTP_CDN_SERVER = FtpUtil.getConfigProperties().getProperty("ftp.zuche.cdnServer");
/**
* ftp上传路径配置文件
*/
private static final String FTP_UPLOAD_CONFIG = "ftpUploadPathConfig.properties";
/**
* ftp上传静态页面配置文件key,见配置文件ftpUploadPathConfig.properties
*/
public static final String FTP_UPLOAD_HTML_KEY = "12";
/**
* ftp上传图片配置文件key,见配置文件ftpUploadPathConfig.properties
*/
public static final String FTP_UPLOAD_PIC_KEY = "11";
/**
* ftp上传静态页面目录
*/
public static final String FTP_HTML_PATH = PropertiesUtil.getMe().getProperties(FTP_UPLOAD_CONFIG, FTP_UPLOAD_HTML_KEY);
/**
* ftp上传图片目录
*/
public static final String FTP_PIC_PATH = PropertiesUtil.getMe().getProperties(FTP_UPLOAD_CONFIG, FTP_UPLOAD_PIC_KEY);
/**
* ftp上传静态页面目录前缀
*/
public static final String FTP_HTML_PREFIX = FtpUtil.getConfigProperties().getProperty("ftp.zuche.remotePathHtml");
/**
* ftp上传图片目录前缀
*/
public static final String FTP_PIC_PREFIX = FtpUtil.getConfigProperties().getProperty("ftp.zuche.remotePathPic");
/**
* Description:定义freemark静态页面模板枚举接口
* All Rights Reserved.
* @version 1.0 2015-12-24 上午9:24:45 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
*/
public enum Templete{
W3C("strategy_w3c.html"), MOBILE("strategy_mobile.html");
private final String value;
Templete(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
/**
* Description:定义司机消息通知-freemark静态页面模板枚举接口
* All Rights Reserved.
* @version 1.0 2016-3-15 上午11:07:49 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
*/
public enum NoticeTemplete{
MOBILE("notice_mobile.html"),DEFAULT("default_mobile.html");
private final String value;
NoticeTemplete(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
private HtmlUtils(){}
/**
* Description: 使用freemark模板刷新内容并返回页面HTML片段
* @Version1.0 2015-12-23 下午4:02:15 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param paramMap freemark模板变量
* @param templete freemark模板文件
* @return
*/
public static String generate(Map<String, Object> paramMap,final String templete){
String html = null;
BufferedWriter writer = null;
try {
//创建一个合适的Configration对象
Configuration configuration = new Configuration();
configuration.setDirectoryForTemplateLoading(new File(TEMPLETE_PATH));
configuration.setObjectWrapper(new DefaultObjectWrapper());
configuration.setDefaultEncoding("UTF-8"); //这个一定要设置,不然在生成的页面中 会乱码
//获取或创建一个模版。
Template template = configuration.getTemplate(templete);
StringWriter stringWriter = new StringWriter();
writer = new BufferedWriter(stringWriter);
template.process(paramMap, writer);
writer.flush();
html = stringWriter.toString();
} catch (Exception e) {
logger.error("freemark模板解析异常,paramMap={},templete={}:", JSON.toJSON(paramMap),templete, e);
}finally{
try {
if(null!=writer){
writer.close();
}
} catch (IOException e) {
logger.error("BufferedWriter释放资源异常:", e);
}
}
return html;
}
/**
* Description:把静态页面内容上传到ftp并生成文件,并返回文件地址
* @Version1.0 2015-12-23 下午4:25:41 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param prefix 静态页面文件目录
* @param fileName 静态页面文件名称
* @param html 静态页面HTML片段内容
* @param hasCdn 地址是否包含CDN主机
* @return 返回ftp静态页面访问地址
*/
public static String create(String prefix,String fileName,String html,boolean hasCdn){
String linkUrl = null;
ByteArrayInputStream in = null;
try {
FtpUtil ftpUtil = FtpUtil.getInstance();
ftpUtil.connectServer();
byte[] arr = html.getBytes("UTF-8");
in = new ByteArrayInputStream(arr);
prefix = (null==prefix || "".equals(prefix)) ? "" : prefix;
String ftpPath = (null==prefix || "".equals(prefix)) ? FTP_HTML_PATH : FTP_HTML_PATH + prefix;
boolean flag = ftpUtil.upload(ftpPath, fileName, in);
if(flag){
linkUrl = (hasCdn ? FTP_CDN_SERVER : "") + FTP_HTML_PREFIX + prefix + fileName;
}
ftpUtil.closeConnect();
} catch (Exception e) {
logger.error("ftp上传静态页面异常prefix={},fileName={},html={},hasCdn={}:",prefix,fileName,html,hasCdn, e);
}finally{
try {
if(null!=in){
in.close();
}
} catch (IOException e) {
logger.error("ByteArrayInputStream释放资源异常:", e);
}
}
return linkUrl;
}
/**
* Description: 返回文件名称
* @Version1.0 2016-3-15 上午11:10:42 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @return
*/
public static String getUUFileName(){
return UUID.randomUUID().toString() + FILE_PREFIX;
}
/**
* Description:获取CDN url
* @Version1.0 2016-3-28 下午6:31:54 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param url 原url
* @return
*/
public static String getUrlWithCdn(String url){
if("".equals(url)||null==url){
return url;
}
return FTP_CDN_SERVER + url;
}
/**
* 格式化内容删除input字符串中的html格式
* @param content 字符内容
* @param length 截取字符长度
* @return
*/
public static String formatHtml(String content, int length) {
if (content == null || content.trim().equals("")) {
return "";
}
// 去掉所有html元素,
String str = content.replaceAll("\\&[a-zA-Z]{1,10};", "").replaceAll( "<[^>]*>", "");
str = str.replaceAll("[(/>)<]", "");
int len = str.length();
if (length==-1||len <= length) {
return str;
} else {
str = str.substring(0, length);
str += "...";
}
return str;
}
/**
* Description: 去除空格,tab键生成的HTML标签
* @Version1.0 2016-4-4 下午8:55:46 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param content
* @return
*/
public static String removeBlank(String content){
if (content == null || content.trim().equals("")) {
return "";
}
content=content.replaceAll("\r\n", "").replaceAll(" ", "");
return content;
}
public static String formatHtml(String content) {
return formatHtml(content, -1);
}
}
NoticeServiceImpl#saveContent
@Override
public Message saveContent(NoticeBean noticeBean, boolean isSync) {
Message msg = new Message();
msg.setSuccess(true);
List<String> mesList = new ArrayList<String>();
try {
Long noticeId = noticeBean.getId();
Long contentId = noticeBean.getContentId();
NoticeHisBean nhb = new NoticeHisBean();
nhb.setNoticeId(noticeId);
nhb.setOperateType(NoticeOperateHisTypeEnum.MODIFY.getIndex()+"");
User user = RequestContext.getUser();
Long userId = 1l;
if (user != null) {
userId = NumberUtil.parseLong(user.getUserIdNum());
}
nhb.setOperator(userId.intValue());
if (!isSync) {
noticeHisDao.insertHis(nhb);
}
String content = noticeBean.getContent();
NoticeContentBean noticeContentBean = new NoticeContentBean(contentId,noticeId,content);
if(null!=contentId && !NumberUtil.isEquals(contentId, 0)){
this.noticeContentDao.update(noticeContentBean);
}else{
this.noticeContentDao.insert(noticeContentBean);
}
//1、判断是否通用模板
String templeteName = NoticeTempleteUtils.NoticeContentCommEnum.getNameByContent(content);
String linkUrl = null;
boolean exists = false;
//2、通用模板时,获取系统已经预置的模板
if(null!=templeteName){
linkUrl = NoticeTempleteUtils.getLinkUrl(templeteName);
exists = true;
}
if(null==linkUrl){exists = false;}
//3、如果依然得不到模板地址,将重新生成一个
if(!exists){
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("content",null == content ? "" : content);
String html = HtmlUtils.generate(paramMap,NoticeTemplete.DEFAULT.getValue());
String fileName = HtmlUtils.getUUFileName();
String prefix = String.valueOf(noticeId) + "/";
linkUrl = HtmlUtils.create(prefix,fileName, html , false);
}
HbaseLogUtil.sendHbaseLogMQ(noticeId, LogEnum.Alarminfo.LogTypeEnum.NOTICE_TEMPLETE_CREATE, "【司机通知内容模板】静态页面输出", null, linkUrl);
//logger.error(TimeOuter.format("【通知内容】生成静态页面地址:" + HtmlUtils.FTP_CDN_SERVER + linkUrl));
//4、更新通知模板
noticeBean.setLinkUrl(linkUrl);
//再更新对象的linkUrl属性
this.noticeDao.updateLinkUrl(noticeBean);
mesList.add("保存成功!");
} catch (Exception e) {
msg.setSuccess(false);
mesList.add("保存通知内容失败");
logger.error("保存通知内容异常", e);
}
msg.setMessages(mesList);
return msg;
}
AlarmClassifyServiceImpl#saveStrategy
@Transactional(readOnly = false, rollbackFor = Throwable.class)
@Override
public Message saveStrategy(HttpServletRequest request,AlarmClassifyBean alarmClassifyBean) {
Message msg = new Message();
msg.setSuccess(true);
List<String> mesList = new ArrayList<String>();
try {
if(null==alarmClassifyBean){
msg.setSuccess(false);
mesList.add("对象为空");
}else{
Long classifyId = alarmClassifyBean.getId();
Long strategyId = alarmClassifyBean.getStrategyId();
String content = alarmClassifyBean.getStrategy();
if(null!=strategyId && strategyId != 0){
AlarmStrategyBean alarmStrategyBean = new AlarmStrategyBean(strategyId,classifyId, content);
this.alarmStrategyDao.update(alarmStrategyBean);
}else{
AlarmStrategyBean alarmStrategyBean = new AlarmStrategyBean(classifyId, content);
strategyId=this.alarmStrategyDao.insert(alarmStrategyBean);
}
//以下为通过freemark生成静态页面,并上传ftp服务器。
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("content",content);
String html = HtmlUtils.generate(paramMap,HtmlUtils.Templete.MOBILE.getValue());
String fileName = UUID.randomUUID().toString() + ".html";
String prefix = String.valueOf(classifyId) + "/";
String linkUrl = HtmlUtils.create(prefix,fileName, html , false);
logger.error(TimeOuter.format("生成静态页面地址:"+HtmlUtils.FTP_CDN_SERVER+linkUrl));
alarmClassifyBean.setStrategyId(strategyId);
alarmClassifyBean.setLinkUrl(linkUrl);
//再更新对象的linkUrl属性
this.alarmClassifyDao.updateLinkUrl(alarmClassifyBean);
mesList.add("保存成功!");
}
} catch (Exception e) {
msg.setSuccess(false);
logger.error("保存异常", e);
mesList.add("保存失败");
}
msg.setMessages(mesList);
return msg;
}
AlarmImageUploadController
主要用于富文本编辑器上传图片,需要写入到FTP服务器
/**
* Description:富文本编辑器图片FTP上传
* All Rights Reserved.
* @version 1.0 2015-12-22 下午5:02:40 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
*/
@Controller
@RequestMapping("/alarmImageUpload")
public class AlarmImageUploadController{
public static final long IMG_K = 1024;
public static final long IMG_M = 1048576;
public static final long IMG_G = 1073741824;
private static Logger LOGGER = LoggerFactory.getLogger(AlarmImageUploadController.class);
private static String[] FILE_TYPES = {"gif","jpg","jpeg","png","bmp"};
private static final String FTP_CDN_SERVER = HtmlUtils.FTP_CDN_SERVER;
/**
* Description:图片上传
* @Version1.0 2015-12-24 上午9:40:53 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param request
* @param response
*/
@RequestMapping("/upload")
public void upload(HttpServletRequest request, HttpServletResponse response){
JSONObject resultJSON = new JSONObject();
String message = "";
int error = 0;
String imageUrl = "";
String fileName = null,fileType=null;//上传图片名称,文件类型
boolean verify = true;
ByteArrayInputStream in = null;
String classifyId = null;
String newFile = "N";//是否系统生成文件名{Y:系统生成 N:原文件}
try {
response.setContentType("text/html;charset=UTF-8");
if (!(request instanceof MultipartHttpServletRequest)) {
verify = false;
error=1;
message = "无法获取上传的文件!";
}
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
Map para = multipartRequest.getParameterMap();
Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
String imgMaxSize = getUploadParam(para,"imgMaxSize");
classifyId = getUploadParam(para,"classifyId");;
newFile = getUploadParam(para,"newFile");
CommonsMultipartFile item = (CommonsMultipartFile) fileMap.get("imgFile");
long imgSize = item.getSize();
in = new ByteArrayInputStream(item.getBytes());
FileItem fileItem = item.getFileItem();
fileName = fileItem.getName();
fileType = fileName.substring(fileName.lastIndexOf(".")+1).toLowerCase();
if(!Arrays.asList(FILE_TYPES).contains(fileType)){
verify = false;
error=1;
message = "图片只支持gif,jpg,jpeg,png,bmp文件类型";
}
if(imgSize>Long.valueOf(imgMaxSize)){
verify = false;
error=1;
message = "图片大小不能大于1MB";
}
if(verify){
FtpUtil ftpUtil = FtpUtil.getInstance();
ftpUtil.connectServer();
String prefix = (null == classifyId || "".equals(classifyId)) ? "" : ( classifyId + "/" );
String uploadPath = HtmlUtils.FTP_PIC_PREFIX + prefix + getFtpRemoteFileDir();
String ftpUploadPath = getRemoteFileDir(prefix);
if(!verifyImg(para, in)){
message = "图片尺寸大小不符合";
error=1;
verify = false;
}else{
in.reset();
String ftpFileName = UUID.randomUUID().toString()+"."+fileType;
if("Y".equals(newFile)){
ftpFileName = fileName;
}
boolean flag = ftpUtil.upload(ftpUploadPath, ftpFileName, in);
if (!flag) {
error=1;
message = "图片上传失败";
}
imageUrl = FTP_CDN_SERVER + uploadPath + ftpFileName;
}
if(verify){
message = "图片上传成功";
error=0;
}
ftpUtil.closeConnect();
}
}catch (Exception e) {
error=1;
message = "上传文件失败:"+e.getLocalizedMessage();
LOGGER.error(e.getMessage(), e);
}finally{
try {
if(null!=in){in.close();}
} catch (IOException e) {
HbaseLogUtil.sendHbaseLogMQ(0L, LogEnum.Alarminfo.LogTypeEnum.FTP_UPLOAD_IMAGE, "ByteArrayInputStream释放异常:", e, classifyId,fileName);
error=1;
message = "ByteArrayInputStream释放异常:"+e.getLocalizedMessage();
LOGGER.error("ByteArrayInputStream释放异常:", e);
}
}
resultJSON.put("error", error);
resultJSON.put("message", message);
resultJSON.put("url", imageUrl);
resultJSON.put("fileName", fileName);
resultJSON.put("fileType", fileType);
try {
LOGGER.error(resultJSON.toJSONString());
response.getWriter().print(resultJSON.toJSONString());
} catch (IOException e) {
HbaseLogUtil.sendHbaseLogMQ(0L, LogEnum.Alarminfo.LogTypeEnum.FTP_UPLOAD_IMAGE, "图片上传IOException处理异常:", e, classifyId,fileName);
LOGGER.error(e.getMessage(), e);
}
}
/**
* Description: 获取图片上传目录
* @Version1.0 2015-12-22 上午10:58:35 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @return
*/
private static String getFtpRemoteFileDir() {
StringBuilder remoteFileDir = new StringBuilder();
SimpleDateFormat sf = new SimpleDateFormat("yyyyMMdd");
String datePath = sf.format(new Date());
remoteFileDir.append(datePath).append("/");
return remoteFileDir.toString();
}
/**
* Description:获取ftp图片目录
* @Version1.0 2015-12-24 下午3:18:20 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @return
*/
private static String getRemoteFileDir(String prefix) {
StringBuilder remoteFileDir = new StringBuilder();
String dir = HtmlUtils.FTP_PIC_PATH + prefix;
remoteFileDir.append(dir);
SimpleDateFormat sf = new SimpleDateFormat("yyyyMMdd");
String datePath = sf.format(new Date());
remoteFileDir.append(datePath).append("/");
return remoteFileDir.toString();
}
/**
* Description:获取上传参数
* @Version1.0 2016-5-5 下午1:25:42 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param para
* @param key 参数
* @return
*/
private static String getUploadParam(Map para,String key){
Object obj = para.get(key);
if(null!=obj){
String[] objArray = (String[])obj;
return objArray[0];
}
return null;
}
/**
* Description:图片校验
* @Version1.0 2015-12-22 下午5:32:04 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param para
* @param in
* @return
*/
private static boolean verifyImg(Map<String, Object> para,InputStream in){
boolean flag = true;
try {
String imgMaxWidth = getUploadParam(para,"imgMaxWidth");
String imgMaxHeight = getUploadParam(para,"imgMaxHeight");
if(null!=imgMaxWidth && null!=imgMaxHeight){
int maxWidth = Integer.parseInt(imgMaxWidth);
int maxHeight = Integer.parseInt(imgMaxHeight);
BufferedImage img = ImageIO.read(in);
int width = img.getWidth();
int height = img.getHeight();
if(width>maxWidth||height>maxHeight){
flag = false;
}
}
} catch (Exception e) {
LOGGER.error("【图片导入】图片校验处理异常", e);
}
return flag;
}
public String FormetFileSize(long fileS) {
DecimalFormat df = new DecimalFormat("#.00");
String fileSizeString = "";
if (fileS < 1024) {
fileSizeString = df.format((double) fileS) + "B";
} else if (fileS < 1048576) {
fileSizeString = df.format((double) fileS / 1024) + "K";
} else if (fileS < 1073741824) {
fileSizeString = df.format((double) fileS / 1048576) + "M";
} else {
fileSizeString = df.format((double) fileS / 1073741824) + "G";
}
return fileSizeString;
}
}
NoticeTempleteUtils
/**
* Description:司机通知FTP静态页面工具类 All Rights Reserved.
* @version 1.0 2016-4-14 上午9:33:39 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
*/
public class NoticeTempleteUtils {
private static final Logger logger = Logger.getLogger(NoticeTempleteUtils.class);
private static final String TEMPLETE_URL_PREFIX = "templete/notice/";
private static final String TEMPLETE_REDIS_PREFIX = "notice:templete";
private static final Map<String, String> localLinkUrlMap = new ConcurrentHashMap<String, String>();
static{
init();
}
/**
* Description:统一初始化模板入口
* @Version1.0 2016-4-14 上午9:44:22 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @return
*/
private static void init(){
try {
HashCommands cmd = NoticeRedisUtil.getHashCmd();
Map<String, Object> paramMap = new HashMap<String, Object>();
Map<String, String> linkUrlMap = new HashMap<String, String>();
for (NoticeContentCommEnum c : NoticeContentCommEnum.values()) {
paramMap.put("content",c.getContent());
String html = HtmlUtils.generate(paramMap,NoticeTemplete.DEFAULT.getValue());
String linkUrl = HtmlUtils.create(TEMPLETE_URL_PREFIX,c.name, html , false);
linkUrlMap.put(c.name, linkUrl);
localLinkUrlMap.put(c.name, linkUrl);
}
cmd.putAll(RedisConstant.NamespaceEnum.NOTICE.getIndex(), TEMPLETE_REDIS_PREFIX, linkUrlMap);
logger.error(TimeOuter.format("统一初始化模板,Redis_linkUrlMap="+JSON.toJSON(linkUrlMap)+",Local_LINKURLMAP="+JSON.toJSON(localLinkUrlMap)));
HbaseLogUtil.sendHbaseLogMQ(0L, LogEnum.Alarminfo.LogTypeEnum.NOTICE_FTP_TEMPLETE, "【司机通知内容模板】统一初始化模板", null, linkUrlMap,localLinkUrlMap);
} catch (Exception e) {
logger.error("统一初始化模板异常", e);
}
}
/**
* Description:根据通知内容模板文件名称获取ftp文件url
* @Version1.0 2016-4-14 下午2:06:13 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param name 通知内容模板文件名称
* @param isRedis 是否从Redis获取
* @return
*/
public static String getLinkUrl(String name,boolean isRedis){
if(isRedis){
return getTempleteUrlFromRedis(name);
}else{
return getTempleteUrl(name);
}
}
/**
* Description:根据通知内容模板文件名称获取ftp文件url
* @Version1.0 2016-4-14 下午2:06:55 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param name 通知内容模板文件名称
* @return 默认Redis,Redis获取不了从缓存中
*/
public static String getLinkUrl(String name){
String linkUrl = getTempleteUrlFromRedis(name);
if(null==linkUrl){
return getTempleteUrl(name);
}
return getTempleteUrl(name);
}
/**
* Description:根据模板文件名称获取模板url
* @Version1.0 2016-4-14 上午10:17:16 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param name 模板文件名称
* @return
*/
private static String getTempleteUrl(String name){
if(CollectionsTools.isEmpty(localLinkUrlMap)){
init();
}
return localLinkUrlMap.get(name);
}
/**
* Description:根据模板文件名称获取模板url
* @Version1.0 2016-4-14 下午1:42:10 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
* @param name 模板文件名称
* @return
*/
private static String getTempleteUrlFromRedis(String name){
String url = null;
try {
HashCommands cmd = NoticeRedisUtil.getHashCmd();
Object o = cmd.get(RedisConstant.NamespaceEnum.NOTICE.getIndex(), TEMPLETE_REDIS_PREFIX,name);
if(null!=o){
url = o.toString();
}
} catch (Exception e) {
logger.error("统一初始化模板异常", e);
}
return url;
}
/**
* Description:司机通知内容通用模板定义枚举 All Rights Reserved.
* @version 1.0 2016-4-14 上午9:32:49 by 石冬冬-Chris Suk(dd.shi02@zuche.com)创建
*/
public enum NoticeContentCommEnum implements EnumValue {
RELOGIN(1,"relogin_20160614.html","您的账户已在其他设备登录,请重新登录");
private int index;
private String name;
private String content;
NoticeContentCommEnum(int index, String name, String content) {
this.index = index;
this.name = name;
this.content = content;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static String getNameByIndex(int index) {
for (NoticeContentCommEnum c : NoticeContentCommEnum.values()) {
if (c.getIndex() == index) {
return c.name;
}
}
return null;
}
public static String getNameByContent(String content) {
for (NoticeContentCommEnum c : NoticeContentCommEnum.values()) {
if (c.getContent().equals(content)) {
return c.name;
}
}
return null;
}
}
private NoticeTempleteUtils(){}
}
4.5、同步写入老库
基于MQ消息通知,由司机系统消费消息。
MqSendUtil
/**
*
* Description:
* All Rights Reserved.
*
* @version 1.0 2016年3月24日 下午3:01:48 created by 张淑峰(sf.zhang02@zuche.com)
*/
public class MqSendUtil {
private static Logger logger = LoggerFactory.getLogger(MqSendUtil.class);
private static String PRODUCER_PROPERTIES = "metaProducer";
private static String REWRITE_TOPIC_KEY = "1.producer.topic";
private static String REWRITE_PREFIX_KEY = "1.producer.prefix";
private static final String prefix = PropertiesReader.getAppointPropertiesAttribute(PRODUCER_PROPERTIES, REWRITE_PREFIX_KEY,String.class);
private static final String topic = PropertiesReader.getAppointPropertiesAttribute(PRODUCER_PROPERTIES, REWRITE_TOPIC_KEY,String.class);
/**
*
* Description:
* All Rights Reserved.
*
*
* @version 1.0 2016年3月24日 下午3:01:56 created by 张淑峰(sf.zhang02@zuche.com)
*/
public static void sendAlarmInfoRewriteMq( AlarminfoRewriteNoticeDTO alarminfoRewriteDTO) {
try {
if (alarminfoRewriteDTO == null ) {
return ;
}
String content = alarminfoRewriteDTO.getContent();
if(StringTools.isNotEmpty(content)){
//由于老系统app不支持富文本,对富文本通知去除HTML标签
content=HtmlUtils.formatHtml(content);
content=HtmlUtils.removeBlank(content);
alarminfoRewriteDTO.setContent(content);
}else{
alarminfoRewriteDTO.setContent(alarminfoRewriteDTO.getTitle());
}
ResultVO resultVO = CommonMessageSend.sendMessage(prefix, topic, HessianSerializerUtils.serialize(alarminfoRewriteDTO));
//logger.error("【司机通知同步写入老库】MQ消息,alarminfoRewriteDTO={}",JSON.toJSON(alarminfoRewriteDTO));
if (resultVO != null && !resultVO.isSuccess()) {
HbaseLogUtil.sendHbaseLogMQ(0L, LogEnum.Alarminfo.LogTypeEnum.NOTICE_MQ_PRODUCTER, "【司机通知同步写入老库】MQ消息生产", null, alarminfoRewriteDTO);
//logger.error("【司机通知同步写入老库】向MQ写入报警信息重写信息出错,alarminfoRewriteDTO={},MQ错误信息={}",JSON.toJSON(alarminfoRewriteDTO),resultVO.getErrorMessage());
return;
}
} catch (Exception e) {
logger.error("报警信息同步到driver异常!mq发送信息={}",JSON.toJSONString(alarminfoRewriteDTO),e);
}
}
}
五、尾语
为了写完这篇四年前的回忆性文章,重新翻开之前的代码,看起来代码惨不忍睹啊,一个service或者controler类,代码量长达上千行太普通了。总而言之,过去的会议铭记着那个时代的记忆,没有曾经的当年,也不会有今日的今天。