53 Star 141 Fork 42

newflydd / jblog

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
jblog.sql 223.68 KB
一键复制 编辑 原始数据 按行查看 历史
Administrator 提交于 2016-12-12 17:49 . 2016年12月12日17:48:14
# Host: www.hexcode.cn (Version 5.7.9-log)
# Date: 2016-11-21 20:28:03
# Generator: MySQL-Front 5.4 (Build 4.5)
# Internet: http://www.mysqlfront.de/
/*!40101 SET NAMES utf8 */;
#
# Structure for table "jblog_archive_t"
#
DROP TABLE IF EXISTS `jblog_archive_t`;
CREATE TABLE `jblog_archive_t` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '文档(typle为0的文章)的归档,块状不含标题的文章不需要归档',
`title` varchar(10) NOT NULL COMMENT '归档标题,不可以为空,也不可以重复,统一格式为yyyy年M月',
`url_name` varchar(10) NOT NULL DEFAULT '201608',
PRIMARY KEY (`id`),
UNIQUE KEY `title_UNIQUE` (`title`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
#
# Data for table "jblog_archive_t"
#
INSERT INTO `jblog_archive_t` VALUES (1,'2016年8月','201608'),(2,'2016年9月','201609'),(3,'2016年10月','201610'),(4,'2016年11月','201611');
#
# Structure for table "jblog_article"
#
DROP TABLE IF EXISTS `jblog_article`;
CREATE TABLE `jblog_article` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '文章块,显示标题的是正式文章,显示标题的是文章块,通过type字段区分',
`title` varchar(60) NOT NULL DEFAULT '' COMMENT '文章标题不可以为空,块状文字类型(type=1)的article的title在页面上并不显示,但是用title来作为系统中的识别标识',
`html` text COMMENT 'HTML数据',
`markdown` text COMMENT 'markdown数据',
`create_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`type` tinyint(3) NOT NULL DEFAULT '0' COMMENT '文章类别 0:article 正式文章 1:顶部archive 无标题块状文章 2:底部无标题块状文字;3:外部数据,引入url获取外部数据作为data 11:自定义widget小插件 12:widget插件tags 13:存档插件',
`url` varchar(80) DEFAULT NULL COMMENT '当存档类型为2,外部数据时,本字段保存指向外部数据的URL,该URL将使用GET请求访问,并将响应放入data区域中,回显给浏览器。',
`url_name` varchar(45) DEFAULT NULL COMMENT '文档url别名,如果文档拥有url_name属性,则其在博客系统中可以通过url_name作为标识访问,浏览器地址栏中可以以此为reset标识检索文档,适用于比较经典的文章,方便搜索引擎收录。任何地方的列表显示文档时,优先使用url_name作为标识,为空的话,则使用id作为标识。',
`last_edit_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后一次修改时间,前端并不现实',
`archive_id` int(11) DEFAULT NULL COMMENT '归档编号,可以为空,表示未归档',
`state` tinyint(4) NOT NULL DEFAULT '1' COMMENT '文档状态\n0:编辑状态,不显示在页面上,显示在后台列表\n1:发布\n2:删除,不显示在前台和后台列表,显示在后台垃圾箱',
`rate` bigint(20) NOT NULL DEFAULT '0' COMMENT '点击量,长整,不要显示在页面上,显示在后台',
`page_index` int(6) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `url_name_UNIQUE` (`url_name`),
KEY `fk_archive_idx` (`archive_id`),
CONSTRAINT `fk_archive` FOREIGN KEY (`archive_id`) REFERENCES `jblog_archive_t` (`id`) ON DELETE SET NULL ON UPDATE SET NULL
) ENGINE=InnoDB AUTO_INCREMENT=146 DEFAULT CHARSET=utf8;
#
# Data for table "jblog_article"
#
INSERT INTO `jblog_article` VALUES (3,'jblog简介','<a target=\"_blank\" href=\"http://git.oschina.net/newflydd/jblog\">Jblog</a>&nbsp;是一个基于JAVA;适合JAVA初、中级水平编程人员使用、阅读并修改源代码的个人博客系统,不如来丁丁的&nbsp;<a target=\"_blank\" href=\"http://git.oschina.net/newflydd/jblog\">Jblog Git OSChina</a>&nbsp;瞧瞧?',NULL,'2015-07-29 00:00:00',1,NULL,NULL,'2016-08-19 16:36:01',NULL,1,0,3),(4,'个人简介','<p>丁丁生于 1987.07.01 ,30岁,英文ID:newflydd</p>\r\n<li>现居住地 <a target=\"_blank\" href=\"http://baike.baidu.com/link?url=_WR6cOv0PV697Q4N5DcPDf0Lg8dPCtPqufJv3RzTFteFfvzj98GfuoGHoDRNhapfSqiceI4KQSNGbUCtryhZYz5WPFvFe2SlybKvqcj9DamRpApYJBfAwBYNyLJSkjJoeNc7aI-yLzBaDCrUubBWwh_Vv6A4B6lT7TNs5-_XkDW\">江苏 ● 泰州 ● 姜堰</a></li><li>创建了 <a target=\"_blank\" href=\"http://git.oschina.net/newflydd/jblog\">Jblog 开源博客系统</a></li><li>坚持十余年的 <a href=\"http://www.hexcode.cn\">独立博客</a> 作者</li><li>大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。</li><li>在 <a target=\"_blank\" href=\"http://git.oschina.net/newflydd\">Git OSChina</a> 上积极参与开源社区</li>\r\n','丁丁生于 1987.07.01 ,30岁,英文ID:newflydd\r\n<li>现居住地 <a target=\"_blank\" href=\"http://baike.baidu.com/link?url=_WR6cOv0PV697Q4N5DcPDf0Lg8dPCtPqufJv3RzTFteFfvzj98GfuoGHoDRNhapfSqiceI4KQSNGbUCtryhZYz5WPFvFe2SlybKvqcj9DamRpApYJBfAwBYNyLJSkjJoeNc7aI-yLzBaDCrUubBWwh_Vv6A4B6lT7TNs5-_XkDW\">江苏 ● 泰州 ● 姜堰</a></li><li>创建了 <a target=\"_blank\" href=\"http://git.oschina.net/newflydd/jblog\">Jblog 开源博客系统</a></li><li>坚持十余年的 <a href=\"http://www.hexcode.cn\">独立博客</a> 作者</li><li>大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。</li><li>在 <a target=\"_blank\" href=\"http://git.oschina.net/newflydd\">Git OSChina</a> 上积极参与开源社区</li>\r\n','2016-08-22 18:08:56',11,NULL,NULL,'2016-09-24 16:06:27',NULL,1,0,4),(5,'Tags','<p>这是「Tag」标签的插件占位符,本身的HTML无任何含义,无需编辑。</p>\r\n','这是「Tag」标签的插件占位符,本身的HTML无任何含义,无需编辑。\r\n','2016-08-22 18:08:55',12,NULL,NULL,'2016-09-24 15:48:47',NULL,1,0,5),(6,'Archive','<p>这是个「归档」插件的占位符,本身的HTML无任何含义,无需编辑。</p>\r\n','这是个「归档」插件的占位符,本身的HTML无任何含义,无需编辑。\r\n','2016-08-21 08:13:16',13,NULL,NULL,'2016-09-24 15:48:56',NULL,1,0,6),(13,'极限编程','<p>这几天,准确来说是连续4天了</p><p>真的能称之为极限编程了</p><p>关于N皇后算法的极限挑战,最终很满意</p><p>代码使用了“一维棋盘”,“对称剪枝”,“递归回溯”,“多线程”等特色</p><p>最终结果:</p><p>15皇后,用时:4903毫秒,计算结果:2279184</p><p>16皇后,用时:33265毫秒,计算结果:14772512</p><p>17皇后,用时:267460毫秒,计算结果:95815104</p><p>比起我第一天写N皇后,14皇后用时87秒的成绩,提高太多了!!!</p><p><br/></p><p>说好的一定要在100秒内解16皇后,终于解脱了</p><p>啥都不说了,贴上代码和运算成绩</p><pre class=\"brush:java;toolbar:false\">package&nbsp;com.newflypig.eightqueen;\r\nimport&nbsp;java.util.ArrayList;\r\nimport&nbsp;java.util.Date;\r\nimport&nbsp;java.util.List;\r\nimport&nbsp;java.util.concurrent.Callable;\r\nimport&nbsp;java.util.concurrent.ExecutorService;\r\nimport&nbsp;java.util.concurrent.Executors;\r\nimport&nbsp;java.util.concurrent.Future;\r\n\r\n\r\npublic&nbsp;class&nbsp;EightQueen7&nbsp;{\r\n\tprivate&nbsp;static&nbsp;final&nbsp;short&nbsp;K=8;\t\t//使用常量来定义,方便之后解N皇后问题\r\n\tprivate&nbsp;static&nbsp;short&nbsp;N=0;\r\n\t\r\n\tpublic&nbsp;static&nbsp;void&nbsp;main(String[]&nbsp;args)&nbsp;throws&nbsp;Exception&nbsp;{\r\n\t\tfor(N=9;N&lt;=17;N++){\r\n\t\t\tlong&nbsp;count=0;\r\n\t\t\tDate&nbsp;begin&nbsp;=new&nbsp;Date();\r\n\t\t\t/**\r\n\t\t\t&nbsp;*&nbsp;初始化棋盘,使用一维数组存放棋盘信息\r\n\t\t\t&nbsp;*&nbsp;chess[n]=X:表示第n行X列有一个皇后\r\n\t\t\t&nbsp;*/\r\n\t\t\t\r\n\t\t\tList&lt;short[]&gt;&nbsp;chessList=new&nbsp;ArrayList&lt;short[]&gt;(N);\r\n\t\t\tfor(short&nbsp;i=0;i&lt;N;i++){\r\n\t\t\t\tshort&nbsp;chess[]=new&nbsp;short[N];\r\n\t\t\t\tchess[0]=i;\r\n\t\t\t\tchessList.add(chess);\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tshort&nbsp;taskSize&nbsp;=(short)(&nbsp;N/2+(N%2==1?1:0)&nbsp;);\r\n\t\t\t//&nbsp;创建一个线程池\r\n\t\t\tExecutorService&nbsp;pool&nbsp;=&nbsp;Executors.newFixedThreadPool(taskSize);\r\n\t\t\t//&nbsp;创建多个有返回值的任务\r\n\t\t\tList&lt;Future&lt;Long&gt;&gt;&nbsp;futureList&nbsp;=&nbsp;new&nbsp;ArrayList&lt;Future&lt;Long&gt;&gt;(taskSize);\r\n\t\t\tfor&nbsp;(int&nbsp;i&nbsp;=&nbsp;0;&nbsp;i&nbsp;&lt;&nbsp;taskSize;&nbsp;i++)&nbsp;{\r\n\t\t\t\tCallable&lt;Long&gt;&nbsp;c&nbsp;=&nbsp;new&nbsp;EightQueenThread(chessList.get(i));\r\n\t\t\t\t//&nbsp;执行任务并获取Future对象\r\n\t\t\t\tFuture&lt;Long&gt;&nbsp;f&nbsp;=&nbsp;pool.submit(c);\r\n\t\t\t\tfutureList.add(f);\r\n\t\t\t}\r\n\t\t\t//&nbsp;关闭线程池\r\n\t\t\tpool.shutdown();\r\n\t\t\t\r\n\t\t\tfor(short&nbsp;i=0;&nbsp;i&lt;(short)&nbsp;(taskSize&nbsp;-&nbsp;(N%2==1?1:0));&nbsp;i++){\t\t\t\t\r\n\t\t\t\tcount+=futureList.get(i).get();\r\n\t\t\t}\r\n\t\t\tcount=count*2;\r\n\t\t\tif(N%2==1)\r\n\t\t\t\tcount+=futureList.get(N/2).get();\r\n\t\t\t\r\n\t\t\tDate&nbsp;end&nbsp;=new&nbsp;Date();\r\n\t\t\tSystem.out.println(&quot;解决&nbsp;&quot;&nbsp;+N+&nbsp;&quot;皇后问题,用时:&quot;&nbsp;+String.valueOf(end.getTime()-begin.getTime())+&nbsp;&quot;毫秒,计算结果:&quot;+count);\r\n\t\t}\r\n\t}\r\n}\r\n\r\nclass&nbsp;EightQueenThread&nbsp;implements&nbsp;Callable&lt;Long&gt;{\r\n\tprivate&nbsp;short[]&nbsp;chess;\r\n\tprivate&nbsp;short&nbsp;N;\r\n\t\r\n\tpublic&nbsp;EightQueenThread(short[]&nbsp;chess){\r\n\t\tthis.chess=chess;\r\n\t\tthis.N=(short)&nbsp;chess.length;\r\n\t}\r\n\t\r\n\t\r\n\t@Override\r\n\tpublic&nbsp;Long&nbsp;call()&nbsp;throws&nbsp;Exception&nbsp;{\r\n\t\treturn&nbsp;putQueenAtRow(chess,&nbsp;(short)1)&nbsp;;\r\n\t}\r\n\r\n\r\n\tprivate&nbsp;Long&nbsp;putQueenAtRow(short[]&nbsp;chess,&nbsp;short&nbsp;row)&nbsp;{\r\n\t\tif(row==N){\r\n\t\t\treturn&nbsp;(long)&nbsp;1;\r\n\t\t}\r\n\t\t\r\n\t\tshort[]&nbsp;chessTemp=chess.clone();\r\n\t\tlong&nbsp;sum=0;\r\n\t\t/**\r\n\t\t&nbsp;*&nbsp;向这一行的每一个位置尝试排放皇后\r\n\t\t&nbsp;*&nbsp;然后检测状态,如果安全则继续执行递归函数摆放下一行皇后\r\n\t\t&nbsp;*/\r\n\t\tfor(short&nbsp;i=0;i&lt;N;i++){\r\n\t\t\t//摆放这一行的皇后\r\n\t\t\tchessTemp[row]=i;\r\n\t\t\t\r\n\t\t\tif(&nbsp;isSafety(&nbsp;chessTemp,row,i)&nbsp;){\r\n\t\t\t\tsum+=putQueenAtRow(chessTemp,(short)&nbsp;(row+1));\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\treturn&nbsp;sum;\r\n\t}\r\n\t\r\n\tprivate&nbsp;static&nbsp;boolean&nbsp;isSafety(short[]&nbsp;chess,short&nbsp;row,short&nbsp;col)&nbsp;{\r\n\t\t//判断中上、左上、右上是否安全\r\n\t\tshort&nbsp;step=1;\r\n\t\tfor(short&nbsp;i=(short)&nbsp;(row-1);i&gt;=0;i--){\r\n\t\t\tif(chess[i]==col)\t//中上\r\n\t\t\t\treturn&nbsp;false;\r\n\t\t\tif(chess[i]==col-step)\t//左上\r\n\t\t\t\treturn&nbsp;false;\r\n\t\t\tif(chess[i]==col+step)\t//右上\r\n\t\t\t\treturn&nbsp;false;\r\n\t\t\t\r\n\t\t\tstep++;\r\n\t\t}\r\n\t\t\r\n\t\treturn&nbsp;true;\r\n\t}\r\n}</pre><p><br/></p><p><br/></p>',NULL,'2016-08-24 18:38:00',0,NULL,'eight-queen','2016-08-24 18:38:00',1,1,55,13),(15,'代码测试','<div class=\"markdown-toc editormd-markdown-toc\">[TOC]</div><h4 id=\"h4-disabled-options\"><a name=\"Disabled options\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Disabled options</h4><ul>\r\n<li>TeX (Based on KaTeX);</li><li>Emoji;</li><li>Task lists;</li><li>HTML tags decode;</li><li>Flowchart and Sequence Diagram;</li></ul>\r\n<pre><code>@Override\r\npublic void save(Article article) {\r\n String sql = &quot;insert into jblog_article(title, markdown, html) values(:title, :markdown, :html)&quot;;\r\n this.getSession().createSQLQuery(sql)\r\n .setString(&quot;title&quot;, article.getTitle())\r\n .setString(&quot;markdown&quot;, article.getMarkdown())\r\n .setString(&quot;html&quot;, article.getHtml())\r\n .executeUpdate();\r\n}\r\n</code></pre>','[TOC]\r\n\r\n#### Disabled options\r\n\r\n- TeX (Based on KaTeX);\r\n- Emoji;\r\n- Task lists;\r\n- HTML tags decode;\r\n- Flowchart and Sequence Diagram;\r\n\r\n```\r\n@Override\r\npublic void save(Article article) {\r\n\tString sql = \"insert into jblog_article(title, markdown, html) values(:title, :markdown, :html)\";\r\n\tthis.getSession().createSQLQuery(sql)\r\n\t\t.setString(\"title\", article.getTitle())\r\n\t\t.setString(\"markdown\", article.getMarkdown())\r\n\t\t.setString(\"html\", article.getHtml())\r\n\t\t.executeUpdate();\r\n}\r\n```','2016-08-25 00:31:50',0,NULL,NULL,'2016-08-25 00:31:50',1,1,51,15),(25,'Chrome浏览器kiosk模式下,退出的方法','<p>Chrome浏览器的kiosk模式真乃神器<br>可以方便的开发基于HTML5的展示型全屏系统<br>一般人根本不知道是运行的HTML应用</p>\r\n<p>该模式下,只有按Alt+F4或者Ctrl+W才能退出应用<br>但对于触摸屏(无键盘)场景下有些困难</p>\r\n<p>这就要求在HTML中加上关闭功能<br>令人吃惊的是<code>javascript:window.close()</code>在kiosk模式下居然也不起作用<br>经过百般搜索,终于发现了一个chrome插件可以解决此问题<br><a href=\"https://chrome.google.com/webstore/detail/close-kiosk/dfbjahmenldfpkokepmfmkjkhdjelmkb\">Close Kiosk 插件地址</a></p>\r\n<p>现在Chrome插件必须Store上在线安装<br>如果你的展柜机台没有网络环境<br>又或者Chrome Stroe上哪一天这个插件下架了<br>又或者哪天翻不了墙了就糟糕了</p>\r\n<p>百度搜索「<a href=\"http://jingyan.baidu.com/article/9158e0004ff9bba25512284d.html\">Chrome 插件导出</a>」<br>将其导出为CRX文件,再到其他电脑上,打开chrome拖入即可,我这边备份了一个,可供下载<br><a href=\"http://ocsy1jhmk.bkt.clouddn.com/c92adb65-359b-4499-9ba4-f6a0bd4af652.crx\">下载地址</a></p>\r\n<p>安装到Chrome以后,在HTML中只要打开的URL包含closekiosk字符串,这个插件就能将Chrome关闭,达到退出的效果。</p>\r\n','Chrome浏览器的kiosk模式真乃神器\r\n可以方便的开发基于HTML5的展示型全屏系统\r\n一般人根本不知道是运行的HTML应用\r\n\r\n该模式下,只有按Alt+F4或者Ctrl+W才能退出应用\r\n但对于触摸屏(无键盘)场景下有些困难\r\n\r\n这就要求在HTML中加上关闭功能\r\n令人吃惊的是`javascript:window.close()`在kiosk模式下居然也不起作用\r\n经过百般搜索,终于发现了一个chrome插件可以解决此问题\r\n[Close Kiosk 插件地址](https://chrome.google.com/webstore/detail/close-kiosk/dfbjahmenldfpkokepmfmkjkhdjelmkb)\r\n\r\n现在Chrome插件必须Store上在线安装\r\n如果你的展柜机台没有网络环境\r\n又或者Chrome Stroe上哪一天这个插件下架了\r\n又或者哪天翻不了墙了就糟糕了\r\n\r\n百度搜索「[Chrome 插件导出](http://jingyan.baidu.com/article/9158e0004ff9bba25512284d.html)」\r\n将其导出为CRX文件,再到其他电脑上,打开chrome拖入即可,我这边备份了一个,可供下载\r\n[下载地址](http://ocsy1jhmk.bkt.clouddn.com/c92adb65-359b-4499-9ba4-f6a0bd4af652.crx)\r\n\r\n安装到Chrome以后,在HTML中只要打开的URL包含closekiosk字符串,这个插件就能将Chrome关闭,达到退出的效果。','2016-08-31 11:46:33',0,NULL,'kiosk-quite','2016-10-28 12:19:44',1,1,113,25),(29,'轻量级屏幕录制工具LICEcap,轻松制作GIF','<p><a href=\"http://www.cockos.com/licecap\"><strong>LICEcap</strong></a> 是一款轻巧的屏幕录制并制作GIF的小工具<br>它轻巧到安装文件只有 <strong>228KB</strong> 大小<br>录制10秒的GIF小视频只有 <strong>148KB</strong> 大小<br>录制1分钟的GIF视频只有 <strong>1.8M</strong> 大小</p>\r\n<p>如果你是独立博客作者,尤其是写计算机方向的博客的话<br>想必很多地方需要借助屏幕录制来讲解操作方法<br>这款软件相当适用<br>下面是一个只有 140KB 大小的测试GIF<br><img src=\"http://ocsy1jhmk.bkt.clouddn.com/35a79756-7fc2-4805-9a46-edc38855e900.gif\" alt=\"\"></p>\r\n','[**LICEcap**](http://www.cockos.com/licecap) 是一款轻巧的屏幕录制并制作GIF的小工具\r\n它轻巧到安装文件只有 **228KB** 大小\r\n录制10秒的GIF小视频只有 **148KB** 大小\r\n录制1分钟的GIF视频只有 **1.8M** 大小\r\n\r\n如果你是独立博客作者,尤其是写计算机方向的博客的话\r\n想必很多地方需要借助屏幕录制来讲解操作方法\r\n这款软件相当适用\r\n下面是一个只有 140KB 大小的测试GIF\r\n![](http://ocsy1jhmk.bkt.clouddn.com/35a79756-7fc2-4805-9a46-edc38855e900.gif)','2016-08-31 14:09:57',0,NULL,'licecap','2016-09-18 16:40:54',1,1,66,29),(30,'g++编译器中的「-Wl,--kill-at」用法,解决JNI中的UnsatisfiedLinkError','<p>在将cpp打包成dll动态链接库时<br>你可以使用以下命令</p>\r\n<pre><code>g++ -shared -o test.dll test.cpp\r\n</code></pre><p>但是有时候你会发现编译成的dll根本无法被调用<br>因为这样编译g++有可能会 <strong>「自作主张」</strong> 将你cpp中的函数名 <strong>优化</strong></p>\r\n<p>如果你使用了C++的「多态」,即函数名相同,参数不同的特性<br>那g++会百分之一百会使用函数名优化机制,修改你的函数名的</p>\r\n<p>而如果你使用JNI,函数名都类似</p>\r\n<pre><code>JNIEXPORT jint JNICALL Java_com_saiyang_newflypig_rwt_cnc_CNCHelper_readToolNos (JNIEnv* env, jclass);\r\n</code></pre><p>这么大一坨时,g++也会 <strong>好心</strong> 帮你优化优化函数名<br>(帮你加个‘@’之类的无意义符号,根本不知道这是什么目的)</p>\r\n<p>一旦函数名称被优化成其他名称,你的dll是说什么也不会成功被调用的<br>这时候我们在编译dll时,应该加上<code>-Wl,--kill-at</code>参数,杀掉‘@’,并取消警告<br>很可惜,百度这个参数,搜到有用的结果寥寥无几<br>只有几篇有价值的文章介绍了这个参数,并且还都是跟JNI有关的<br>比如 <a href=\"http://dikar.iteye.com/blog/382701\">http://dikar.iteye.com/blog/382701</a></p>\r\n','在将cpp打包成dll动态链接库时\r\n你可以使用以下命令\r\n```\r\ng++ -shared -o test.dll test.cpp\r\n```\r\n但是有时候你会发现编译成的dll根本无法被调用\r\n因为这样编译g++有可能会 **「自作主张」** 将你cpp中的函数名 **优化**\r\n\r\n如果你使用了C++的「多态」,即函数名相同,参数不同的特性\r\n那g++会百分之一百会使用函数名优化机制,修改你的函数名的\r\n\r\n而如果你使用JNI,函数名都类似\r\n```\r\nJNIEXPORT jint JNICALL Java_com_saiyang_newflypig_rwt_cnc_CNCHelper_readToolNos (JNIEnv* env, jclass);\r\n```\r\n这么大一坨时,g++也会 **好心** 帮你优化优化函数名\r\n(帮你加个‘@’之类的无意义符号,根本不知道这是什么目的)\r\n\r\n一旦函数名称被优化成其他名称,你的dll是说什么也不会成功被调用的\r\n这时候我们在编译dll时,应该加上`-Wl,--kill-at`参数,杀掉‘@’,并取消警告\r\n很可惜,百度这个参数,搜到有用的结果寥寥无几\r\n只有几篇有价值的文章介绍了这个参数,并且还都是跟JNI有关的\r\n比如 http://dikar.iteye.com/blog/382701','2016-08-31 15:43:20',0,NULL,NULL,'2016-08-31 15:43:20',1,1,64,30),(32,'七牛图片上传测试','<p><img src=\"http://ocsy1jhmk.bkt.clouddn.com/FmedNCgr9iiyQeOMMCaninnKCGde\" alt=\"\"></p>\r\n','![](http://ocsy1jhmk.bkt.clouddn.com/FmedNCgr9iiyQeOMMCaninnKCGde)','2016-09-01 11:46:26',0,NULL,NULL,'2016-09-01 11:46:26',2,1,41,32),(47,'制作editor.md的七牛上传插件','<p>成功制作了 <a href=\"https://pandao.github.io/editor.md/examples/index.html\">editor.md</a> 的七牛上传插件</p>\r\n<p>得益于editor.md高可配置性,以及插件化</p>\r\n<p>还得益于七牛的 <a href=\"http://developer.qiniu.com/code/v7/sdk/java.html\">开发文档</a> 相当详细</p>\r\n<p>具体的制作步骤我会抽时间写一篇较为详细的博文带大家一起做。</p>\r\n<p><img src=\"http://ocsy1jhmk.bkt.clouddn.com/e58c6db5-1eba-401a-9cac-04a2bbcad124.png\" alt=\"\"></p>\r\n','成功制作了 [editor.md](https://pandao.github.io/editor.md/examples/index.html) 的七牛上传插件\r\n\r\n得益于editor.md高可配置性,以及插件化\r\n\r\n还得益于七牛的 [开发文档](http://developer.qiniu.com/code/v7/sdk/java.html) 相当详细\r\n\r\n具体的制作步骤我会抽时间写一篇较为详细的博文带大家一起做。\r\n\r\n![](http://ocsy1jhmk.bkt.clouddn.com/e58c6db5-1eba-401a-9cac-04a2bbcad124.png)','2016-09-03 00:08:32',0,NULL,NULL,'2016-09-03 00:08:32',2,1,53,47),(48,'JBlog图标设计','<p>在 <a href=\"http://www.logoko.com.cn/\">http://www.logoko.com.cn/</a> 上为JBlog设计了一款图标。</p>\r\n<p>上传到七牛服务器上。</p>\r\n<p><img src=\"http://ocsy1jhmk.bkt.clouddn.com/f1500c4f-6dda-4105-8261-0440c79cffee.png\" alt=\"\"></p>\r\n','在 [http://www.logoko.com.cn/](http://www.logoko.com.cn/) 上为JBlog设计了一款图标。\r\n\r\n上传到七牛服务器上。\r\n\r\n![](http://ocsy1jhmk.bkt.clouddn.com/f1500c4f-6dda-4105-8261-0440c79cffee.png)','2016-09-03 14:03:16',0,NULL,'create-logo','2016-09-18 22:19:12',2,1,66,48),(50,'开始使用番茄土豆训练自己的专注力','<p><img src=\"https://pomotodo.com/images/pomotodo@2x.png\" alt=\"\"></p>\r\n<h2 id=\"h2-u756Au8304u5DE5u4F5Cu6CD5\"><a name=\"番茄工作法\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>番茄工作法</h2><p>番茄工作法是弗朗西斯科·西里洛于1992年创立的一种时间管理方法。</p>\r\n<p>番茄工作法的基本原理是把工作时间划分为多个番茄钟,一个番茄钟由 25 分钟工作时间和 5 分钟休息时间组成。</p>\r\n<p>25分钟的工作时间内,要保持专注,避免干扰。5分钟的休息时间建议离开自己的工作区域,可以喝一杯茶,做一些简单的伸展运动。</p>\r\n<p>番茄工作法是一种可以帮助你提高工作效率,改变学习习惯的高效而又简单的方法。</p>\r\n<h2 id=\"h2-u756Au8304u571Fu8C46\"><a name=\"番茄土豆\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span><a href=\"https://pomotodo.com\">番茄土豆</a></h2><p><a href=\"https://pomotodo.com\">番茄土豆</a> 是国人(上海贝朋信息科技有限公司,我跟你一样对此公司知之甚少)开发的一款基于番茄工作法的互联网APP,其客户端全平台覆盖Windows,Linux,Chrom插件,IOS,Android,是治疗拖延症的居家必备。</p>\r\n<h2 id=\"h2-u5DE5u4F5Cu8BA1u5212\"><a name=\"工作计划\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>工作计划</h2><p>在编写JBlog开源博客系统的中前期,很多功能需要完善,同时越来越多的新想法和构思不断涌入大脑,为了系统地组织这些工作计划,并克服自己的拖延症,我决定使用 <a href=\"https://pomotodo.com\">番茄土豆</a> 。</p>\r\n<p>下面是一天使用 <a href=\"https://pomotodo.com\">番茄土豆</a> 一个上午的成就,效果很好。<br><img src=\"http://ocsy1jhmk.bkt.clouddn.com/435ab5f4-3752-4ebc-84ef-0d5cd7c439c7.png\" alt=\"\"><br>决定以后长期使用,放弃之前一直用的 TODO LIST 。</p>\r\n','![](https://pomotodo.com/images/pomotodo@2x.png)\r\n##番茄工作法\r\n番茄工作法是弗朗西斯科·西里洛于1992年创立的一种时间管理方法。\r\n\r\n番茄工作法的基本原理是把工作时间划分为多个番茄钟,一个番茄钟由 25 分钟工作时间和 5 分钟休息时间组成。\r\n\r\n25分钟的工作时间内,要保持专注,避免干扰。5分钟的休息时间建议离开自己的工作区域,可以喝一杯茶,做一些简单的伸展运动。\r\n\r\n番茄工作法是一种可以帮助你提高工作效率,改变学习习惯的高效而又简单的方法。\r\n\r\n##[番茄土豆](https://pomotodo.com)\r\n[番茄土豆](https://pomotodo.com) 是国人(上海贝朋信息科技有限公司,我跟你一样对此公司知之甚少)开发的一款基于番茄工作法的互联网APP,其客户端全平台覆盖Windows,Linux,Chrom插件,IOS,Android,是治疗拖延症的居家必备。\r\n\r\n##工作计划\r\n在编写JBlog开源博客系统的中前期,很多功能需要完善,同时越来越多的新想法和构思不断涌入大脑,为了系统地组织这些工作计划,并克服自己的拖延症,我决定使用 [番茄土豆](https://pomotodo.com) 。\r\n\r\n下面是一天使用 [番茄土豆](https://pomotodo.com) 一个上午的成就,效果很好。\r\n![](http://ocsy1jhmk.bkt.clouddn.com/435ab5f4-3752-4ebc-84ef-0d5cd7c439c7.png)\r\n决定以后长期使用,放弃之前一直用的 TODO LIST 。','2016-09-05 14:27:38',0,NULL,'pomotodo','2016-09-06 14:01:14',2,1,134,50),(52,'服务器分配线程测试','<p>新增文章后,服务器立即返回视图给用户<br>同时后台分配一支线程更新首页上的tag标签内存数据</p>\r\n<p>测试代码:</p>\r\n<pre><code>@RequestMapping(value=&quot;/admin/add&quot;,method=RequestMethod.POST)\r\npublic String add(@ModelAttribute(&quot;article&quot;) Article article){\r\n\r\n this.articleService.add(article);\r\n\r\n //添加文章后,可以立即返回视图给用户,此时分配一个线程去更新内存中tags数据\r\n new Thread(() -&gt; {\r\n blogCommon.setTags(this.tagService.findAll());\r\n }).start();\r\n\r\n return &quot;redirect:/resources/amazeui/1.html&quot;;\r\n}\r\n</code></pre>','新增文章后,服务器立即返回视图给用户\r\n同时后台分配一支线程更新首页上的tag标签内存数据\r\n\r\n测试代码:\r\n```\r\n@RequestMapping(value=\"/admin/add\",method=RequestMethod.POST)\r\npublic String add(@ModelAttribute(\"article\") Article article){\r\n\r\n\tthis.articleService.add(article);\r\n\r\n\t//添加文章后,可以立即返回视图给用户,此时分配一个线程去更新内存中tags数据\r\n\tnew Thread(() -> {\r\n\t\tblogCommon.setTags(this.tagService.findAll());\r\n\t}).start();\r\n\r\n\treturn \"redirect:/resources/amazeui/1.html\";\r\n}\r\n```','2016-09-06 12:46:53',0,NULL,'test-new-thread-in-server','2016-09-06 13:20:21',2,1,87,52),(69,'给AmazeUI-TagsInput组件添加自动提示typeahead功能','<h1 id=\"h1-amazeui-tagsinput-\"><a name=\"AmazeUI-TagsInput 插件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span><a href=\"http://amazeui.github.io/tagsinput/\">AmazeUI-TagsInput</a> 插件</h1><p>使用 <a href=\"http://amazeui.org/\">AmazeUI</a> 有一段时间了,各方面都挺满意的,相比 <a href=\"http://www.bootcss.com/\">Bootstrap</a> 对国人来说确实更加友好,他们之间具体的区别可以参考 <a href=\"http://www.5icool.org/a/201501/a9836.html\">http://www.5icool.org/a/201501/a9836.html</a> 这篇对几种前端骨架型框架介绍的文章。</p>\r\n<p>在使用 AmazeUI 的设计 JBlog 后台的过程中,有一个功能是添加文章,其中关于文章标签的交互我是用了他的插件 「<a href=\"http://amazeui.github.io/tagsinput/\">AmazeUI-TagsInput</a>」,效果是这样的:</p>\r\n<p><img src=\"http://ocsy1jhmk.bkt.clouddn.com/18b62d91-bd58-4eaa-aa59-384f3672a5d7.gif\" alt=\"\"></p>\r\n<p>TagsInput本身很好实现,在添加完相应的资源(包括一个JS和一个CSS)后,直接在需要美化的 <code>&lt;input&gt;</code> 标签中打上 <code>data-am-tagsinput</code> 属性即可。</p>\r\n<h1 id=\"h1-typeahead-js-\"><a name=\"typeahead.js 自动提示插件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span><a href=\"http://twitter.github.io/typeahead.js/\">typeahead.js</a> 自动提示插件</h1><p>已经使用 TagsInput 插件的 <code>input</code> 标签呈现了比较美观的「标签感」,我还想让它更智能一些,就像上图一样输入一个字符(包括中文),能够在下方自动提醒之前输入过的标签。<br>这时候需要使用 jQuery 的 <a href=\"http://twitter.github.io/typeahead.js/\">typeahead.js</a> 插件,这个跟 bootstrap一样,也是 twitter 开发的,专门对输入框组进行下拉提示,配置性很强,其中封装了一个匪夷所思的对象叫做 <a href=\"https://github.com/twitter/typeahead.js/blob/master/doc/bloodhound.md\">Bloodhound</a>(中文名叫做:猎犬,挺有意思的),在 <a href=\"http://amazeui.github.io/tagsinput/docs/demo.html#%E8%BE%93%E5%85%A5%E6%8F%90%E7%A4%BA\">AmazeUI TagsInput</a> 的文档中也是用了这个 Bloodhound 对象来实现自动化提示,但是我没有用,感觉挺重量级的,自己写ajax获取了服务器上的tags标签列表,自己写mach函数适配了。<br>我们先来看看官方的示例:</p>\r\n<pre><code>var citynames = new Bloodhound({\r\n datumTokenizer: Bloodhound.tokenizers.obj.whitespace(&#39;name&#39;),\r\n queryTokenizer: Bloodhound.tokenizers.whitespace,\r\n prefetch: {\r\n url: &#39;js/citynames.json&#39;,\r\n filter: function(list) {\r\n return $.map(list, function(cityname) {\r\n return { name: cityname }; });\r\n }\r\n }\r\n});\r\ncitynames.initialize();\r\n$(&#39;#input-sg&#39;).tagsinput({\r\n typeaheadjs: {\r\n name: &#39;citynames&#39;,\r\n displayKey: &#39;name&#39;,\r\n valueKey: &#39;name&#39;,\r\n source: citynames.ttAdapter()\r\n }\r\n});\r\n</code></pre><p>函数中的 url 指向服务器后台的资源文件,返回json格式就可以,用 springMVC + freemarker 做起来很简单,但在前台我用这个「猎犬」对象试了很多次并没有成功。顺便提一下,手动使用<code>tagsinput()</code>函数的时候,别忘了移除<code>&lt;input&gt;</code>标签上的<code>data-am-tagsinput</code>属性,要不然<code>tagsinput()</code>函数是不起作用的,这也是我试错了很久发现的。<br>随后我放弃了「猎犬」对象,虽然在 <a href=\"https://github.com/twitter/typeahead.js/blob/master/doc/bloodhound.md/\">typeahead.js</a> 官方文档中提到强烈建议使用这个对象,但我觉得不仅不好用,还将本来很简单的业务逻辑又封装得有点复杂了,索性放弃,参考 <a href=\"http://twitter.github.io/typeahead.js/examples/\">typeahead.js examples</a> 页面的源代码,使用第一种最直观原始的方式加载智能提示数据。<br>事实证明,即使是官方示例,我们也有可能用不起来,继续参考了一片文章: <a href=\"http://www.codesec.net/view/445202.html\">http://www.codesec.net/view/445202.html</a> 才顺利使用,最终的代码如下:</p>\r\n<pre><code>//定义一个匹配判断函数\r\nvar substringMatcher = function(strs) {\r\n return function findMatches(q, cb) {\r\n //传入的q是键盘输入的字符,传入的cb一个处理数组的函数\r\n var matches, substringRegex;\r\n matches = [];\r\n substrRegex = new RegExp(q,&quot;i&quot;);\r\n $.each(strs, function(i, str) {\r\n if (substrRegex.test(str)){\r\n matches.push({&quot;title&quot; : str});\r\n }\r\n });\r\n cb(matches);\r\n };\r\n};\r\n//使用ajax获取json数据,并给标签增加智能提醒功能\r\n$.get(&quot;${rc.contextPath}/tags/admin/list&quot;, function(result){\r\n if(result.resultCode == 0){\r\n $(&quot;#input-sg&quot;).tagsinput({\r\n typeaheadjs : {\r\n name : &quot;tagsHint&quot;,\r\n displayKey : &quot;title&quot;,\r\n valueKey : &quot;title&quot;,\r\n source : substringMatcher(result.data)\r\n }\r\n });\r\n }\r\n}, &quot;json&quot;);\r\n</code></pre><p>这样就实现了一开始的动画所显示的效果了。</p>\r\n','#[AmazeUI-TagsInput](http://amazeui.github.io/tagsinput/) 插件\r\n使用 [AmazeUI](http://amazeui.org/) 有一段时间了,各方面都挺满意的,相比 [Bootstrap](http://www.bootcss.com/) 对国人来说确实更加友好,他们之间具体的区别可以参考 http://www.5icool.org/a/201501/a9836.html 这篇对几种前端骨架型框架介绍的文章。\r\n\r\n在使用 AmazeUI 的设计 JBlog 后台的过程中,有一个功能是添加文章,其中关于文章标签的交互我是用了他的插件 「[AmazeUI-TagsInput](http://amazeui.github.io/tagsinput/)」,效果是这样的:\r\n\r\n![](http://ocsy1jhmk.bkt.clouddn.com/18b62d91-bd58-4eaa-aa59-384f3672a5d7.gif)\r\n\r\nTagsInput本身很好实现,在添加完相应的资源(包括一个JS和一个CSS)后,直接在需要美化的 `<input>` 标签中打上 `data-am-tagsinput ` 属性即可。\r\n\r\n#[typeahead.js](http://twitter.github.io/typeahead.js/) 自动提示插件\r\n已经使用 TagsInput 插件的 `input` 标签呈现了比较美观的「标签感」,我还想让它更智能一些,就像上图一样输入一个字符(包括中文),能够在下方自动提醒之前输入过的标签。\r\n这时候需要使用 jQuery 的 [typeahead.js](http://twitter.github.io/typeahead.js/) 插件,这个跟 bootstrap一样,也是 twitter 开发的,专门对输入框组进行下拉提示,配置性很强,其中封装了一个匪夷所思的对象叫做 [Bloodhound](https://github.com/twitter/typeahead.js/blob/master/doc/bloodhound.md)(中文名叫做:猎犬,挺有意思的),在 [AmazeUI TagsInput](http://amazeui.github.io/tagsinput/docs/demo.html#%E8%BE%93%E5%85%A5%E6%8F%90%E7%A4%BA) 的文档中也是用了这个 Bloodhound 对象来实现自动化提示,但是我没有用,感觉挺重量级的,自己写ajax获取了服务器上的tags标签列表,自己写mach函数适配了。\r\n我们先来看看官方的示例:\r\n```\r\nvar citynames = new Bloodhound({\r\n datumTokenizer: Bloodhound.tokenizers.obj.whitespace(\'name\'),\r\n queryTokenizer: Bloodhound.tokenizers.whitespace,\r\n prefetch: {\r\n url: \'js/citynames.json\',\r\n filter: function(list) {\r\n return $.map(list, function(cityname) {\r\n return { name: cityname }; });\r\n }\r\n }\r\n});\r\ncitynames.initialize();\r\n$(\'#input-sg\').tagsinput({\r\n typeaheadjs: {\r\n name: \'citynames\',\r\n displayKey: \'name\',\r\n valueKey: \'name\',\r\n source: citynames.ttAdapter()\r\n }\r\n});\r\n```\r\n函数中的 url 指向服务器后台的资源文件,返回json格式就可以,用 springMVC + freemarker 做起来很简单,但在前台我用这个「猎犬」对象试了很多次并没有成功。顺便提一下,手动使用`tagsinput()`函数的时候,别忘了移除`<input>`标签上的`data-am-tagsinput`属性,要不然`tagsinput()`函数是不起作用的,这也是我试错了很久发现的。\r\n随后我放弃了「猎犬」对象,虽然在 [typeahead.js](https://github.com/twitter/typeahead.js/blob/master/doc/bloodhound.md/) 官方文档中提到强烈建议使用这个对象,但我觉得不仅不好用,还将本来很简单的业务逻辑又封装得有点复杂了,索性放弃,参考 [typeahead.js examples](http://twitter.github.io/typeahead.js/examples/) 页面的源代码,使用第一种最直观原始的方式加载智能提示数据。\r\n事实证明,即使是官方示例,我们也有可能用不起来,继续参考了一片文章: http://www.codesec.net/view/445202.html 才顺利使用,最终的代码如下:\r\n```\r\n//定义一个匹配判断函数\r\nvar substringMatcher = function(strs) {\r\n\treturn function findMatches(q, cb) {\r\n //传入的q是键盘输入的字符,传入的cb一个处理数组的函数\r\n\t\tvar matches, substringRegex;\r\n\t\tmatches = [];\r\n\t\tsubstrRegex = new RegExp(q,\"i\");\r\n\t\t$.each(strs, function(i, str) {\r\n\t\t\tif (substrRegex.test(str)){\r\n\t\t\t\tmatches.push({\"title\" : str});\r\n\t\t\t}\r\n\t\t});\r\n\t\tcb(matches);\r\n\t};\r\n};\r\n//使用ajax获取json数据,并给标签增加智能提醒功能\r\n$.get(\"${rc.contextPath}/tags/admin/list\", function(result){\r\n\tif(result.resultCode == 0){\r\n\t\t$(\"#input-sg\").tagsinput({\r\n\t\t\ttypeaheadjs : {\r\n\t \t\t\tname : \"tagsHint\",\r\n\t \t\tdisplayKey : \"title\",\r\n\t \t\tvalueKey : \"title\",\r\n\t \t\tsource : substringMatcher(result.data)\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n}, \"json\");\r\n```\r\n这样就实现了一开始的动画所显示的效果了。','2016-09-07 13:22:56',0,NULL,'auto-hint-tags','2016-09-07 19:57:27',2,1,164,69),(86,'给editor.md添加七牛上传插件','<p><img src=\"http://ocsy1jhmk.bkt.clouddn.com/6a6850af-51c8-4ae2-9f0f-c25e98d09b7a.png\" alt=\"\"><br><img src=\"http://ocsy1jhmk.bkt.clouddn.com/be956a9e-023c-42de-9387-9baff27eca5c.png\" alt=\"\"><br><a href=\"http://pandao.github.io/editor.md/\">editor.md</a> 是一款支持markdown的WEB编辑器,用户可以在自己的网站上使用其进行内容编辑,这在之前的博客里有详细的使用介绍。<br>本文主要介绍如何自定义一个支持七牛上传的editor.md插件。</p>\r\n<h1 id=\"h1-editor-md-\"><a name=\"editor.md 自带图片上传组件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>editor.md 自带图片上传组件</h1><p>首先介绍一下使用editor.md的自带图片上传组件的配置,这对下面开发七牛上传插件有很大的帮助。<br>初始化 editor.md 时使用以下配置即可打开图片上传功能。</p>\r\n<pre><code>&lt;div class=&quot;editormd&quot; id=&quot;id_editormd&quot;&gt;\r\n &lt;textarea class=&quot;editormd-markdown-textarea&quot; name=&quot;markdown&quot;&gt;&lt;/textarea&gt;\r\n &lt;textarea class=&quot;editormd-html-textarea&quot; name=&quot;html&quot;&gt;&lt;/textarea&gt;\r\n&lt;/div&gt;\r\n&lt;script&gt;\r\neditormd(&quot;id_editormd&quot;, {\r\n width : &quot;100%&quot;,\r\n height : 540,\r\n syncScrolling : &quot;single&quot;,\r\n path : &quot;${rc.contextPath}/resources/editormd/lib/&quot;,\r\n imageUpload : true,\r\n imageFormats : [ &quot;jpg&quot;, &quot;jpeg&quot;, &quot;gif&quot;, &quot;png&quot;, &quot;bmp&quot;, &quot;webp&quot; ],\r\n imageUploadURL : &quot;/uploadfile&quot;,\r\n saveHTMLToTextarea : true\r\n}\r\n&lt;/script&gt;\r\n</code></pre><p>其中的<code>/uploadfile</code>指向了图片上传的后台URL地址,在editor.md进行图片上传时,将构造一个 <code>enctype=&quot;multipart/form-data&quot;</code> 的form表单,然后使用iframe的方式向该URL异步POST上传,上传后后台返回一个固定格式json,包含了上传成功与否,以及上传成功后的图片URL信息,json格式如下:<code>{&quot;success&quot;:0|1,&quot;message&quot;:&quot;xxx&quot;,&quot;fileName&quot;:&quot;imageURL&quot;}</code><br><img src=\"http://ocsy1jhmk.bkt.clouddn.com/fc9d93cb-137f-42e5-8904-89616f5b446f.png\" alt=\"\"><br>以JAVA为例,编写后台上传URL服务器端代码(SpringMVC):</p>\r\n<pre><code>@RequestMapping(value=&quot;/uploadfile&quot;,method=RequestMethod.POST)\r\npublic ModelAndView upload(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = &quot;editormd-image-file&quot;, required = false) MultipartFile attach){\r\n String rootPath = request.getSession().getServletContext().getRealPath(&quot;/resources/upload/&quot;);\r\n response.setHeader( &quot;Content-Type&quot; , &quot;text/html&quot; );\r\n ModelAndView mv = new ModelAndView(&quot;upload_result&quot;);\r\n try {\r\n File filePath=new File(rootPath); \r\n /**\r\n * 文件路径不存在则需要创建文件路径\r\n */\r\n if(!filePath.exists()){\r\n filePath.mkdirs();\r\n }\r\n\r\n //最终文件名\r\n File realFile=new File(rootPath + File.separator + UUID.randomUUID().toString() + &quot;.jpg&quot;);\r\n FileUtils.copyInputStreamToFile(attach.getInputStream(), realFile);\r\n\r\n mv.addObject(&quot;success&quot;, 1);\r\n mv.addObject(&quot;message&quot;, &quot;上传成功&quot;);\r\n mv.addObject(&quot;fileName&quot;, realFile.getName());\r\n } catch (Exception e) {\r\n mv.addObject(&quot;success&quot;, 0);\r\n mv.addObject(&quot;message&quot;, &quot;上传失败,异常信息:&quot; + e.getMessage());\r\n }\r\n\r\n return mv;\r\n}\r\n\r\n//upload_result.json:\r\n//{&quot;success&quot;: ${success}, &quot;message&quot;:&quot;${message}&quot;&lt;#if fileName ??&gt;,&quot;url&quot;:&quot;/resources/upload/${fileName}&quot;&lt;/#if&gt;}\r\n</code></pre><p>以上就是使用editor.md图片上传的整个流程了。</p>\r\n<h1 id=\"h1-u4E03u725Bu4E91u5B58u50A8\"><a name=\"七牛云存储\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>七牛云存储</h1><p><a href=\"http://www.qiniu.com/\">七牛云存储</a> 是国内比较著名的云存储服务,为免费用户提供高达10G的存储空间,API齐全,文档够详细,上传下载速度快,丰富的水印、缩放、防盗链等功能。<br>使用云存储对于博客类网站是非常不错的主意,节省了大量的带宽和流量,毕竟我们购买的VPS,包括阿里云、腾讯云等产品,都是流量计费比包年付费划算的,如果采用流量计费的方式,图片流量绝对会比HTML文本大得多,而如果采用包年包月计费,这时候带宽是一定的,加载图片会占用大量带宽,从而影响页面加载速度。此外在进行博客搬家时,我们无需面对高达几百M甚至上G的上传文件夹,仅需将WEB代码和mySQL数据库拷贝一份出来即可。综上所述,为了节省带宽和流量资源,提高网页加载速度,并且简化搬家过程,使用云服务器进行图片资源的第三方存储,是绝对有好处的。</p>\r\n<h1 id=\"h1--editor-md-\"><a name=\"为 editor.md 设计七牛云存储插件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>为 editor.md 设计七牛云存储插件</h1><p>我百度了一下 editor.md 的云存储插件,并没有发现任何可以拿来直接用的插件,于是萌生了自己开发editor.md插件的想法。</p>\r\n<h1 id=\"h1-u4E0Au4F20u6D41u7A0B\"><a name=\"上传流程\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>上传流程</h1><p>七牛的上传流程是这样的:<br>第一步:浏览器客户端向本地URL发送GET请求,请求一串上传令牌token,服务器端通过七牛的公钥、密钥和空间名称,以及自定义的上传策略,构造一串token给客户端,这个token是一组加密过的散列码,长度不固定。这个过程推荐使用ajax请求和json传输。<br>第二步:浏览器将拿到的令牌token和要上传的图片一起通过multipart加密form传输给七牛服务器,七牛服务器对token使用密钥验证和解析,得到是谁上传的,上传到哪个资源空间等信息,然后返回一个处理结果给浏览器。这其中包含上传成功与否,图片上传后的URL后缀地址,错误信息等。<br>以上两步即完成了七牛云服务器上传的全部,下面着手来开发:</p>\r\n<h2 id=\"h2-u63D2u4EF6u5165u53E3\"><a name=\"插件入口\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>插件入口</h2><p>首先editor.md提供了自定义插件接口,这对我们编写插件带来了可行性性,官方的自定义插件文档在这里:<a href=\"http://pandao.github.io/editor.md/examples/define-plugin.html\">http://pandao.github.io/editor.md/examples/define-plugin.html</a><br>依照这个示例程序,改造editor.md的初始化配置函数:</p>\r\n<pre><code>editormd(&quot;domId&quot;, {\r\n width : &quot;100%&quot;,\r\n height : 540,\r\n syncScrolling : &quot;single&quot;,\r\n path : &quot;${rc.contextPath}/resources/editormd/lib/&quot;,\r\n imageUpload : true,\r\n imageFormats : [ &quot;jpg&quot;, &quot;jpeg&quot;, &quot;gif&quot;, &quot;png&quot;, &quot;bmp&quot;, &quot;webp&quot; ],\r\n imageUploadURL : &quot;/uploadfile&quot;,\r\n saveHTMLToTextarea : true,\r\n previewTheme : &quot;dark&quot;,\r\n toolbarIcons : function() {\r\n return [&quot;bold&quot;, &quot;del&quot;, &quot;italic&quot;, &quot;hr&quot;, &quot;image&quot;, &quot;qiniu&quot;, &quot;table&quot;, &quot;datetime&quot;, &quot;|&quot;, &quot;preview&quot;, &quot;watch&quot;, &quot;|&quot;, &quot;fullscreen&quot;];\r\n },\r\n //配置七牛上传插件\r\n toolbarIconsClass : {\r\n qiniu : &quot;fa-cloud-upload&quot;\r\n },\r\n toolbarHandlers : {\r\n qiniu : function(cm, icon, cursor, selection) {\r\n this.imageDialogQiniu();\r\n }\r\n },\r\n qiniuTokenUrl : &quot;/getQiniuToken&quot;, //本地服务器获取七牛token的url\r\n qiniuPublishUrl : &quot;http://ocsy1jhmk.bkt.clouddn.com/&quot; //远程七牛服务器个人发布地址\r\n});\r\n</code></pre><p>可以看到上面我们配置了一个一个名为<code>qiniu</code>的插件,并且定义了它的入口函数:<code>imageDialogQiniu();</code>,并添加了两个配置字符串<code>qiniuTokenUrl</code>和<code>qiniuPublishUrl</code>,这两个字符串第一个是本地服务器URL,用来从本地获取上传令牌,这个后面会讲;第二个URL是在你注册七牛帐户后七牛分配给你的默认空间URL前缀,你可以付费开通正式帐号来自定义这个前缀,我暂时没有特别的需求,先使用的七牛免费功能,等后续有更多需求的时候再考虑是否成为七牛付费会员。</p>\r\n<h2 id=\"h2-u4E03u725Bu7A7Au95F4u524Du7F00u53CAu4E0Au4F20u4EE4u724C\"><a name=\"七牛空间前缀及上传令牌\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>七牛空间前缀及上传令牌</h2><p><img src=\"http://ocsy1jhmk.bkt.clouddn.com/0a8b0c67-1080-4f62-ad42-9da75bc37ac5.png\" alt=\"\"><br>注册成为七牛会员后,进入七牛控制台,添加一个「对象存储」资源,如图所示,右边的「我的资源」中会出现这个资源空间,点击这个资源,即可看到自己的上传空间的URL前缀了,上面讲过,免费用户的空间前缀是随机的HASH码,付费以后可以自定义这个前缀,界面如下:<br><img src=\"http://ocsy1jhmk.bkt.clouddn.com/12d7899f-4d76-438a-83ee-6b0f797236d4.png\" alt=\"\"><br>下面我们来查看七牛分配给我们构造上传令牌时所需的公钥和密钥:<br><img src=\"http://ocsy1jhmk.bkt.clouddn.com/de9ab5cc-ad93-4fe7-bb3c-d98e7fb40b03.png\" alt=\"\"><br>右上方「个人面板」-&gt;「密钥管理」即可查看,其中AK是公钥,SK是密钥,虽说现在的RSA加密机制中公钥是可以传播的,密钥是不可以传播的,但我还是建议这两个密钥都不要随意公布,所以这里我隐藏了。<br>好了,URL前缀,公钥,密钥都有了,七牛给我们的这三个看上去像乱码,却蕴含特殊意义的字符串都拿到手了,继续我们的开发吧。</p>\r\n<h2 id=\"h2--js\"><a name=\"编写插件JS\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>编写插件JS</h2><p><img src=\"http://ocsy1jhmk.bkt.clouddn.com/4ba65039-0a9d-4bf4-9d30-e8efcf4b3d13.png\" alt=\"\"><br>在editor.md的文档结构中,plugins文件夹是所有的自带的插件,我们添加七牛插件也是如此,添加一个文件夹,名为<code>image-dialog-qiniu</code>,并创建<code>image-dialog-qiniu.js</code>。<br>其中的代码我大部分模仿了原有的<code>image-dialog</code>插件,但是改造了其有点不知所谓的iframe上传方法,使用我偏爱的jQuery ajax post异步上传,掌握这些知识点的朋友应该能够看明白,代码如下:</p>\r\n<pre><code>/*!\r\n * Image (upload) dialog By Qiniu plugin for Editor.md\r\n *\r\n * @file image-dialog-qiniu.js\r\n * @author newflydd@189.cn\r\n * @version 1.3.4\r\n * @updateTime 2016-09-02\r\n * {@link http://www.hexcode.cn}\r\n * @license MIT\r\n */\r\n\r\n(function() {\r\n\r\n var factory = function (exports) {\r\n\r\n var pluginName = &quot;image-dialog-qiniu&quot;;\r\n\r\n exports.fn.imageDialogQiniu = function() {\r\n\r\n var _this = this;\r\n var cm = this.cm;\r\n var lang = this.lang;\r\n var editor = this.editor;\r\n var settings = this.settings;\r\n var cursor = cm.getCursor();\r\n var selection = cm.getSelection();\r\n var imageLang = lang.dialog.image;\r\n var classPrefix = this.classPrefix;\r\n var iframeName = classPrefix + &quot;image-iframe&quot;;\r\n var dialogName = classPrefix + pluginName, dialog;\r\n var ajaxToken = &quot;&quot;; //向本地服务器请求七牛的上传token\r\n\r\n cm.focus();\r\n\r\n var loading = function(show) {\r\n var _loading = dialog.find(&quot;.&quot; + classPrefix + &quot;dialog-mask&quot;);\r\n _loading[(show) ? &quot;show&quot; : &quot;hide&quot;]();\r\n };\r\n\r\n if (editor.find(&quot;.&quot; + dialogName).length &lt; 1) {\r\n var guid = (new Date).getTime();\r\n var action = settings.imageUploadURL + (settings.imageUploadURL.indexOf(&quot;?&quot;) &gt;= 0 ? &quot;&amp;&quot; : &quot;?&quot;) + &quot;guid=&quot; + guid;\r\n\r\n if (settings.crossDomainUpload)\r\n {\r\n action += &quot;&amp;callback=&quot; + settings.uploadCallbackURL + &quot;&amp;dialog_id=editormd-image-dialog-&quot; + guid;\r\n }\r\n\r\n var dialogContent = ( (settings.imageUpload) ? &quot;&lt;form id=\\&quot;qiniuUploadForm\\&quot; method=\\&quot;post\\&quot; enctype=\\&quot;multipart/form-data\\&quot; class=\\&quot;&quot; + classPrefix + &quot;form\\&quot; onsubmit=\\&quot;return false;\\&quot;&gt;&quot; : &quot;&lt;div class=\\&quot;&quot; + classPrefix + &quot;form\\&quot;&gt;&quot; ) + \r\n &quot;&lt;label&gt;&quot; + imageLang.url + &quot;&lt;/label&gt;&quot; +\r\n &quot;&lt;input type=\\&quot;text\\&quot; data-url /&gt;&quot; + (function(){\r\n return (settings.imageUpload) ? &quot;&lt;div class=\\&quot;&quot; + classPrefix + &quot;file-input\\&quot;&gt;&quot; +\r\n &quot;&lt;input type=\\&quot;file\\&quot; name=\\&quot;file\\&quot; accept=\\&quot;image/*\\&quot; /&gt;&quot; +\r\n &quot;&lt;input type=\\&quot;submit\\&quot; value=\\&quot;七牛上传\\&quot; click=\\&quot;alert(&#39;dd&#39;)\\&quot; /&gt;&quot; +\r\n &quot;&lt;/div&gt;&quot; : &quot;&quot;;\r\n })() +\r\n &quot;&lt;br/&gt;&quot; +\r\n &quot;&lt;input name=\\&quot;token\\&quot; type=\\&quot;hidden\\&quot; value=\\&quot;&quot; + ajaxToken + &quot;\\&quot;&gt;&quot; + //七牛的上传token\r\n &quot;&lt;label&gt;&quot; + imageLang.alt + &quot;&lt;/label&gt;&quot; +\r\n &quot;&lt;input type=\\&quot;text\\&quot; value=\\&quot;&quot; + selection + &quot;\\&quot; data-alt /&gt;&quot; +\r\n &quot;&lt;br/&gt;&quot; +\r\n &quot;&lt;label&gt;&quot; + imageLang.link + &quot;&lt;/label&gt;&quot; +\r\n &quot;&lt;input type=\\&quot;text\\&quot; value=\\&quot;http://\\&quot; data-link /&gt;&quot; +\r\n &quot;&lt;br/&gt;&quot; +\r\n ( (settings.imageUpload) ? &quot;&lt;/form&gt;&quot; : &quot;&lt;/div&gt;&quot;);\r\n\r\n dialog = this.createDialog({\r\n title : imageLang.title,\r\n width : (settings.imageUpload) ? 465 : 380,\r\n height : 254,\r\n name : dialogName,\r\n content : dialogContent,\r\n mask : settings.dialogShowMask,\r\n drag : settings.dialogDraggable,\r\n lockScreen : settings.dialogLockScreen,\r\n maskStyle : {\r\n opacity : settings.dialogMaskOpacity,\r\n backgroundColor : settings.dialogMaskBgColor\r\n },\r\n buttons : {\r\n enter : [lang.buttons.enter, function() {\r\n var url = this.find(&quot;[data-url]&quot;).val();\r\n var alt = this.find(&quot;[data-alt]&quot;).val();\r\n var link = this.find(&quot;[data-link]&quot;).val();\r\n\r\n if (url === &quot;&quot;) {\r\n alert(imageLang.imageURLEmpty);\r\n return false;\r\n }\r\n\r\n var altAttr = (alt !== &quot;&quot;) ? &quot; \\&quot;&quot; + alt + &quot;\\&quot;&quot; : &quot;&quot;;\r\n\r\n if (link === &quot;&quot; || link === &quot;http://&quot;)\r\n {\r\n cm.replaceSelection(&quot;![&quot; + alt + &quot;](&quot; + url + altAttr + &quot;)&quot;);\r\n }\r\n else\r\n {\r\n cm.replaceSelection(&quot;[![&quot; + alt + &quot;](&quot; + url + altAttr + &quot;)](&quot; + link + altAttr + &quot;)&quot;);\r\n }\r\n\r\n if (alt === &quot;&quot;) {\r\n cm.setCursor(cursor.line, cursor.ch + 2);\r\n }\r\n\r\n this.hide().lockScreen(false).hideMask();\r\n\r\n return false;\r\n }],\r\n\r\n cancel : [lang.buttons.cancel, function() {\r\n this.hide().lockScreen(false).hideMask();\r\n\r\n return false;\r\n }]\r\n }\r\n });\r\n\r\n dialog.attr(&quot;id&quot;, classPrefix + &quot;image-dialog-&quot; + guid);\r\n\r\n if (!settings.imageUpload) {\r\n return ;\r\n }\r\n\r\n var fileInput = dialog.find(&quot;[name=\\&quot;file\\&quot;]&quot;);\r\n\r\n var submitHandler = function() {\r\n $.ajax({\r\n url : settings.qiniuTokenUrl,\r\n type : &quot;post&quot;,\r\n dataType : &quot;json&quot;,\r\n timeout : 2000,\r\n beforeSend : function(){loading(true);},\r\n success : function(result){\r\n if(result.resultCode == 0){\r\n ajaxToken = result.data;\r\n\r\n if(ajaxToken === &quot;&quot;){\r\n loading(false);\r\n alert(&quot;没有获取到有效的上传令牌,无法上传!&quot;);\r\n return;\r\n }\r\n dialog.find(&quot;[name=\\&quot;token\\&quot;]&quot;).val(ajaxToken);\r\n var formData = new FormData( $(&quot;#qiniuUploadForm&quot;)[0] );\r\n dialog.find(&quot;[name=\\&quot;token\\&quot;]&quot;).val(); //隐藏令牌\r\n $.ajax({\r\n url: &#39;http://upload.qiniu.com/&#39; , \r\n type: &#39;POST&#39;, \r\n data: formData,\r\n dataType: &quot;json&quot;,\r\n beforeSend:function(){loading(true);},\r\n cache: false,\r\n contentType: false,\r\n processData: false,\r\n timeout : 30000,\r\n success: function (result) {\r\n dialog.find(&quot;[data-url]&quot;).val(settings.qiniuPublishUrl + result.key);\r\n },\r\n error : function(){alert(&quot;上传超时&quot;);},\r\n complete:function(){loading(false);}\r\n });\r\n }\r\n else\r\n alert(result.message);\r\n }\r\n });\r\n };\r\n\r\n dialog.find(&quot;[type=\\&quot;submit\\&quot;]&quot;).bind(&quot;click&quot;, submitHandler);\r\n\r\n fileInput.bind(&quot;change&quot;, function() {\r\n var fileName = fileInput.val();\r\n var isImage = new RegExp(&quot;(\\\\.(&quot; + settings.imageFormats.join(&quot;|&quot;) + &quot;))$&quot;); // /(\\.(webp|jpg|jpeg|gif|bmp|png))$/\r\n\r\n if (fileName === &quot;&quot;)\r\n {\r\n alert(imageLang.uploadFileEmpty);\r\n\r\n return false;\r\n }\r\n\r\n if (!isImage.test(fileName)){\r\n alert(imageLang.formatNotAllowed + settings.imageFormats.join(&quot;, &quot;));\r\n return false;\r\n }\r\n\r\n dialog.find(&quot;[type=\\&quot;submit\\&quot;]&quot;).trigger(&quot;click&quot;);\r\n });\r\n }\r\n\r\n dialog = editor.find(&quot;.&quot; + dialogName);\r\n dialog.find(&quot;[type=\\&quot;text\\&quot;]&quot;).val(&quot;&quot;);\r\n dialog.find(&quot;[type=\\&quot;file\\&quot;]&quot;).val(&quot;&quot;);\r\n dialog.find(&quot;[data-link]&quot;).val(&quot;http://&quot;);\r\n\r\n this.dialogShowMask(dialog);\r\n this.dialogLockScreen();\r\n dialog.show();\r\n\r\n };\r\n\r\n };\r\n\r\n // CommonJS/Node.js\r\n if (typeof require === &quot;function&quot; &amp;&amp; typeof exports === &quot;object&quot; &amp;&amp; typeof module === &quot;object&quot;)\r\n {\r\n module.exports = factory;\r\n }\r\n else if (typeof define === &quot;function&quot;) // AMD/CMD/Sea.js\r\n {\r\n if (define.amd) { // for Require.js\r\n\r\n define([&quot;editormd&quot;], function(editormd) {\r\n factory(editormd);\r\n });\r\n\r\n } else { // for Sea.js\r\n define(function(require) {\r\n var editormd = require(&quot;./../../editormd&quot;);\r\n factory(editormd);\r\n });\r\n }\r\n }\r\n else\r\n {\r\n factory(window.editormd);\r\n }\r\n\r\n})();\r\n</code></pre><p>后面一段的<code>// CommonJS/Node.js</code>是原模板自带的,可能是为Node.js编写的WEB服务器提供功能,我们用不到,这里就不深究了。</p>\r\n<h2 id=\"h2-u672Cu5730u670Du52A1u5668u4EE4u724Cu751Fu6210\"><a name=\"本地服务器令牌生成\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>本地服务器令牌生成</h2><p>在刚刚那个插件JS中,我们首先向本地服务器索取上传令牌,这个令牌是一串hash字符串,内容包括了从申请令牌到上传图片成功或者失败之间的上传时限,上传的空间,以及更多的上传策略。构造这一串令牌的代码在本地WEB服务器中实现,以JAVA为例:</p>\r\n<pre><code>/**\r\n * 七牛上传令牌获取\r\n * @return\r\n * @throws JblogException \r\n */\r\n@RequestMapping(value=&quot;/getQiniuToken&quot;,method=RequestMethod.POST)\r\npublic ModelAndView qiniu() throws JblogException{\r\n Result result = new Result();\r\n\r\n if(StringUtils.isEmpty(BlogConfig.QN_ACCESSKEY) || StringUtils.isEmpty(BlogConfig.QN_SECRETKEY)){\r\n result.setCode(Result.FAIL);\r\n result.setMessage(&quot;数据库中没有正确的七牛服务器密钥,无法生成令牌&quot;);\r\n return new ModelAndView(&quot;ajaxResult&quot;, &quot;result&quot;, result);\r\n }\r\n\r\n Auth auth = Auth.create(BlogConfig.QN_ACCESSKEY, BlogConfig.QN_SECRETKEY);\r\n String token = auth.uploadToken(\r\n &quot;blog-images&quot;, //空间名称\r\n null, //key,最终资源名称,一般为空,让服务器自己生成\r\n 3600, //3600秒,上传时限\r\n new StringMap() //其他上传策略\r\n .put(&quot;saveKey&quot;, UUID.randomUUID().toString() + &quot;$(ext)&quot;)\r\n );\r\n\r\n result.setData(&quot;\\&quot;&quot; + token + &quot;\\&quot;&quot;);\r\n\r\n return new ModelAndView(&quot;ajaxResult&quot;, &quot;result&quot;, result);\r\n}\r\n</code></pre><p>其中的Auth是七牛官网提供的jar包中的,不需要直接下载,可以使用maven或者gradle获取,七牛已经将这个jar提交到了maven国际公共库了,介绍文档在这里:<a href=\"http://developer.qiniu.com/code/v7/sdk/java.html\">http://developer.qiniu.com/code/v7/sdk/java.html</a><br>我用的gradle,直接添加:</p>\r\n<pre><code>compile &#39;com.qiniu:qiniu-java-sdk:7.x.+&#39;\r\n</code></pre><p>到此,我们整个的editor.md七牛插件就编写好了。如下方所示:<br><img src=\"http://ocsy1jhmk.bkt.clouddn.com/ecd82c08-0a1a-4365-83bf-a998ea87c36e.png\" alt=\"\"></p>\r\n<h1 id=\"h1-u56DEu987Eu4E00u4E0Bu5F00u53D1u6B65u9AA4\"><a name=\"回顾一下开发步骤\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>回顾一下开发步骤</h1><ol>\r\n<li>在editor.md的初始化函数中添加插件的描述。</li><li>在editor.md的plugins文件中添加相关的插件文件夹和插件JS,js代码可以根据插件模板书写。</li><li>到七牛官网注册,并在个人中心添加资源空间,收集三个字符串数据:资源空间的url前缀,AK公钥,SK私钥。</li><li>编写本地服务器端代码,使用AK和SK构造token上传令牌,构造时使用的类来自七牛JAVA文档。</li></ol>\r\n<p>其中最复杂的是第二步,我所公布的插件JS篇幅比较长,需要耐心观察。其中的知识点包括jQuery;动态生成form;ajax提交等,这些都是互联网应用开发的基础知识点。</p>\r\n','![](http://ocsy1jhmk.bkt.clouddn.com/6a6850af-51c8-4ae2-9f0f-c25e98d09b7a.png)\r\n![](http://ocsy1jhmk.bkt.clouddn.com/be956a9e-023c-42de-9387-9baff27eca5c.png)\r\n[editor.md](http://pandao.github.io/editor.md/) 是一款支持markdown的WEB编辑器,用户可以在自己的网站上使用其进行内容编辑,这在之前的博客里有详细的使用介绍。\r\n本文主要介绍如何自定义一个支持七牛上传的editor.md插件。\r\n#editor.md 自带图片上传组件\r\n首先介绍一下使用editor.md的自带图片上传组件的配置,这对下面开发七牛上传插件有很大的帮助。\r\n初始化 editor.md 时使用以下配置即可打开图片上传功能。\r\n```\r\n<div class=\"editormd\" id=\"id_editormd\">\r\n\t<textarea class=\"editormd-markdown-textarea\" name=\"markdown\"></textarea>\r\n\t<textarea class=\"editormd-html-textarea\" name=\"html\"></textarea>\r\n</div>\r\n<script>\r\neditormd(\"id_editormd\", {\r\n\twidth : \"100%\",\r\n\theight : 540,\r\n\tsyncScrolling : \"single\",\r\n\tpath : \"${rc.contextPath}/resources/editormd/lib/\",\r\n\timageUpload : true,\r\n\timageFormats : [ \"jpg\", \"jpeg\", \"gif\", \"png\", \"bmp\", \"webp\" ],\r\n\timageUploadURL : \"/uploadfile\",\r\n\tsaveHTMLToTextarea : true\r\n}\r\n</script>\r\n```\r\n其中的`/uploadfile`指向了图片上传的后台URL地址,在editor.md进行图片上传时,将构造一个 `enctype=\"multipart/form-data\"` 的form表单,然后使用iframe的方式向该URL异步POST上传,上传后后台返回一个固定格式json,包含了上传成功与否,以及上传成功后的图片URL信息,json格式如下:`{\"success\":0|1,\"message\":\"xxx\",\"fileName\":\"imageURL\"}`\r\n![](http://ocsy1jhmk.bkt.clouddn.com/fc9d93cb-137f-42e5-8904-89616f5b446f.png)\r\n以JAVA为例,编写后台上传URL服务器端代码(SpringMVC):\r\n```\r\n@RequestMapping(value=\"/uploadfile\",method=RequestMethod.POST)\r\npublic ModelAndView upload(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = \"editormd-image-file\", required = false) MultipartFile attach){\r\n\tString rootPath = request.getSession().getServletContext().getRealPath(\"/resources/upload/\");\r\n\tresponse.setHeader( \"Content-Type\" , \"text/html\" );\r\n\tModelAndView mv = new ModelAndView(\"upload_result\");\r\n\ttry {\r\n\t\tFile filePath=new File(rootPath);\t\t\r\n\t\t/**\r\n\t\t * 文件路径不存在则需要创建文件路径\r\n\t\t */\r\n\t\tif(!filePath.exists()){\r\n\t\t\tfilePath.mkdirs();\r\n\t\t}\r\n\t\t\r\n\t\t//最终文件名\r\n File realFile=new File(rootPath + File.separator + UUID.randomUUID().toString() + \".jpg\");\r\n FileUtils.copyInputStreamToFile(attach.getInputStream(), realFile);\r\n\t\t\r\n mv.addObject(\"success\", 1);\r\n\t\tmv.addObject(\"message\", \"上传成功\");\r\n\t\tmv.addObject(\"fileName\", realFile.getName());\r\n\t} catch (Exception e) {\r\n\t\tmv.addObject(\"success\", 0);\r\n\t\tmv.addObject(\"message\", \"上传失败,异常信息:\" + e.getMessage());\r\n\t}\r\n\t\r\n\treturn mv;\r\n}\r\n\r\n//upload_result.json:\r\n//{\"success\": ${success}, \"message\":\"${message}\"<#if fileName ??>,\"url\":\"/resources/upload/${fileName}\"</#if>}\r\n```\r\n以上就是使用editor.md图片上传的整个流程了。\r\n#七牛云存储\r\n[七牛云存储](http://www.qiniu.com/) 是国内比较著名的云存储服务,为免费用户提供高达10G的存储空间,API齐全,文档够详细,上传下载速度快,丰富的水印、缩放、防盗链等功能。\r\n使用云存储对于博客类网站是非常不错的主意,节省了大量的带宽和流量,毕竟我们购买的VPS,包括阿里云、腾讯云等产品,都是流量计费比包年付费划算的,如果采用流量计费的方式,图片流量绝对会比HTML文本大得多,而如果采用包年包月计费,这时候带宽是一定的,加载图片会占用大量带宽,从而影响页面加载速度。此外在进行博客搬家时,我们无需面对高达几百M甚至上G的上传文件夹,仅需将WEB代码和mySQL数据库拷贝一份出来即可。综上所述,为了节省带宽和流量资源,提高网页加载速度,并且简化搬家过程,使用云服务器进行图片资源的第三方存储,是绝对有好处的。\r\n\r\n#为 editor.md 设计七牛云存储插件\r\n我百度了一下 editor.md 的云存储插件,并没有发现任何可以拿来直接用的插件,于是萌生了自己开发editor.md插件的想法。\r\n#上传流程\r\n七牛的上传流程是这样的:\r\n第一步:浏览器客户端向本地URL发送GET请求,请求一串上传令牌token,服务器端通过七牛的公钥、密钥和空间名称,以及自定义的上传策略,构造一串token给客户端,这个token是一组加密过的散列码,长度不固定。这个过程推荐使用ajax请求和json传输。\r\n第二步:浏览器将拿到的令牌token和要上传的图片一起通过multipart加密form传输给七牛服务器,七牛服务器对token使用密钥验证和解析,得到是谁上传的,上传到哪个资源空间等信息,然后返回一个处理结果给浏览器。这其中包含上传成功与否,图片上传后的URL后缀地址,错误信息等。\r\n以上两步即完成了七牛云服务器上传的全部,下面着手来开发:\r\n##插件入口\r\n首先editor.md提供了自定义插件接口,这对我们编写插件带来了可行性性,官方的自定义插件文档在这里:http://pandao.github.io/editor.md/examples/define-plugin.html\r\n依照这个示例程序,改造editor.md的初始化配置函数:\r\n```\r\neditormd(\"domId\", {\r\n width : \"100%\",\r\n height : 540,\r\n syncScrolling : \"single\",\r\n path : \"${rc.contextPath}/resources/editormd/lib/\",\r\n\timageUpload : true,\r\n\timageFormats : [ \"jpg\", \"jpeg\", \"gif\", \"png\", \"bmp\", \"webp\" ],\r\n\timageUploadURL : \"/uploadfile\",\r\n\tsaveHTMLToTextarea : true,\r\n\tpreviewTheme : \"dark\",\r\n\ttoolbarIcons : function() {\r\n return [\"bold\", \"del\", \"italic\", \"hr\", \"image\", \"qiniu\", \"table\", \"datetime\", \"|\", \"preview\", \"watch\", \"|\", \"fullscreen\"];\r\n },\r\n //配置七牛上传插件\r\n toolbarIconsClass : {\r\n qiniu : \"fa-cloud-upload\"\r\n },\r\n toolbarHandlers : {\r\n qiniu : function(cm, icon, cursor, selection) {\r\n \tthis.imageDialogQiniu();\r\n }\r\n },\r\n qiniuTokenUrl : \"/getQiniuToken\",\t\t\t\t\t\t//本地服务器获取七牛token的url\r\n qiniuPublishUrl : \"http://ocsy1jhmk.bkt.clouddn.com/\"\t//远程七牛服务器个人发布地址\r\n});\r\n```\r\n可以看到上面我们配置了一个一个名为`qiniu`的插件,并且定义了它的入口函数:`imageDialogQiniu();`,并添加了两个配置字符串`qiniuTokenUrl`和`qiniuPublishUrl`,这两个字符串第一个是本地服务器URL,用来从本地获取上传令牌,这个后面会讲;第二个URL是在你注册七牛帐户后七牛分配给你的默认空间URL前缀,你可以付费开通正式帐号来自定义这个前缀,我暂时没有特别的需求,先使用的七牛免费功能,等后续有更多需求的时候再考虑是否成为七牛付费会员。\r\n##七牛空间前缀及上传令牌\r\n![](http://ocsy1jhmk.bkt.clouddn.com/0a8b0c67-1080-4f62-ad42-9da75bc37ac5.png)\r\n注册成为七牛会员后,进入七牛控制台,添加一个「对象存储」资源,如图所示,右边的「我的资源」中会出现这个资源空间,点击这个资源,即可看到自己的上传空间的URL前缀了,上面讲过,免费用户的空间前缀是随机的HASH码,付费以后可以自定义这个前缀,界面如下:\r\n![](http://ocsy1jhmk.bkt.clouddn.com/12d7899f-4d76-438a-83ee-6b0f797236d4.png)\r\n下面我们来查看七牛分配给我们构造上传令牌时所需的公钥和密钥:\r\n![](http://ocsy1jhmk.bkt.clouddn.com/de9ab5cc-ad93-4fe7-bb3c-d98e7fb40b03.png)\r\n右上方「个人面板」->「密钥管理」即可查看,其中AK是公钥,SK是密钥,虽说现在的RSA加密机制中公钥是可以传播的,密钥是不可以传播的,但我还是建议这两个密钥都不要随意公布,所以这里我隐藏了。\r\n好了,URL前缀,公钥,密钥都有了,七牛给我们的这三个看上去像乱码,却蕴含特殊意义的字符串都拿到手了,继续我们的开发吧。\r\n## 编写插件JS\r\n![](http://ocsy1jhmk.bkt.clouddn.com/4ba65039-0a9d-4bf4-9d30-e8efcf4b3d13.png)\r\n在editor.md的文档结构中,plugins文件夹是所有的自带的插件,我们添加七牛插件也是如此,添加一个文件夹,名为`image-dialog-qiniu`,并创建`image-dialog-qiniu.js`。\r\n其中的代码我大部分模仿了原有的`image-dialog`插件,但是改造了其有点不知所谓的iframe上传方法,使用我偏爱的jQuery ajax post异步上传,掌握这些知识点的朋友应该能够看明白,代码如下:\r\n```\r\n/*!\r\n * Image (upload) dialog By Qiniu plugin for Editor.md\r\n *\r\n * @file image-dialog-qiniu.js\r\n * @author newflydd@189.cn\r\n * @version 1.3.4\r\n * @updateTime 2016-09-02\r\n * {@link http://www.hexcode.cn}\r\n * @license MIT\r\n */\r\n\r\n(function() {\r\n\r\n var factory = function (exports) {\r\n\r\n\t\tvar pluginName = \"image-dialog-qiniu\";\r\n\r\n\t\texports.fn.imageDialogQiniu = function() {\r\n\r\n var _this = this;\r\n var cm = this.cm;\r\n var lang = this.lang;\r\n var editor = this.editor;\r\n var settings = this.settings;\r\n var cursor = cm.getCursor();\r\n var selection = cm.getSelection();\r\n var imageLang = lang.dialog.image;\r\n var classPrefix = this.classPrefix;\r\n var iframeName = classPrefix + \"image-iframe\";\r\n\t\t\tvar dialogName = classPrefix + pluginName, dialog;\r\n\t\t\tvar ajaxToken\t= \"\";\t\t//向本地服务器请求七牛的上传token\r\n\t\t\t\r\n\t\t\tcm.focus();\r\n\r\n var loading = function(show) {\r\n var _loading = dialog.find(\".\" + classPrefix + \"dialog-mask\");\r\n _loading[(show) ? \"show\" : \"hide\"]();\r\n };\r\n \r\n if (editor.find(\".\" + dialogName).length < 1) {\r\n var guid = (new Date).getTime();\r\n var action = settings.imageUploadURL + (settings.imageUploadURL.indexOf(\"?\") >= 0 ? \"&\" : \"?\") + \"guid=\" + guid;\r\n\r\n if (settings.crossDomainUpload)\r\n {\r\n action += \"&callback=\" + settings.uploadCallbackURL + \"&dialog_id=editormd-image-dialog-\" + guid;\r\n }\r\n \r\n var dialogContent = ( (settings.imageUpload) ? \"<form id=\\\"qiniuUploadForm\\\" method=\\\"post\\\" enctype=\\\"multipart/form-data\\\" class=\\\"\" + classPrefix + \"form\\\" onsubmit=\\\"return false;\\\">\" : \"<div class=\\\"\" + classPrefix + \"form\\\">\" ) + \r\n \"<label>\" + imageLang.url + \"</label>\" +\r\n \"<input type=\\\"text\\\" data-url />\" + (function(){\r\n return (settings.imageUpload) ? \"<div class=\\\"\" + classPrefix + \"file-input\\\">\" +\r\n \"<input type=\\\"file\\\" name=\\\"file\\\" accept=\\\"image/*\\\" />\"\t\t\t+\r\n \"<input type=\\\"submit\\\" value=\\\"七牛上传\\\" click=\\\"alert(\'dd\')\\\" />\" +\r\n \"</div>\" : \"\";\r\n })() +\r\n \"<br/>\" +\r\n \"<input name=\\\"token\\\" type=\\\"hidden\\\" value=\\\"\" + ajaxToken + \"\\\">\"\t+\t\t//七牛的上传token\r\n \"<label>\" + imageLang.alt + \"</label>\" +\r\n \"<input type=\\\"text\\\" value=\\\"\" + selection + \"\\\" data-alt />\" +\r\n \"<br/>\" +\r\n \"<label>\" + imageLang.link + \"</label>\" +\r\n \"<input type=\\\"text\\\" value=\\\"http://\\\" data-link />\" +\r\n \"<br/>\" +\r\n ( (settings.imageUpload) ? \"</form>\" : \"</div>\");\r\n\r\n dialog = this.createDialog({\r\n title : imageLang.title,\r\n width : (settings.imageUpload) ? 465 : 380,\r\n height : 254,\r\n name : dialogName,\r\n content : dialogContent,\r\n mask : settings.dialogShowMask,\r\n drag : settings.dialogDraggable,\r\n lockScreen : settings.dialogLockScreen,\r\n maskStyle : {\r\n opacity : settings.dialogMaskOpacity,\r\n backgroundColor : settings.dialogMaskBgColor\r\n },\r\n buttons : {\r\n enter : [lang.buttons.enter, function() {\r\n var url = this.find(\"[data-url]\").val();\r\n var alt = this.find(\"[data-alt]\").val();\r\n var link = this.find(\"[data-link]\").val();\r\n\r\n if (url === \"\") {\r\n alert(imageLang.imageURLEmpty);\r\n return false;\r\n }\r\n\r\n\t\t\t\t\t\t\tvar altAttr = (alt !== \"\") ? \" \\\"\" + alt + \"\\\"\" : \"\";\r\n\r\n if (link === \"\" || link === \"http://\")\r\n {\r\n cm.replaceSelection(\"![\" + alt + \"](\" + url + altAttr + \")\");\r\n }\r\n else\r\n {\r\n cm.replaceSelection(\"[![\" + alt + \"](\" + url + altAttr + \")](\" + link + altAttr + \")\");\r\n }\r\n\r\n if (alt === \"\") {\r\n cm.setCursor(cursor.line, cursor.ch + 2);\r\n }\r\n\r\n this.hide().lockScreen(false).hideMask();\r\n\r\n return false;\r\n }],\r\n\r\n cancel : [lang.buttons.cancel, function() {\r\n this.hide().lockScreen(false).hideMask();\r\n\r\n return false;\r\n }]\r\n }\r\n });\r\n\r\n dialog.attr(\"id\", classPrefix + \"image-dialog-\" + guid);\r\n\r\n\t\t\t\tif (!settings.imageUpload) {\r\n return ;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tvar fileInput = dialog.find(\"[name=\\\"file\\\"]\");\r\n\t\t\t\t\r\n\t\t\t\tvar submitHandler = function() {\r\n\t\t\t\t\t$.ajax({\r\n\t\t \turl\t\t : settings.qiniuTokenUrl,\r\n\t\t \ttype\t : \"post\",\r\n\t\t \tdataType : \"json\",\r\n\t\t \ttimeout : 2000,\r\n\t\t \tbeforeSend : function(){loading(true);},\r\n\t\t \tsuccess : function(result){\r\n\t\t \t\tif(result.resultCode == 0){\r\n\t ajaxToken = result.data;\r\n\t \r\n\t if(ajaxToken === \"\"){\r\n\t \tloading(false);\r\n\t \talert(\"没有获取到有效的上传令牌,无法上传!\");\r\n\t \t\t\t\t\t\treturn;\r\n\t \t\t\t\t\t}\r\n\t \t\t\t\t\tdialog.find(\"[name=\\\"token\\\"]\").val(ajaxToken);\r\n\t \tvar formData = new FormData( $(\"#qiniuUploadForm\")[0] );\r\n\t \tdialog.find(\"[name=\\\"token\\\"]\").val();\t//隐藏令牌\r\n\t \t\t$.ajax({\r\n\t \t\t\turl: \'http://upload.qiniu.com/\' , \r\n\t \t\t\ttype: \'POST\', \r\n\t \t\t\tdata: formData,\r\n\t \t\t\tdataType: \"json\",\r\n\t \t\t\tbeforeSend:function(){loading(true);},\r\n\t \t\t\tcache: false,\r\n\t \t\t\tcontentType: false,\r\n\t \t\t\tprocessData: false,\r\n\t \t\t\ttimeout : 30000,\r\n\t \t\t\tsuccess: function (result) {\r\n\t \t\t\t\tdialog.find(\"[data-url]\").val(settings.qiniuPublishUrl + result.key);\r\n\t \t\t\t},\r\n\t \t\t\terror : function(){alert(\"上传超时\");},\r\n\t \t\t\tcomplete:function(){loading(false);}\r\n\t \t\t});\r\n\t\t \t\t}\r\n\t else\r\n\t alert(result.message);\r\n\t\t \t}\r\n\t\t });\r\n };\r\n \r\n dialog.find(\"[type=\\\"submit\\\"]\").bind(\"click\", submitHandler);\r\n\r\n\t\t\t\tfileInput.bind(\"change\", function() {\r\n\t\t\t\t\tvar fileName = fileInput.val();\r\n\t\t\t\t\tvar isImage = new RegExp(\"(\\\\.(\" + settings.imageFormats.join(\"|\") + \"))$\"); // /(\\.(webp|jpg|jpeg|gif|bmp|png))$/\r\n\r\n\t\t\t\t\tif (fileName === \"\")\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\talert(imageLang.uploadFileEmpty);\r\n\r\n return false;\r\n\t\t\t\t\t}\r\n\r\n if (!isImage.test(fileName)){\r\n\t\t\t\t\t\talert(imageLang.formatNotAllowed + settings.imageFormats.join(\", \"));\r\n return false;\r\n\t\t\t\t\t}\r\n \r\n dialog.find(\"[type=\\\"submit\\\"]\").trigger(\"click\");\r\n\t\t\t\t});\r\n }\r\n\r\n\t\t\tdialog = editor.find(\".\" + dialogName);\r\n\t\t\tdialog.find(\"[type=\\\"text\\\"]\").val(\"\");\r\n\t\t\tdialog.find(\"[type=\\\"file\\\"]\").val(\"\");\r\n\t\t\tdialog.find(\"[data-link]\").val(\"http://\");\r\n\r\n\t\t\tthis.dialogShowMask(dialog);\r\n\t\t\tthis.dialogLockScreen();\r\n\t\t\tdialog.show();\r\n\r\n\t\t};\r\n\r\n\t};\r\n\r\n\t// CommonJS/Node.js\r\n\tif (typeof require === \"function\" && typeof exports === \"object\" && typeof module === \"object\")\r\n {\r\n module.exports = factory;\r\n }\r\n\telse if (typeof define === \"function\") // AMD/CMD/Sea.js\r\n {\r\n\t\tif (define.amd) { // for Require.js\r\n\r\n\t\t\tdefine([\"editormd\"], function(editormd) {\r\n factory(editormd);\r\n });\r\n\r\n\t\t} else { // for Sea.js\r\n\t\t\tdefine(function(require) {\r\n var editormd = require(\"./../../editormd\");\r\n factory(editormd);\r\n });\r\n\t\t}\r\n\t}\r\n\telse\r\n\t{\r\n factory(window.editormd);\r\n\t}\r\n\r\n})();\r\n\r\n```\r\n后面一段的`// CommonJS/Node.js`是原模板自带的,可能是为Node.js编写的WEB服务器提供功能,我们用不到,这里就不深究了。\r\n## 本地服务器令牌生成\r\n在刚刚那个插件JS中,我们首先向本地服务器索取上传令牌,这个令牌是一串hash字符串,内容包括了从申请令牌到上传图片成功或者失败之间的上传时限,上传的空间,以及更多的上传策略。构造这一串令牌的代码在本地WEB服务器中实现,以JAVA为例:\r\n```\r\n/**\r\n * 七牛上传令牌获取\r\n * @return\r\n * @throws JblogException \r\n */\r\n@RequestMapping(value=\"/getQiniuToken\",method=RequestMethod.POST)\r\npublic ModelAndView qiniu() throws JblogException{\r\n\tResult result = new Result();\r\n\t\r\n\tif(StringUtils.isEmpty(BlogConfig.QN_ACCESSKEY) || StringUtils.isEmpty(BlogConfig.QN_SECRETKEY)){\r\n\t\tresult.setCode(Result.FAIL);\r\n\t\tresult.setMessage(\"数据库中没有正确的七牛服务器密钥,无法生成令牌\");\r\n\t\treturn new ModelAndView(\"ajaxResult\", \"result\", result);\r\n\t}\r\n\t\r\n Auth auth = Auth.create(BlogConfig.QN_ACCESSKEY, BlogConfig.QN_SECRETKEY);\r\n\tString token = auth.uploadToken(\r\n\t\t\"blog-images\",\t//空间名称\r\n\t\tnull,\t\t\t//key,最终资源名称,一般为空,让服务器自己生成\r\n\t\t3600,\t\t\t//3600秒,上传时限\r\n\t\tnew StringMap()\t//其他上传策略\r\n\t\t\t.put(\"saveKey\", UUID.randomUUID().toString() + \"$(ext)\")\r\n\t);\r\n\t\r\n\tresult.setData(\"\\\"\" + token + \"\\\"\");\r\n\t\r\n return new ModelAndView(\"ajaxResult\", \"result\", result);\r\n}\r\n```\r\n其中的Auth是七牛官网提供的jar包中的,不需要直接下载,可以使用maven或者gradle获取,七牛已经将这个jar提交到了maven国际公共库了,介绍文档在这里:http://developer.qiniu.com/code/v7/sdk/java.html\r\n我用的gradle,直接添加:\r\n```\r\ncompile \'com.qiniu:qiniu-java-sdk:7.x.+\'\r\n```\r\n到此,我们整个的editor.md七牛插件就编写好了。如下方所示:\r\n![](http://ocsy1jhmk.bkt.clouddn.com/ecd82c08-0a1a-4365-83bf-a998ea87c36e.png)\r\n#回顾一下开发步骤\r\n1. 在editor.md的初始化函数中添加插件的描述。\r\n2. 在editor.md的plugins文件中添加相关的插件文件夹和插件JS,js代码可以根据插件模板书写。\r\n3. 到七牛官网注册,并在个人中心添加资源空间,收集三个字符串数据:资源空间的url前缀,AK公钥,SK私钥。\r\n4. 编写本地服务器端代码,使用AK和SK构造token上传令牌,构造时使用的类来自七牛JAVA文档。\r\n\r\n其中最复杂的是第二步,我所公布的插件JS篇幅比较长,需要耐心观察。其中的知识点包括jQuery;动态生成form;ajax提交等,这些都是互联网应用开发的基础知识点。','2016-09-13 10:48:13',0,NULL,'qiniu-upload-plugin-for-editormd','2016-09-18 22:20:32',2,1,264,86),(87,'使用并改造editor.md在JAVA-WEB项目中实现Markdown编辑器','<h1 id=\"h1-markdown-editor-md-\"><a name=\"Markdown和Editor.md简介\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Markdown和Editor.md简介</h1><p>Markdwon编辑器在技术工作者圈子中已经越来越流行,简单的语法,统一的格式,强大的扩展功能,最重要的是:你可以用Markdown,设计一篇精彩绝伦的文档而完全不需要将你的右手从键盘上移到鼠标上去,这是我和很多编程工作者最热爱的。长期使用Leanote的原因,也是基于有着强大的WEB端和客户端的Markdown编辑器(个人甚至偏向于客户端Leanote)。<br><img src=\"https://leanote.com/api/file/getImage?fileId=57abec87ab644135ea046800\" alt=\"\"><br><a href=\"http://pandao.github.io/editor.md/\">Editor.md</a> 是国人开发的开源在线Markdown编辑器,单纯基于前端JavaScript,无需后台代码加持,适用于任何语言(仅在上传图片功能时需要一点后台代码与之配合,其余都交给Editor.md吧),因为是中国人开发的,对中文支持得相当到位。在我的个人博客设计过程中,相当长一段时间都是使用的百度的ueditor作为文档编辑器,说实话,配置那玩意儿相当繁琐,而要想把ueditor配置得符合自己心意更是烦上加烦。在经历了一次SpringMVC与ueditor上传组件冲突的bug后,有种彻底想放弃ueditor的冲动。<br>长期使用Leanote的Markdown编辑器的我,苦苦寻求一种能取代ueditor的Markdown编辑器,<a href=\"http://pandao.github.io/editor.md/\">Editor.md</a> 正是我想要的。</p>\r\n<h1 id=\"h1-editor-md-\"><a name=\"Editor.md的安装使用\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Editor.md的安装使用</h1><h2 id=\"h2-1-\"><a name=\"1.基本使用及表单提交\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1.基本使用及表单提交</h2><p>基本使用markdown是相当简单的,比ueditor还要简单,从git上下载回来的Editor.md是1.5版,压缩包里有分门别类详细的文件夹。<br><img src=\"https://leanote.com/api/file/getImage?fileId=57abec87ab644135ea046802\" alt=\"\"><br>在examples文件夹中有一个简单的示例simple.html,可以在浏览器里打开,并查看源代码,我这里做一个简单的总结,并加上表单提交的配置:</p>\r\n<ul>\r\n<li>在HTML中加载CSS:editormd.css</li><li>在HTML中加载JS:顺序为jQuery,editormd.min.js,</li><li>在HTML中写一个div节点,包含两个textarea,格式如下:<pre><code>&lt;div class=&quot;editormd&quot; id=&quot;test-editormd&quot;&gt;\r\n &lt;textarea class=&quot;editormd-markdown-textarea&quot; name=&quot;test-editormd-markdown-doc&quot;&gt;&lt;/textarea&gt;\r\n &lt;!-- 第二个隐藏文本域,用来构造生成的HTML代码,方便表单POST提交,这里的name可以任意取,后台接受时以这个name键为准 --&gt;\r\n &lt;textarea class=&quot;editormd-html-textarea&quot; name=&quot;text&quot;&gt;&lt;/textarea&gt;\r\n&lt;/div&gt;\r\n</code></pre></li><li><p>在HTML中写一句javascript:</p>\r\n<pre><code>&lt;script type=&quot;text/javascript&quot;&gt;\r\n $(function() {\r\n editormd(&quot;test-editormd&quot;, {\r\n width : &quot;90%&quot;,\r\n height : 640,\r\n syncScrolling : &quot;single&quot;,\r\n //你的lib目录的路径,我这边用JSP做测试的\r\n path : &quot;&lt;%=request.getContextPath()%&gt;/resources/editormd/lib/&quot;,\r\n //这个配置在simple.html中并没有,但是为了能够提交表单,使用这个配置可以让构造出来的HTML代码直接在第二个隐藏的textarea域中,方便post提交表单。\r\n saveHTMLToTextarea : true\r\n });\r\n });\r\n&lt;/script&gt;\r\n</code></pre><p>OK,这样就完成了一个最简单的editor.md的编辑器了,你可以在这里面书写你熟悉的Markdown文档,里面可以包含代码,右侧有实时的预览。如图所示:<br><img src=\"https://leanote.com/api/file/getImage?fileId=57abec87ab644135ea046804\" alt=\"\"></p>\r\n<h2 id=\"h2-2-\"><a name=\"2.图片上传\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2.图片上传</h2><p>有了基本的Markdown功能,集成editor.md就完成了一半了,下面开始处理图片上传。<br>图片上传的语法是<code>![alt](url)</code>,这个用来嵌入互联网上现成的图片是很方便的,但是如果想要上传本地图片就要后台代码配合了,我下面以JAVA为例(官方文档有PHP的示例),配合SpringMVC和commons-fileupload-1.3.1.jar,简单给个DEMO:<br>根据Editor.md的官方文档介绍,上传图片功能需要添加一点配置,如下:</p>\r\n<pre><code>editormd(&quot;test-editormd&quot;, {\r\n width : &quot;90%&quot;,\r\n height : 640,\r\n syncScrolling : &quot;single&quot;,\r\n path : &quot;&lt;%=request.getContextPath()%&gt;/resources/editormd/lib/&quot;,\r\n imageUpload : true,\r\n imageFormats : [&quot;jpg&quot;, &quot;jpeg&quot;, &quot;gif&quot;, &quot;png&quot;, &quot;bmp&quot;, &quot;webp&quot;],\r\n imageUploadURL : &quot;/uploadfile&quot;,\r\n saveHTMLToTextarea : true,\r\n});\r\n //editor.md期望得到一个json格式的上传后的返回值,格式是这样的:\r\n /*\r\n {\r\n success : 0 | 1, // 0 表示上传失败,1 表示上传成功\r\n message : &quot;提示的信息,上传成功或上传失败及错误信息等。&quot;,\r\n url : &quot;图片地址&quot; // 上传成功时才返回\r\n }\r\n */\r\n</code></pre><p>以上代码并不难理解,也就加了三行配置,关键的是<code>imageUploadURL : &quot;/uploadfile&quot;</code>这个配置,这里的URL指向了你处理图片上传的action,与之对应的,我的SpringMVC控制器是这样的,这里贴出了整个代码,防止有小伙伴对JAVA以及SpringMVC处理文件上传还不太熟练:</p>\r\n<pre><code class=\"lang-java\">package com.newflypig.jblog.controller;\r\nimport java.io.File;\r\nimport java.io.IOException;\r\nimport javax.servlet.http.HttpServletRequest;\r\nimport javax.servlet.http.HttpServletResponse;\r\nimport org.apache.commons.io.FileUtils;\r\nimport org.springframework.stereotype.Controller;\r\nimport org.springframework.web.bind.annotation.RequestMapping;\r\nimport org.springframework.web.bind.annotation.RequestMethod;\r\nimport org.springframework.web.bind.annotation.RequestParam;\r\nimport org.springframework.web.multipart.MultipartFile;\r\n<a href=\"https://github.com/Controller\" title=\"&#64;Controller\" class=\"at-link\">@Controller</a>\r\npublic class UploadController {\r\n\r\n <a href=\"https://github.com/RequestMapping\" title=\"&#64;RequestMapping\" class=\"at-link\">@RequestMapping</a>(value=&quot;/uploadfile&quot;,method=RequestMethod.POST)\r\n public void hello(HttpServletRequest request,HttpServletResponse response,<a href=\"https://github.com/RequestParam\" title=\"&#64;RequestParam\" class=\"at-link\">@RequestParam</a>(value = &quot;editormd-image-file&quot;, required = false) MultipartFile attach){\r\n try {\r\n request.setCharacterEncoding( &quot;utf-8&quot; );\r\n response.setHeader( &quot;Content-Type&quot; , &quot;text/html&quot; );\r\n String rootPath = request.getSession().getServletContext().getRealPath(&quot;/resources/upload/&quot;);\r\n\r\n /**\r\n * 文件路径不存在则需要创建文件路径\r\n */\r\n File filePath=new File(rootPath);\r\n if(!filePath.exists()){\r\n filePath.mkdirs();\r\n }\r\n\r\n //最终文件名\r\n File realFile=new File(rootPath+File.separator+attach.getOriginalFilename());\r\n FileUtils.copyInputStreamToFile(attach.getInputStream(), realFile);\r\n\r\n //下面response返回的json格式是editor.md所限制的,规范输出就OK\r\n response.getWriter().write( &quot;{\\&quot;success\\&quot;: 1, \\&quot;message\\&quot;:\\&quot;上传成功\\&quot;,\\&quot;url\\&quot;:\\&quot;/resources/upload/&quot; + attach.getOriginalFilename() + &quot;\\&quot;}&quot; );\r\n } catch (Exception e) {\r\n try {\r\n response.getWriter().write( &quot;{\\&quot;success\\&quot;:0}&quot; );\r\n } catch (IOException e1) {\r\n e1.printStackTrace();\r\n }\r\n }\r\n }\r\n}\r\n</code></pre>\r\n<p>这样就完成了图片上传了,上传后,后台action返回了一个url给editor.md,editor.md使用这个url作为你嵌套在文档中的图片url。<br>这样就大功告成了,是不是很爽,要比ueditor的上传配置简单100倍。</p>\r\n<h2 id=\"h2-3-editor-md-\"><a name=\"3.Editor.md代码黑色主题\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.Editor.md代码黑色主题</h2><p>用惯了sublime text等编辑器,是不是对代码的渲染有点要求呢,先上博主两个IDE的截图吧,一个是sublime text3,一个是myeclipse2015:<br><img src=\"https://leanote.com/api/file/getImage?fileId=57abf682ab644133ed04754d\" alt=\"\"><br><img src=\"https://leanote.com/api/file/getImage?fileId=57abf682ab644133ed04754c\" alt=\"\"><br>习惯了黑色背景的代码样式,就希望editor.md也能实现代码黑色样式,果然,editor.md从1.5版本以后为大家提供了dark样式主题,但是会让除代码以外的其他编辑区域也变黑色,所以根据个人需要来小小的改造一下:</p>\r\n</li><li><p>首先添加样式配置,在原来的editor.md配置基础上,添加配置项:</p>\r\n<pre><code>$(function() {\r\n editormd(&quot;test-editormd&quot;, {\r\n width : &quot;90%&quot;,\r\n height : 640,\r\n syncScrolling : &quot;single&quot;,\r\n path : &quot;&lt;%=request.getContextPath()%&gt;/resources/editormd/lib/&quot;,\r\n imageUpload : true,\r\n imageFormats : [&quot;jpg&quot;, &quot;jpeg&quot;, &quot;gif&quot;, &quot;png&quot;, &quot;bmp&quot;, &quot;webp&quot;],\r\n imageUploadURL : &quot;/uploadfile&quot;,\r\n saveHTMLToTextarea : true,\r\n //下面这一行将使用dark主题\r\n previewTheme : &quot;dark&quot;\r\n });\r\n});\r\n</code></pre><p>配置好dark主题以后,编辑区还是原来的编辑区,预览区已经使用了暗黑模式,但是代码以外的部分也都变成黑色背景了,这不是我想要的,所以我对editormd.css做了一些修正,将dark主题代码以外的部分取消了样式定义,这样预览起来只有代码块是暗黑模式,截图如下:<br><img src=\"https://leanote.com/api/file/getImage?fileId=57ac14c3ab644133ed0476bc\" alt=\"\"><br>修改了editormd.css后,别忘了使用CSS压缩工具再压缩一遍,生成editormd.min.css,这样正式部署时能减轻一点服务器压力,提高加载效率。我把压缩好重新生成的editormd.min.css放出来,有需要的可以直接下载。<br><a href=\"https://leanote.com/api/file/getAttach?fileId=57ac14c3ab644133ed0476bb\">editormd.min.css</a></p>\r\n<h2 id=\"h2-4-\"><a name=\"4. 文档的显示\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. 文档的显示</h2><p>编辑区的代码格式已经调整成为我们喜欢的样式了,在表单POST提交时,editormd将我们的markdown语法文档翻译成了HTML语言,并将html字符串提交给了我们的后台,后台应该将这些HTML字符串持久化到数据库中,在文章显示时将他们显示在页面上。<br>具体的做法是:</p>\r\n<pre><code class=\"lang-html\">&lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;%=request.getContextPath()%&gt;/resources/editormd/css/editormd.preview.min.css&quot; /&gt;\r\n&lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;%=request.getContextPath()%&gt;/resources/editormd/css/editormd.css&quot; /&gt;\r\n&lt;!-- 因为我们使用了dark主题,所以在容器div上加上dark的主题类,实现我们自定义的代码样式 --&gt;\r\n&lt;div class=&quot;content editormd-preview-theme-dark&quot; id=&quot;content&quot;&gt;${article.text }&lt;/div&gt;\r\n&lt;script src=&quot;&lt;%=request.getContextPath()%&gt;/resources/js/jquery.min.js&quot;&gt;&lt;/script&gt;\r\n&lt;script src=&quot;&lt;%=request.getContextPath()%&gt;/resources/editormd/lib/marked.min.js&quot;&gt;&lt;/script&gt;\r\n&lt;script src=&quot;&lt;%=request.getContextPath()%&gt;/resources/editormd/lib/prettify.min.js&quot;&gt;&lt;/script&gt;\r\n&lt;script src=&quot;&lt;%=request.getContextPath()%&gt;/resources/editormd/editormd.min.js&quot;&gt;&lt;/script&gt;\r\n&lt;script type=&quot;text/javascript&quot;&gt;\r\n editormd.markdownToHTML(&quot;content&quot;);\r\n&lt;/script&gt;\r\n</code></pre>\r\n<p>至此,我们所有的工作都完成了。(另外还有些editor.md高级功能,比如[TOC]标签自动生成文档目录结构、流程图语法等,我还没研究,不过现在已经满足我的所有要求了,感兴趣的朋友可以继续阅读examples文件夹中各种示例。)<br>如果您各项基础知识掌握得都还可以的话,将editor.md这个编辑器引入你的项目是相当轻松加愉快的。写这篇blog也确实因为对这个编辑器的喜爱,加上官方尚未有一个系统的cookbook,都是一个个小demo,希望能帮到想使用editor.md的朋友。</p>\r\n</li></ul>\r\n','#Markdown和Editor.md简介\r\nMarkdwon编辑器在技术工作者圈子中已经越来越流行,简单的语法,统一的格式,强大的扩展功能,最重要的是:你可以用Markdown,设计一篇精彩绝伦的文档而完全不需要将你的右手从键盘上移到鼠标上去,这是我和很多编程工作者最热爱的。长期使用Leanote的原因,也是基于有着强大的WEB端和客户端的Markdown编辑器(个人甚至偏向于客户端Leanote)。\r\n![](https://leanote.com/api/file/getImage?fileId=57abec87ab644135ea046800)\r\n[Editor.md](http://pandao.github.io/editor.md/) 是国人开发的开源在线Markdown编辑器,单纯基于前端JavaScript,无需后台代码加持,适用于任何语言(仅在上传图片功能时需要一点后台代码与之配合,其余都交给Editor.md吧),因为是中国人开发的,对中文支持得相当到位。在我的个人博客设计过程中,相当长一段时间都是使用的百度的ueditor作为文档编辑器,说实话,配置那玩意儿相当繁琐,而要想把ueditor配置得符合自己心意更是烦上加烦。在经历了一次SpringMVC与ueditor上传组件冲突的bug后,有种彻底想放弃ueditor的冲动。\r\n长期使用Leanote的Markdown编辑器的我,苦苦寻求一种能取代ueditor的Markdown编辑器,[Editor.md](http://pandao.github.io/editor.md/) 正是我想要的。\r\n#Editor.md的安装使用\r\n##1.基本使用及表单提交\r\n基本使用markdown是相当简单的,比ueditor还要简单,从git上下载回来的Editor.md是1.5版,压缩包里有分门别类详细的文件夹。\r\n![](https://leanote.com/api/file/getImage?fileId=57abec87ab644135ea046802)\r\n在examples文件夹中有一个简单的示例simple.html,可以在浏览器里打开,并查看源代码,我这里做一个简单的总结,并加上表单提交的配置:\r\n\r\n- 在HTML中加载CSS:editormd.css\r\n- 在HTML中加载JS:顺序为jQuery,editormd.min.js,\r\n- 在HTML中写一个div节点,包含两个textarea,格式如下:\r\n```\r\n<div class=\"editormd\" id=\"test-editormd\">\r\n\t<textarea class=\"editormd-markdown-textarea\" name=\"test-editormd-markdown-doc\"></textarea>\r\n <!-- 第二个隐藏文本域,用来构造生成的HTML代码,方便表单POST提交,这里的name可以任意取,后台接受时以这个name键为准 -->\r\n\t<textarea class=\"editormd-html-textarea\" name=\"text\"></textarea>\r\n</div>\r\n```\r\n- 在HTML中写一句javascript:\r\n```\r\n<script type=\"text/javascript\">\r\n $(function() {\r\n editormd(\"test-editormd\", {\r\n width : \"90%\",\r\n height : 640,\r\n syncScrolling : \"single\",\r\n //你的lib目录的路径,我这边用JSP做测试的\r\n path : \"<%=request.getContextPath()%>/resources/editormd/lib/\",\r\n //这个配置在simple.html中并没有,但是为了能够提交表单,使用这个配置可以让构造出来的HTML代码直接在第二个隐藏的textarea域中,方便post提交表单。\r\n saveHTMLToTextarea : true\r\n });\r\n });\r\n</script>\r\n```\r\nOK,这样就完成了一个最简单的editor.md的编辑器了,你可以在这里面书写你熟悉的Markdown文档,里面可以包含代码,右侧有实时的预览。如图所示:\r\n![](https://leanote.com/api/file/getImage?fileId=57abec87ab644135ea046804)\r\n##2.图片上传\r\n有了基本的Markdown功能,集成editor.md就完成了一半了,下面开始处理图片上传。\r\n图片上传的语法是``![alt](url)``,这个用来嵌入互联网上现成的图片是很方便的,但是如果想要上传本地图片就要后台代码配合了,我下面以JAVA为例(官方文档有PHP的示例),配合SpringMVC和commons-fileupload-1.3.1.jar,简单给个DEMO:\r\n根据Editor.md的官方文档介绍,上传图片功能需要添加一点配置,如下:\r\n```\r\neditormd(\"test-editormd\", {\r\n width : \"90%\",\r\n height : 640,\r\n syncScrolling : \"single\",\r\n path : \"<%=request.getContextPath()%>/resources/editormd/lib/\",\r\n imageUpload : true,\r\n imageFormats : [\"jpg\", \"jpeg\", \"gif\", \"png\", \"bmp\", \"webp\"],\r\n imageUploadURL : \"/uploadfile\",\r\n saveHTMLToTextarea : true,\r\n});\r\n //editor.md期望得到一个json格式的上传后的返回值,格式是这样的:\r\n /*\r\n {\r\n success : 0 | 1, // 0 表示上传失败,1 表示上传成功\r\n message : \"提示的信息,上传成功或上传失败及错误信息等。\",\r\n url : \"图片地址\" // 上传成功时才返回\r\n }\r\n */\r\n```\r\n以上代码并不难理解,也就加了三行配置,关键的是``imageUploadURL : \"/uploadfile\"``这个配置,这里的URL指向了你处理图片上传的action,与之对应的,我的SpringMVC控制器是这样的,这里贴出了整个代码,防止有小伙伴对JAVA以及SpringMVC处理文件上传还不太熟练:\r\n```java\r\npackage com.newflypig.jblog.controller;\r\nimport java.io.File;\r\nimport java.io.IOException;\r\nimport javax.servlet.http.HttpServletRequest;\r\nimport javax.servlet.http.HttpServletResponse;\r\nimport org.apache.commons.io.FileUtils;\r\nimport org.springframework.stereotype.Controller;\r\nimport org.springframework.web.bind.annotation.RequestMapping;\r\nimport org.springframework.web.bind.annotation.RequestMethod;\r\nimport org.springframework.web.bind.annotation.RequestParam;\r\nimport org.springframework.web.multipart.MultipartFile;\r\n@Controller\r\npublic class UploadController {\r\n\t\r\n\t@RequestMapping(value=\"/uploadfile\",method=RequestMethod.POST)\r\n\tpublic void hello(HttpServletRequest request,HttpServletResponse response,@RequestParam(value = \"editormd-image-file\", required = false) MultipartFile attach){\r\n\t\ttry {\r\n\t\t\trequest.setCharacterEncoding( \"utf-8\" );\r\n\t\t\tresponse.setHeader( \"Content-Type\" , \"text/html\" );\r\n\t\t\tString rootPath = request.getSession().getServletContext().getRealPath(\"/resources/upload/\");\r\n\t\t\t\r\n\t\t\t/**\r\n\t\t\t * 文件路径不存在则需要创建文件路径\r\n\t\t\t */\r\n\t\t\tFile filePath=new File(rootPath);\r\n\t\t\tif(!filePath.exists()){\r\n\t\t\t\tfilePath.mkdirs();\r\n\t\t\t}\r\n\t\t\t\t\t\t\r\n\t //最终文件名\r\n\t File realFile=new File(rootPath+File.separator+attach.getOriginalFilename());\r\n\t FileUtils.copyInputStreamToFile(attach.getInputStream(), realFile);\r\n\t\t\t\r\n\t\t\t//下面response返回的json格式是editor.md所限制的,规范输出就OK\r\n\t\t\tresponse.getWriter().write( \"{\\\"success\\\": 1, \\\"message\\\":\\\"上传成功\\\",\\\"url\\\":\\\"/resources/upload/\" + attach.getOriginalFilename() + \"\\\"}\" );\r\n\t\t} catch (Exception e) {\r\n\t\t\ttry {\r\n\t\t\t\tresponse.getWriter().write( \"{\\\"success\\\":0}\" );\r\n\t\t\t} catch (IOException e1) {\r\n\t\t\t\te1.printStackTrace();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n```\r\n这样就完成了图片上传了,上传后,后台action返回了一个url给editor.md,editor.md使用这个url作为你嵌套在文档中的图片url。\r\n这样就大功告成了,是不是很爽,要比ueditor的上传配置简单100倍。\r\n##3.Editor.md代码黑色主题\r\n用惯了sublime text等编辑器,是不是对代码的渲染有点要求呢,先上博主两个IDE的截图吧,一个是sublime text3,一个是myeclipse2015:\r\n![](https://leanote.com/api/file/getImage?fileId=57abf682ab644133ed04754d)\r\n![](https://leanote.com/api/file/getImage?fileId=57abf682ab644133ed04754c)\r\n习惯了黑色背景的代码样式,就希望editor.md也能实现代码黑色样式,果然,editor.md从1.5版本以后为大家提供了dark样式主题,但是会让除代码以外的其他编辑区域也变黑色,所以根据个人需要来小小的改造一下:\r\n\r\n- 首先添加样式配置,在原来的editor.md配置基础上,添加配置项:\r\n```\r\n$(function() {\r\n editormd(\"test-editormd\", {\r\n width : \"90%\",\r\n\theight : 640,\r\n\tsyncScrolling : \"single\",\r\n\tpath : \"<%=request.getContextPath()%>/resources/editormd/lib/\",\r\n\timageUpload : true,\r\n imageFormats : [\"jpg\", \"jpeg\", \"gif\", \"png\", \"bmp\", \"webp\"],\r\n imageUploadURL : \"/uploadfile\",\r\n saveHTMLToTextarea : true,\r\n //下面这一行将使用dark主题\r\n previewTheme : \"dark\"\r\n });\r\n});\r\n```\r\n配置好dark主题以后,编辑区还是原来的编辑区,预览区已经使用了暗黑模式,但是代码以外的部分也都变成黑色背景了,这不是我想要的,所以我对editormd.css做了一些修正,将dark主题代码以外的部分取消了样式定义,这样预览起来只有代码块是暗黑模式,截图如下:\r\n![](https://leanote.com/api/file/getImage?fileId=57ac14c3ab644133ed0476bc)\r\n修改了editormd.css后,别忘了使用CSS压缩工具再压缩一遍,生成editormd.min.css,这样正式部署时能减轻一点服务器压力,提高加载效率。我把压缩好重新生成的editormd.min.css放出来,有需要的可以直接下载。\r\n[editormd.min.css](https://leanote.com/api/file/getAttach?fileId=57ac14c3ab644133ed0476bb)\r\n##4. 文档的显示\r\n编辑区的代码格式已经调整成为我们喜欢的样式了,在表单POST提交时,editormd将我们的markdown语法文档翻译成了HTML语言,并将html字符串提交给了我们的后台,后台应该将这些HTML字符串持久化到数据库中,在文章显示时将他们显示在页面上。\r\n具体的做法是:\r\n```html\r\n<link rel=\"stylesheet\" href=\"<%=request.getContextPath()%>/resources/editormd/css/editormd.preview.min.css\" />\r\n<link rel=\"stylesheet\" href=\"<%=request.getContextPath()%>/resources/editormd/css/editormd.css\" />\r\n<!-- 因为我们使用了dark主题,所以在容器div上加上dark的主题类,实现我们自定义的代码样式 -->\r\n<div class=\"content editormd-preview-theme-dark\" id=\"content\">${article.text }</div>\r\n<script src=\"<%=request.getContextPath()%>/resources/js/jquery.min.js\"></script>\r\n<script src=\"<%=request.getContextPath()%>/resources/editormd/lib/marked.min.js\"></script>\r\n<script src=\"<%=request.getContextPath()%>/resources/editormd/lib/prettify.min.js\"></script>\r\n<script src=\"<%=request.getContextPath()%>/resources/editormd/editormd.min.js\"></script>\r\n<script type=\"text/javascript\">\r\n\teditormd.markdownToHTML(\"content\");\r\n</script>\r\n```\r\n至此,我们所有的工作都完成了。(另外还有些editor.md高级功能,比如[TOC]标签自动生成文档目录结构、流程图语法等,我还没研究,不过现在已经满足我的所有要求了,感兴趣的朋友可以继续阅读examples文件夹中各种示例。)\r\n如果您各项基础知识掌握得都还可以的话,将editor.md这个编辑器引入你的项目是相当轻松加愉快的。写这篇blog也确实因为对这个编辑器的喜爱,加上官方尚未有一个系统的cookbook,都是一个个小demo,希望能帮到想使用editor.md的朋友。','2016-09-13 11:34:49',0,NULL,'editormd','2016-09-18 22:20:38',2,1,192,87),(89,'基于Spring的JAVA开源项目如何保护隐私数据','<p>在 Git 以及 <a href=\"http://git.oschina.net/\">GitOS</a> 上参与各类开源JAVA项目的过程中,<br>尤其是自建的开源项目,如何既能有效地保护自己的隐私数据,又能不破坏项目的完整性呢。</p>\r\n<p>在 <a href=\"http://git.oschina.net/newflydd/jblog\">JBlog</a> 项目开源的过程中,我遇到一个有点头疼的问题:<br>如何在公开源代码的过程中,保护私有数据不公开,同时又不要破坏整个项目的完整性和连贯性<br>开源项目,Git 提交,所有的源代码被第三方一览无余。<br>基于Spring的JAVA项目,数据库URL地址,端口号,用户名,密码等信息一般会被作者明文保存在<code>.properties</code>文件中。<br>而如果我们的数据库不是localhost,而是直接配置在公网上,这些信息的暴露一般是毁灭性的。<br>无论检出你项目源代码的程序员节操有多么高尚,当看到一个暴露在公网上数据库,端口,用户名,密码完全暴露在自己眼前时,都会忍不住搞破坏的,相信我。<br>而 JBlog 开发的初期,我还曾经在 <a href=\"http://www.douyu.com\">斗鱼网</a> 实况直播写代码一段时间,期间关于 IDE 、各种小工具的使用,写到每个模块时的吐槽和注意点什么的,跟网友互动很happy,但当我要写到<code>.properties</code>文件时,却不知怎么下手,屏幕暴露在互联网上,我键入的任何字符都公布于众,此时如何保护公网数据库信息呢?(因为家里使用的动态IP地址,所以没法在数据库中对接入的客户端IP地址做限制)</p>\r\n<p>于是我将<code>.properties</code>文件写成这样:</p>\r\n<pre><code>#application configs\r\n#database password encrypt file path\r\n#it is important but the file&#39;s text can be any string you like\r\njblog.token.filepath = c:/jblog.token\r\n\r\n#jdbc druid config\r\njdbc.url = ENC(oo+7uqEVC/W3FQPJTUdbEFCPjmsYoSbkk5n/uX8LyD7naZiiqBWnaIT1na85TOApl/xJczx9EyWri0GazU0pB3V9Vf0FSh1IIKbPJMfFw3w=)\r\n#jblog_newflypig\r\njdbc.username = newflypig\r\njdbc.password = ENC(xGGGsn8Ni/gDZqumX/5ilg==)\r\njdbc.driverClassName = com.mysql.jdbc.Driver\r\n</code></pre><p>代码中凡是用<code>ENC()</code>括起来的都属于隐私数据,需要解密才能得到明文<br>那么hibernate是怎么得到明文的呢?<br>我重新定义了阅读<code>properties</code>的类<code>PropertyPlaceholderConfigurer</code>:</p>\r\n<pre><code>public class EncryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {\r\n\r\n @Override\r\n protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException {\r\n System.out.println(&quot;=================EncryptPropertyPlaceholderConfigurer setting successful!==================&quot;); \r\n /**\r\n * 遍历key,通过key遍历value,对value进行正则匹配\r\n * 将匹配的值删&quot;ENC(&quot;,再删&quot;)&quot;,继续对剩下的字符串进行DES解密\r\n */\r\n String filePath = props.getProperty(&quot;jblog.token.filepath&quot;);\r\n String secretKey = this.getSecretKey(filePath); \r\n\r\n if(secretKey == null){\r\n System.out.println(&quot;无法读取数据库加密文件,文件地址应该位于:&quot; + filePath);\r\n return;\r\n }\r\n\r\n Set&lt;Object&gt; keys=props.keySet();\r\n for(Object o:keys){\r\n String key = (String)o;\r\n String value = props.getProperty(key);\r\n\r\n if (value != null &amp;&amp; value.matches(&quot;ENC\\\\([\\\\w+=/]+\\\\)&quot;)) { \r\n props.setProperty(key, DESUtil.decryptString(secretKey, value.substring(4,value.length()-1))); \r\n }\r\n }\r\n\r\n super.processProperties(beanFactoryToProcess, props);\r\n }\r\n\r\n private String getSecretKey(String filePath){\r\n StringBuffer keyBuffer = new StringBuffer();\r\n try{\r\n InputStream is = new FileInputStream(filePath);\r\n BufferedReader reader = new BufferedReader(new InputStreamReader(is));\r\n String line = reader.readLine();\r\n while(line != null){\r\n keyBuffer.append(line);\r\n line = reader.readLine();\r\n }\r\n reader.close();\r\n is.close();\r\n }catch(Exception e){\r\n return null;\r\n }\r\n\r\n return keyBuffer.toString();\r\n }\r\n}\r\n</code></pre><p>在这个类中,首先要去读一个文本文件,这个文本文件中装配了解密KEY,文件地址保存在上述<code>.properties</code>文件的第4行,也就是<code>c:/jblog.token</code>,取到了解密KEY后,在阅读<code>.properties</code>文件时如果遇到<code>ENC()</code>包围的字符串,则用其对加密后的密文进行DES解密操作,再将明文返回给spring供其注入。<br>对JAVA中的DES加解密不熟悉的朋友可以去研究一下,并不需要深入理解其中的算法,只需要知道DES是一种可逆加密算法,加密解密需时需要同一个KEY。<br>我将我自己的<code>DESUtil.java</code>类贴出来,可供参照:</p>\r\n<pre><code>public class DESUtil {\r\n\r\n private static byte[] crypt(byte[] data, byte[] key, int mode) throws Exception {\r\n // 生成一个可信任的随机数源\r\n SecureRandom sr = new SecureRandom();\r\n // 从原始密钥数据创建DESKeySpec对象\r\n DESKeySpec dks = new DESKeySpec(key);\r\n // 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象\r\n SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(&quot;DES&quot;);\r\n SecretKey securekey = keyFactory.generateSecret(dks);\r\n // Cipher对象实际完成解密操作\r\n Cipher cipher = Cipher.getInstance(&quot;DES&quot;);\r\n // 用密钥初始化Cipher对象\r\n cipher.init(mode, securekey, sr);\r\n return cipher.doFinal(data);\r\n }\r\n\r\n /**\r\n * DES解密\r\n * @param 密文\r\n * @return 明文\r\n */\r\n public static String decryptString(String key, String data){\r\n if (data == null || &quot;&quot;.equals(data))\r\n return &quot;&quot;;\r\n String str=null;\r\n try {\r\n str=new String(crypt(new BASE64Decoder().decodeBuffer(data), key.getBytes(),Cipher.DECRYPT_MODE));\r\n } catch (Exception e) {\r\n e.printStackTrace();\r\n }\r\n return str;\r\n }\r\n\r\n /**\r\n * 加密\r\n * @param 明文\r\n * @return 密文\r\n */\r\n public static String encryptString(String key, String data){\r\n if(data==null || &quot;&quot;.equals(data))\r\n return &quot;&quot;;\r\n String str=null;\r\n try {\r\n str=new BASE64Encoder().encode(crypt(data.getBytes(), key.getBytes(), Cipher.ENCRYPT_MODE));\r\n } catch (Exception e) {\r\n e.printStackTrace();\r\n }\r\n return str;\r\n }\r\n}\r\n</code></pre><p>最后,我们需要将Spring默认的阅读<code>.properties</code>的类替换成我们自己的类<br>在Spring的配置文件中,重新定义<code>propertyConfigurer</code>bean 即可,然后像什么都没发生一样,配置正常的<code>datasource</code>以供Hibernate使用:</p>\r\n<pre><code>&lt;bean id=&quot;propertyConfigurer&quot; class=&quot;com.newflypig.jblog.util.EncryptPropertyPlaceholderConfigurer&quot;&gt;\r\n &lt;property name=&quot;locations&quot;&gt;\r\n &lt;list&gt;\r\n &lt;value&gt;classpath:config.properties&lt;/value&gt;\r\n &lt;/list&gt;\r\n &lt;/property&gt;\r\n&lt;/bean&gt;\r\n&lt;!-- 配置数据源 --&gt;\r\n&lt;bean id=&quot;dataSource&quot; class=&quot;org.springframework.jdbc.datasource.DriverManagerDataSource&quot;&gt;\r\n &lt;property name=&quot;driverClassName&quot; value=&quot;${jdbc.driverClassName}&quot; &gt;&lt;/property&gt;\r\n &lt;property name=&quot;url&quot; value=&quot;${jdbc.url}&quot; &gt;&lt;/property&gt;\r\n &lt;property name=&quot;username&quot; value=&quot;${jdbc.username}&quot; &gt;&lt;/property&gt;\r\n &lt;property name=&quot;password&quot; value=&quot;${jdbc.password}&quot; &gt;&lt;/property&gt;\r\n&lt;/bean&gt;\r\n</code></pre><p>这样,我们的整个流程就清晰了:</p>\r\n<ol>\r\n<li>首先Spring安排我们自定义的的<code>propertyConfigurer</code>去读<code>.properties</code>文件。</li><li>它在读<code>properties</code>时会对每一个key-value做判断,如果这个value被<code>ENC()</code>包围了,那么就对其中的字符串进行解密,再返回给Spring,否则直接返回给Spring。</li><li>解密时,它会去读一个定义好的磁盘文件,这个文件中存放这着我们的解密密钥,用这个密钥去对密文解密。</li></ol>\r\n<p>如果下载我们源代码的朋友磁盘上没有这个密钥文件,那么他就无法使用我们自用的数据库,对于信任的朋友,我们可以将密钥文件放到云盘上以供下载。普通用户下载回来时,我们建议他们将带有<code>ENC()</code>的键值对修改成自己的数据库连接信息,他们可以不使用<code>ENC()</code>包围,直接在本地明文配置。</p>\r\n','在 Git 以及 [GitOS](http://git.oschina.net/) 上参与各类开源JAVA项目的过程中,\r\n尤其是自建的开源项目,如何既能有效地保护自己的隐私数据,又能不破坏项目的完整性呢。\r\n\r\n在 [JBlog](http://git.oschina.net/newflydd/jblog) 项目开源的过程中,我遇到一个有点头疼的问题:\r\n如何在公开源代码的过程中,保护私有数据不公开,同时又不要破坏整个项目的完整性和连贯性\r\n开源项目,Git 提交,所有的源代码被第三方一览无余。\r\n基于Spring的JAVA项目,数据库URL地址,端口号,用户名,密码等信息一般会被作者明文保存在`.properties`文件中。\r\n而如果我们的数据库不是localhost,而是直接配置在公网上,这些信息的暴露一般是毁灭性的。\r\n无论检出你项目源代码的程序员节操有多么高尚,当看到一个暴露在公网上数据库,端口,用户名,密码完全暴露在自己眼前时,都会忍不住搞破坏的,相信我。\r\n而 JBlog 开发的初期,我还曾经在 [斗鱼网](http://www.douyu.com) 实况直播写代码一段时间,期间关于 IDE 、各种小工具的使用,写到每个模块时的吐槽和注意点什么的,跟网友互动很happy,但当我要写到`.properties`文件时,却不知怎么下手,屏幕暴露在互联网上,我键入的任何字符都公布于众,此时如何保护公网数据库信息呢?(因为家里使用的动态IP地址,所以没法在数据库中对接入的客户端IP地址做限制)\r\n\r\n于是我将`.properties`文件写成这样:\r\n```\r\n#application configs\r\n#database password encrypt file path\r\n#it is important but the file\'s text can be any string you like\r\njblog.token.filepath = c:/jblog.token\r\n\r\n#jdbc druid config\r\njdbc.url = ENC(oo+7uqEVC/W3FQPJTUdbEFCPjmsYoSbkk5n/uX8LyD7naZiiqBWnaIT1na85TOApl/xJczx9EyWri0GazU0pB3V9Vf0FSh1IIKbPJMfFw3w=)\r\n#jblog_newflypig\r\njdbc.username = newflypig\r\njdbc.password = ENC(xGGGsn8Ni/gDZqumX/5ilg==)\r\njdbc.driverClassName = com.mysql.jdbc.Driver\r\n```\r\n代码中凡是用`ENC()`括起来的都属于隐私数据,需要解密才能得到明文\r\n那么hibernate是怎么得到明文的呢?\r\n我重新定义了阅读`properties`的类`PropertyPlaceholderConfigurer`:\r\n```\r\npublic class EncryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {\r\n\t\r\n\t@Override\r\n\tprotected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException {\r\n\t\tSystem.out.println(\"=================EncryptPropertyPlaceholderConfigurer setting successful!==================\");\t\r\n /**\r\n * 遍历key,通过key遍历value,对value进行正则匹配\r\n * 将匹配的值删\"ENC(\",再删\")\",继续对剩下的字符串进行DES解密\r\n */\r\n\t\tString filePath = props.getProperty(\"jblog.token.filepath\");\r\n\t\tString secretKey = this.getSecretKey(filePath); \r\n\t\t\r\n\t\tif(secretKey == null){\r\n\t\t\tSystem.out.println(\"无法读取数据库加密文件,文件地址应该位于:\" + filePath);\r\n\t\t\treturn;\r\n\t\t}\r\n\t\t\r\n Set<Object> keys=props.keySet();\r\n for(Object o:keys){\r\n \tString key = (String)o;\r\n \tString value = props.getProperty(key);\r\n \t\r\n if (value != null && value.matches(\"ENC\\\\([\\\\w+=/]+\\\\)\")) { \t\r\n props.setProperty(key, DESUtil.decryptString(secretKey, value.substring(4,value.length()-1))); \r\n }\r\n }\r\n \r\n\t\tsuper.processProperties(beanFactoryToProcess, props);\r\n\t}\r\n\t\r\n\tprivate String getSecretKey(String filePath){\r\n\t\tStringBuffer keyBuffer = new StringBuffer();\r\n\t\ttry{\r\n\t\t\tInputStream is = new FileInputStream(filePath);\r\n\t\t\tBufferedReader reader = new BufferedReader(new InputStreamReader(is));\r\n\t\t\tString line = reader.readLine();\r\n\t\t\twhile(line != null){\r\n\t\t\t\tkeyBuffer.append(line);\r\n\t\t\t\tline = reader.readLine();\r\n\t\t\t}\r\n\t\t\treader.close();\r\n\t\t\tis.close();\r\n\t\t}catch(Exception e){\r\n\t\t\treturn null;\r\n\t\t}\r\n\t\t\r\n\t\treturn keyBuffer.toString();\r\n\t}\r\n}\r\n```\r\n在这个类中,首先要去读一个文本文件,这个文本文件中装配了解密KEY,文件地址保存在上述`.properties`文件的第4行,也就是`c:/jblog.token`,取到了解密KEY后,在阅读`.properties`文件时如果遇到`ENC()`包围的字符串,则用其对加密后的密文进行DES解密操作,再将明文返回给spring供其注入。\r\n对JAVA中的DES加解密不熟悉的朋友可以去研究一下,并不需要深入理解其中的算法,只需要知道DES是一种可逆加密算法,加密解密需时需要同一个KEY。\r\n我将我自己的`DESUtil.java`类贴出来,可供参照:\r\n```\r\npublic class DESUtil {\r\n\t\r\n\tprivate static byte[] crypt(byte[] data, byte[] key, int mode) throws Exception {\r\n\t\t// 生成一个可信任的随机数源\r\n\t\tSecureRandom sr = new SecureRandom();\r\n\t\t// 从原始密钥数据创建DESKeySpec对象\r\n\t\tDESKeySpec dks = new DESKeySpec(key);\r\n\t\t// 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象\r\n\t\tSecretKeyFactory keyFactory = SecretKeyFactory.getInstance(\"DES\");\r\n\t\tSecretKey securekey = keyFactory.generateSecret(dks);\r\n\t\t// Cipher对象实际完成解密操作\r\n\t\tCipher cipher = Cipher.getInstance(\"DES\");\r\n\t\t// 用密钥初始化Cipher对象\r\n\t\tcipher.init(mode, securekey, sr);\r\n\t\treturn cipher.doFinal(data);\r\n\t}\r\n\t\r\n\t/**\r\n\t * DES解密\r\n\t * @param 密文\r\n\t * @return 明文\r\n\t */\r\n\tpublic static String decryptString(String key, String data){\r\n\t\tif (data == null || \"\".equals(data))\r\n return \"\";\r\n\t\tString str=null;\r\n\t\ttry {\r\n\t\t\tstr=new String(crypt(new BASE64Decoder().decodeBuffer(data), key.getBytes(),Cipher.DECRYPT_MODE));\r\n\t\t} catch (Exception e) {\r\n\t\t\te.printStackTrace();\r\n\t\t}\r\n\t\treturn str;\r\n\t}\r\n\t\r\n\t/**\r\n\t * 加密\r\n\t * @param 明文\r\n\t * @return 密文\r\n\t */\r\n\tpublic static String encryptString(String key, String data){\r\n if(data==null || \"\".equals(data))\r\n \treturn \"\";\r\n\t\tString str=null;\r\n try {\r\n\t\t\tstr=new BASE64Encoder().encode(crypt(data.getBytes(), key.getBytes(), Cipher.ENCRYPT_MODE));\r\n\t\t} catch (Exception e) {\r\n\t\t\te.printStackTrace();\r\n\t\t}\r\n return str;\r\n\t}\r\n}\r\n```\r\n最后,我们需要将Spring默认的阅读`.properties`的类替换成我们自己的类\r\n在Spring的配置文件中,重新定义`propertyConfigurer`bean 即可,然后像什么都没发生一样,配置正常的`datasource`以供Hibernate使用:\r\n```\r\n<bean id=\"propertyConfigurer\" class=\"com.newflypig.jblog.util.EncryptPropertyPlaceholderConfigurer\">\r\n\t<property name=\"locations\">\r\n\t\t<list>\r\n\t\t\t<value>classpath:config.properties</value>\r\n\t\t</list>\r\n\t</property>\r\n</bean>\r\n<!-- 配置数据源 -->\r\n<bean id=\"dataSource\" class=\"org.springframework.jdbc.datasource.DriverManagerDataSource\">\r\n\t<property name=\"driverClassName\" value=\"${jdbc.driverClassName}\" ></property>\r\n\t<property name=\"url\" value=\"${jdbc.url}\" ></property>\r\n\t<property name=\"username\" value=\"${jdbc.username}\" ></property>\r\n\t<property name=\"password\" value=\"${jdbc.password}\" ></property>\r\n</bean>\r\n```\r\n这样,我们的整个流程就清晰了:\r\n1. 首先Spring安排我们自定义的的`propertyConfigurer`去读`.properties`文件。\r\n2. 它在读`properties`时会对每一个key-value做判断,如果这个value被`ENC()`包围了,那么就对其中的字符串进行解密,再返回给Spring,否则直接返回给Spring。\r\n3. 解密时,它会去读一个定义好的磁盘文件,这个文件中存放这着我们的解密密钥,用这个密钥去对密文解密。\r\n\r\n如果下载我们源代码的朋友磁盘上没有这个密钥文件,那么他就无法使用我们自用的数据库,对于信任的朋友,我们可以将密钥文件放到云盘上以供下载。普通用户下载回来时,我们建议他们将带有`ENC()`的键值对修改成自己的数据库连接信息,他们可以不使用`ENC()`包围,直接在本地明文配置。','2016-09-17 16:55:31',0,NULL,'encrypt-properties','2016-09-17 20:36:10',2,1,211,89);
INSERT INTO `jblog_article` VALUES (94,'为网站添加RSS订阅功能','<h1 id=\"h1-rss-\"><a name=\"RSS标准\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>RSS标准</h1><p>RSS|Atom 都是一种以XML形式描述的聚合内容文本,使得阅读者在不打开我方HTML的情况下,即能初步阅读我方的投递内容,阅读者可以使用大量的阅读工具通过一个简单的xml文件URL,订阅我方的内容,而在XML索引发生增加时,这些阅读工具大多能及时提醒阅读者我方有新内容发布。使用RSS已逐渐成为各大Blog系统的标配,诸如博客园、CSDN、WordPress等都内置了RSS生成地址。RSS可以实时主动通知订阅者,提高网站流量和用户粘性。</p>\r\n<h1 id=\"h1--blog-rss-\"><a name=\"为独立BLOG系统添加RSS功能\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>为独立BLOG系统添加RSS功能</h1><p>我们独立设计的BLOG如何添加RSS功能,下面以JBlog项目为例,一步一步带大家来设计。</p>\r\n<ul>\r\n<li>阅读RSS规范文件</li><li>设计Freemarker的RSS模板</li><li>制定生成策略,编码生成rss.xml文件</li><li>验证生成的rss.xml并尝试订阅</li></ul>\r\n<h3 id=\"h3--rss-\"><a name=\"阅读RSS规范文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>阅读RSS规范文件</h3><p>推荐阅读 <a href=\"http://static.userland.com/gems/backend/rssTwoExample2.xml\">RSS2.0标准样例</a></p>\r\n<h3 id=\"h3--freemarker-rss-\"><a name=\"设计Freemarker的RSS模板\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>设计Freemarker的RSS模板</h3><p>我编写的rss模板如下:</p>\r\n<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;\r\n&lt;rss version=&quot;2.0&quot; \r\n xmlns:content=&quot;http://purl.org/rss/1.0/modules/content/&quot;\r\n xmlns:wfw=&quot;http://wellformedweb.org/CommentAPI/&quot;\r\n xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot;\r\n xmlns:atom=&quot;http://www.w3.org/2005/Atom&quot;\r\n xmlns:sy=&quot;http://purl.org/rss/1.0/modules/syndication/&quot;\r\n xmlns:slash=&quot;http://purl.org/rss/1.0/modules/slash/&quot; &gt;\r\n &lt;channel&gt;\r\n &lt;title&gt;${blogCommon.blogTitle}&lt;/title&gt;\r\n &lt;link&gt;http://${blogCommon.blogUrl}&lt;/link&gt;\r\n &lt;atom:link href=&quot;http://${blogCommon.blogUrl}/rss.xml&quot; rel=&quot;self&quot; type=&quot;application/rss+xml&quot; &gt;&lt;/atom:link&gt;\r\n &lt;description&gt;${blogCommon.blogDescription}&lt;/description&gt;\r\n &lt;language&gt;zh-cn&lt;/language&gt;\r\n &lt;copyright&gt;Copyright 2015-2016 Ding Ding&lt;/copyright&gt;\r\n &lt;lastBuildDate&gt;${today?datetime}&lt;/lastBuildDate&gt;\r\n &lt;generator&gt;www.hexcode.cn&lt;/generator&gt;\r\n &lt;managingEditor&gt;${blogCommon.blogEmail} (Ding Ding)&lt;/managingEditor&gt;\r\n &lt;webMaster&gt;${blogCommon.blogEmail} (Ding Ding)&lt;/webMaster&gt;\r\n &lt;ttl&gt;5&lt;/ttl&gt;\r\n &lt;image&gt;\r\n &lt;url&gt;http://${blogCommon.blogUrl}/resources/favicon.png&lt;/url&gt;\r\n &lt;title&gt;${blogCommon.blogTitle}&lt;/title&gt;\r\n &lt;link&gt;http://${blogCommon.blogUrl}&lt;/link&gt;\r\n &lt;/image&gt;\r\n &lt;#list articleList as article&gt;\r\n &lt;item&gt;\r\n &lt;title&gt;&lt;![CDATA[${article.title}]]&gt;&lt;/title&gt;\r\n &lt;link&gt;http://${blogCommon.blogUrl}/article/show/&lt;#if article.urlName ??&gt;${article.urlName}&lt;#else&gt;${article.articleId}&lt;/#if&gt;&lt;/link&gt;\r\n &lt;author&gt;${blogCommon.blogEmail} (Ding Ding)&lt;/author&gt;\r\n &lt;guid&gt;http://${blogCommon.blogUrl}/article/show/&lt;#if article.urlName ??&gt;${article.urlName}&lt;#else&gt;${article.articleId}&lt;/#if&gt;&lt;/guid&gt;\r\n &lt;#list article.tags as tag&gt;\r\n &lt;category&gt;${tag.title}&lt;/category&gt;\r\n &lt;/#list&gt;\r\n &lt;pubDate&gt;${article.createDate}&lt;/pubDate&gt;\r\n &lt;description&gt;\r\n &lt;![CDATA[\r\n ${article.html}\r\n ]]&gt;\r\n &lt;/description&gt;\r\n &lt;/item&gt;\r\n &lt;/#list&gt;\r\n &lt;/channel&gt;\r\n&lt;/rss&gt;\r\n</code></pre><h3 id=\"h3--rss-xml-\"><a name=\"制定生成策略,编码生成rss.xml文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>制定生成策略,编码生成rss.xml文件</h3><p>系统将拦截URL为<code>/admin/article/**</code>或者<code>/article/admin/**</code>通配的路径,在所有的POST请求之后,抛出一个线程,重构根目录下的rss.xml文件,这样的生成策略保证了rss.xml对系统的性能影响最小,不用每次响应用户的RSS请求时,对数据库进行大面积扫描,又保证了每次管理员增、删、改文章时(这些操作一般使用POST方式提交),会生成一个最新的RSS文件,同时,使用后台抛出线程的方式,避免了用户在前台执行这些增删改操作后,WEB不能及时响应,影响用户体验的问题。<br>具体实现方式是使用的SpringMVC的拦截器,SpringMVC配置文件:</p>\r\n<pre><code>&lt;!-- 配置mvc的拦截器 可以配置多个 --&gt;\r\n&lt;mvc:interceptor&gt;\r\n &lt;!-- 需要被拦截的路径 --&gt;\r\n &lt;mvc:mapping path=&quot;/admin/article/**&quot;&gt;&lt;/mvc:mapping&gt;\r\n &lt;mvc:mapping path=&quot;/article/admin/**&quot;&gt;&lt;/mvc:mapping&gt;\r\n &lt;!-- 拦截类 --&gt;\r\n &lt;bean class=&quot;com.newflypig.jblog.controller.interceptor.RSSInterceptor&quot;&gt;&lt;/bean&gt;\r\n &lt;/mvc:interceptor&gt;\r\n&lt;/mvc:interceptors&gt;\r\n</code></pre><p>JAVA拦截器类:</p>\r\n<pre><code>public class RSSInterceptor implements HandlerInterceptor{\r\n\r\n @Autowired\r\n private IArticleService articleService;\r\n\r\n @Autowired\r\n private BlogCommon blogCommon;\r\n\r\n @Override\r\n public boolean preHandle(HttpServletRequest request,\r\n HttpServletResponse response, Object handler) throws Exception {\r\n // TODO Auto-generated method stub\r\n return true; //这里一定要返回true啊,要不然全都拦截掉了,默认生成时是false,你懂的,我查错了很久,坑爹的。\r\n }\r\n\r\n @Override\r\n public void postHandle(HttpServletRequest request,\r\n HttpServletResponse response, Object handler,\r\n ModelAndView modelAndView) throws Exception {\r\n\r\n String templateFilePath = request.getServletContext().getRealPath(&quot;/WEB-INF/views/ftl&quot;);\r\n String outputFilePath = request.getServletContext().getRealPath(&quot;/rss.xml&quot;);\r\n //如果对文章有POST操作,则更新一次RSS\r\n if(&quot;POST&quot;.equalsIgnoreCase(request.getMethod())){\r\n\r\n //抛个线程来处理RSS的重构,别影响浏览器返回response给用户\r\n new Thread(() -&gt; {\r\n List&lt;Article&gt; articleList = this.articleService.findAllForRSS();\r\n\r\n RSSUtil rssUtil = new RSSUtil(\r\n articleList\r\n ,blogCommon\r\n ,templateFilePath\r\n ,outputFilePath);\r\n try {\r\n rssUtil.generateRSS();\r\n } catch (Exception e) {\r\n e.printStackTrace();\r\n }\r\n }).start();\r\n }\r\n\r\n }\r\n\r\n @Override\r\n public void afterCompletion(HttpServletRequest request,\r\n HttpServletResponse response, Object handler, Exception ex)\r\n throws Exception {\r\n // TODO Auto-generated method stub\r\n\r\n }\r\n}\r\n</code></pre><p>RssUtil类:</p>\r\n<pre><code>public class RSSUtil {\r\n private List&lt;Article&gt; articleList;\r\n\r\n private String templateFilePath;\r\n private String outputFilePath;\r\n private BlogCommon blogCommon;\r\n\r\n public RSSUtil(List&lt;Article&gt; articleList,BlogCommon blogCommon, String templateFilePath, String outputFilePath){\r\n this.articleList = articleList;\r\n this.blogCommon = blogCommon;\r\n\r\n this.templateFilePath = templateFilePath; \r\n this.outputFilePath = outputFilePath;\r\n }\r\n\r\n public void generateRSS() throws IOException, TemplateException{\r\n Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);\r\n cfg.setDirectoryForTemplateLoading(new File(templateFilePath));\r\n cfg.setDefaultEncoding(&quot;UTF-8&quot;);\r\n cfg.setClassicCompatible(true);\r\n cfg.setLocale(Locale.ENGLISH);\r\n cfg.setTimeZone(TimeZone.getTimeZone(&quot;GMT&quot;));\r\n cfg.setDateTimeFormat(&quot;EEE, d MMM yyyy hh:mm:ss z&quot;);\r\n //因为RSS标准中,对时间格式和时区都有很明确的要求,所以这里要设置正确\r\n\r\n Map&lt;String, Object&gt; root = new HashMap&lt;String, Object&gt;();\r\n root.put(&quot;articleList&quot;, articleList);\r\n root.put(&quot;blogCommon&quot;, blogCommon);\r\n root.put(&quot;today&quot;, new Date());\r\n\r\n Template temp = cfg.getTemplate(&quot;rss.xml&quot;);\r\n\r\n File outFile = new File(outputFilePath);\r\n Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile),&quot;utf-8&quot;)); \r\n\r\n temp.process(root, out);\r\n }\r\n}\r\n</code></pre><p>生成的rss.xml在根目录下,是静态文件,最后别忘了将它排除到springMVC的控制范围以外,要不然SpringMvc会将它拦截到Controller中,又影响效率了。<br>下面的定义放到SpringMVC拦截器之前,web.xml:</p>\r\n<pre><code>&lt;servlet-mapping&gt; \r\n &lt;servlet-name&gt;default&lt;/servlet-name&gt; \r\n &lt;url-pattern&gt;/rss.xml&lt;/url-pattern&gt; \r\n&lt;/servlet-mapping&gt;\r\n</code></pre><h3 id=\"h3-u9A8Cu8BC1u548Cu8BA2u9605\"><a name=\"验证和订阅\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>验证和订阅</h3><p>将项目部署到公网上,将rss.xml的URL地址提交到 <a href=\"http://www.feedvalidator.org/\">http://www.feedvalidator.org/</a> ,即可以分析生成的rss.xml是否通过验证,如果不能通过,会有详细的错误报告,定位到行,可以反复纠正,最终通过验证后,可以得到一个奖励图标:<br><img src=\"http://www.feedvalidator.org/images/valid-rss-rogers.png\" alt=\"\"><br>通过验证后的rss.xml可以被任何人订阅,最简单的订阅工具是foxmail,在foxmail左下方有个RSS图标,在本地就可以简单的订阅,另外还有 「<a href=\"http://www.yilan.io/\">一览</a>」,可以云端管理很多订阅源,最厉害的 Google Reader 已经成为传说。</p>\r\n','#RSS标准\r\nRSS|Atom 都是一种以XML形式描述的聚合内容文本,使得阅读者在不打开我方HTML的情况下,即能初步阅读我方的投递内容,阅读者可以使用大量的阅读工具通过一个简单的xml文件URL,订阅我方的内容,而在XML索引发生增加时,这些阅读工具大多能及时提醒阅读者我方有新内容发布。使用RSS已逐渐成为各大Blog系统的标配,诸如博客园、CSDN、WordPress等都内置了RSS生成地址。RSS可以实时主动通知订阅者,提高网站流量和用户粘性。\r\n#为独立BLOG系统添加RSS功能\r\n我们独立设计的BLOG如何添加RSS功能,下面以JBlog项目为例,一步一步带大家来设计。\r\n- 阅读RSS规范文件\r\n- 设计Freemarker的RSS模板\r\n- 制定生成策略,编码生成rss.xml文件\r\n- 验证生成的rss.xml并尝试订阅\r\n\r\n### 阅读RSS规范文件\r\n推荐阅读 [RSS2.0标准样例](http://static.userland.com/gems/backend/rssTwoExample2.xml)\r\n\r\n### 设计Freemarker的RSS模板\r\n我编写的rss模板如下:\r\n```\r\n<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<rss version=\"2.0\" \r\n\txmlns:content=\"http://purl.org/rss/1.0/modules/content/\"\r\n\txmlns:wfw=\"http://wellformedweb.org/CommentAPI/\"\r\n\txmlns:dc=\"http://purl.org/dc/elements/1.1/\"\r\n\txmlns:atom=\"http://www.w3.org/2005/Atom\"\r\n\txmlns:sy=\"http://purl.org/rss/1.0/modules/syndication/\"\r\n\txmlns:slash=\"http://purl.org/rss/1.0/modules/slash/\" >\r\n\t<channel>\r\n\t\t<title>${blogCommon.blogTitle}</title>\r\n\t\t<link>http://${blogCommon.blogUrl}</link>\r\n\t\t<atom:link href=\"http://${blogCommon.blogUrl}/rss.xml\" rel=\"self\" type=\"application/rss+xml\" ></atom:link>\r\n\t\t<description>${blogCommon.blogDescription}</description>\r\n\t\t<language>zh-cn</language>\r\n\t\t<copyright>Copyright 2015-2016 Ding Ding</copyright>\r\n\t\t<lastBuildDate>${today?datetime}</lastBuildDate>\r\n\t\t<generator>www.hexcode.cn</generator>\r\n\t\t<managingEditor>${blogCommon.blogEmail} (Ding Ding)</managingEditor>\r\n\t\t<webMaster>${blogCommon.blogEmail} (Ding Ding)</webMaster>\r\n\t\t<ttl>5</ttl>\r\n\t\t<image>\r\n\t\t\t<url>http://${blogCommon.blogUrl}/resources/favicon.png</url>\r\n\t\t\t<title>${blogCommon.blogTitle}</title>\r\n\t\t\t<link>http://${blogCommon.blogUrl}</link>\r\n\t\t</image>\r\n\t\t<#list articleList as article>\r\n\t\t<item>\r\n\t\t\t<title><![CDATA[${article.title}]]></title>\r\n\t\t\t<link>http://${blogCommon.blogUrl}/article/show/<#if article.urlName ??>${article.urlName}<#else>${article.articleId}</#if></link>\r\n\t\t\t<author>${blogCommon.blogEmail} (Ding Ding)</author>\r\n\t\t\t<guid>http://${blogCommon.blogUrl}/article/show/<#if article.urlName ??>${article.urlName}<#else>${article.articleId}</#if></guid>\r\n\t\t\t<#list article.tags as tag>\r\n\t\t\t<category>${tag.title}</category>\r\n\t\t\t</#list>\r\n\t\t\t<pubDate>${article.createDate}</pubDate>\r\n\t\t\t<description>\r\n\t\t\t<![CDATA[\r\n\t\t\t\t${article.html}\r\n\t\t\t]]>\r\n\t\t\t</description>\r\n\t\t</item>\r\n\t\t</#list>\r\n\t</channel>\r\n</rss>\r\n```\r\n\r\n### 制定生成策略,编码生成rss.xml文件\r\n系统将拦截URL为`/admin/article/**`或者`/article/admin/**`通配的路径,在所有的POST请求之后,抛出一个线程,重构根目录下的rss.xml文件,这样的生成策略保证了rss.xml对系统的性能影响最小,不用每次响应用户的RSS请求时,对数据库进行大面积扫描,又保证了每次管理员增、删、改文章时(这些操作一般使用POST方式提交),会生成一个最新的RSS文件,同时,使用后台抛出线程的方式,避免了用户在前台执行这些增删改操作后,WEB不能及时响应,影响用户体验的问题。\r\n具体实现方式是使用的SpringMVC的拦截器,SpringMVC配置文件:\r\n```\r\n<!-- 配置mvc的拦截器 可以配置多个 -->\r\n<mvc:interceptor>\r\n\t<!-- 需要被拦截的路径 -->\r\n\t<mvc:mapping path=\"/admin/article/**\"></mvc:mapping>\r\n\t<mvc:mapping path=\"/article/admin/**\"></mvc:mapping>\r\n\t<!-- 拦截类 -->\r\n\t<bean class=\"com.newflypig.jblog.controller.interceptor.RSSInterceptor\"></bean>\r\n\t</mvc:interceptor>\r\n</mvc:interceptors>\r\n```\r\nJAVA拦截器类:\r\n```\r\npublic class RSSInterceptor implements HandlerInterceptor{\r\n\r\n\t@Autowired\r\n\tprivate IArticleService articleService;\r\n\t\r\n\t@Autowired\r\n\tprivate BlogCommon blogCommon;\r\n\t\r\n\t@Override\r\n\tpublic boolean preHandle(HttpServletRequest request,\r\n\t\t\tHttpServletResponse response, Object handler) throws Exception {\r\n\t\t// TODO Auto-generated method stub\r\n\t\treturn true;\t//这里一定要返回true啊,要不然全都拦截掉了,默认生成时是false,你懂的,我查错了很久,坑爹的。\r\n\t}\r\n\r\n\t@Override\r\n\tpublic void postHandle(HttpServletRequest request,\r\n\t\t\tHttpServletResponse response, Object handler,\r\n\t\t\tModelAndView modelAndView) throws Exception {\r\n\t\t\r\n\t\tString templateFilePath = request.getServletContext().getRealPath(\"/WEB-INF/views/ftl\");\r\n\t\tString outputFilePath = request.getServletContext().getRealPath(\"/rss.xml\");\r\n\t\t//如果对文章有POST操作,则更新一次RSS\r\n\t\tif(\"POST\".equalsIgnoreCase(request.getMethod())){\r\n\t\t\t\r\n\t\t\t//抛个线程来处理RSS的重构,别影响浏览器返回response给用户\r\n\t\t\tnew Thread(() -> {\r\n\t\t\t\tList<Article> articleList = this.articleService.findAllForRSS();\r\n\t\t\t\t\r\n\t\t\t\tRSSUtil rssUtil = new RSSUtil(\r\n\t\t\t\t\t\tarticleList\r\n\t\t\t\t\t\t,blogCommon\r\n\t\t\t\t\t\t,templateFilePath\r\n\t\t\t\t\t\t,outputFilePath);\r\n\t\t\t\ttry {\r\n\t\t\t\t\trssUtil.generateRSS();\r\n\t\t\t\t} catch (Exception e) {\r\n\t\t\t\t\te.printStackTrace();\r\n\t\t\t\t}\r\n\t\t\t}).start();\r\n\t\t}\r\n\t\t\r\n\t}\r\n\r\n\t@Override\r\n\tpublic void afterCompletion(HttpServletRequest request,\r\n\t\t\tHttpServletResponse response, Object handler, Exception ex)\r\n\t\t\tthrows Exception {\r\n\t\t// TODO Auto-generated method stub\r\n\t\t\r\n\t}\r\n}\r\n```\r\nRssUtil类:\r\n```\r\npublic class RSSUtil {\r\n\tprivate List<Article> articleList;\r\n\t\r\n\tprivate String templateFilePath;\r\n\tprivate String outputFilePath;\r\n\tprivate BlogCommon blogCommon;\r\n\t\r\n\tpublic RSSUtil(List<Article> articleList,BlogCommon blogCommon, String templateFilePath, String outputFilePath){\r\n\t\tthis.articleList = articleList;\r\n\t\tthis.blogCommon = blogCommon;\r\n\t\t\r\n\t\tthis.templateFilePath = templateFilePath; \r\n\t\tthis.outputFilePath = outputFilePath;\r\n\t}\r\n\t\r\n\tpublic void generateRSS() throws IOException, TemplateException{\r\n\t\tConfiguration cfg = new Configuration(Configuration.VERSION_2_3_23);\r\n\t\tcfg.setDirectoryForTemplateLoading(new File(templateFilePath));\r\n\t\tcfg.setDefaultEncoding(\"UTF-8\");\r\n\t\tcfg.setClassicCompatible(true);\r\n\t\tcfg.setLocale(Locale.ENGLISH);\r\n\t\tcfg.setTimeZone(TimeZone.getTimeZone(\"GMT\"));\r\n\t\tcfg.setDateTimeFormat(\"EEE, d MMM yyyy hh:mm:ss z\");\r\n\t\t//因为RSS标准中,对时间格式和时区都有很明确的要求,所以这里要设置正确\r\n\t\t\r\n\t\tMap<String, Object> root = new HashMap<String, Object>();\r\n\t\troot.put(\"articleList\", articleList);\r\n\t\troot.put(\"blogCommon\", blogCommon);\r\n\t\troot.put(\"today\", new Date());\r\n\t\t\r\n\t\tTemplate temp = cfg.getTemplate(\"rss.xml\");\r\n\t\t\r\n\t\tFile outFile = new File(outputFilePath);\r\n Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile),\"utf-8\")); \r\n\t\t\r\n\t\ttemp.process(root, out);\r\n\t}\r\n}\r\n```\r\n生成的rss.xml在根目录下,是静态文件,最后别忘了将它排除到springMVC的控制范围以外,要不然SpringMvc会将它拦截到Controller中,又影响效率了。\r\n下面的定义放到SpringMVC拦截器之前,web.xml:\r\n```\r\n<servlet-mapping> \r\n\t<servlet-name>default</servlet-name> \r\n\t<url-pattern>/rss.xml</url-pattern> \r\n</servlet-mapping>\r\n```\r\n\r\n### 验证和订阅\r\n将项目部署到公网上,将rss.xml的URL地址提交到 http://www.feedvalidator.org/ ,即可以分析生成的rss.xml是否通过验证,如果不能通过,会有详细的错误报告,定位到行,可以反复纠正,最终通过验证后,可以得到一个奖励图标:\r\n![](http://www.feedvalidator.org/images/valid-rss-rogers.png)\r\n通过验证后的rss.xml可以被任何人订阅,最简单的订阅工具是foxmail,在foxmail左下方有个RSS图标,在本地就可以简单的订阅,另外还有 「[一览](http://www.yilan.io/)」,可以云端管理很多订阅源,最厉害的 Google Reader 已经成为传说。','2016-09-19 18:13:03',0,NULL,'generate-rss-with-java','2016-09-19 19:33:29',2,1,96,94),(95,'页面中「返回顶部」图标按钮的实现','<p>「返回顶部」现在广泛用于各大网站,尤其是页面高度比较高,干货内容比较多的网站,简直风靡一时<br>如何将「返回顶部」做得美观得体这是个问题,下面跟我一起来设计网站的「返回顶部」图标按钮。</p>\r\n<p>首先定义一个<code>a</code>标签:</p>\r\n<pre><code>&lt;a href=&quot;#&quot; title=&quot;回到顶部&quot; style=&quot;display:none&quot; class=&quot;am-icon-btn am-icon-arrow-up&quot; id=&quot;goTop&quot;&gt;&lt;/a&gt;\r\n</code></pre><p>使用<code>display:none</code>先将其隐藏,这里应用到了AmazeUI的class名,但是我并不想将AmazeUI的css全部引入,毕竟这个页面除了返回顶部,不需要AmazeUI的其他功能。<br>那就自己来写这几个class的css吧:</p>\r\n<pre><code>#goTop{\r\n position:fixed;\r\n bottom:20px;\r\n right:20px;\r\n text-decoration:none;\r\n}\r\n</code></pre><p>这个很容易理解,固定到屏幕右下方。</p>\r\n<pre><code>.am-icon-btn{\r\n box-sizing:border-box;\r\n display:inline-block;\r\n width:48px;height:48px;\r\n font-size: 24px;\r\n line-height:48px;\r\n border-radius:50%;\r\n background-color:#CCC;\r\n color:#555555;\r\n text-align:center\r\n}\r\n</code></pre><p>这个类主要定义的边框,一个灰色背景的圆形按钮</p>\r\n<pre><code>.am-icon-arrow-up:before{\r\n content:&quot;\\f062&quot;;\r\n font-family: &quot;FontAwesome&quot;\r\n}\r\n</code></pre><p>这个是定义图标字体的,并且使用了伪类<code>before</code>,表示将会在<code>a</code>标签内部的开始位置添加一个字符<code>\\f062</code>,这个字符来自于<code>FontAwesome</code>字体,对此不太了解的可以参考:<a href=\"http://fontawesome.io/icons/\">http://fontawesome.io/icons/</a><br>因为并未使用AmazeUI的整个库,那么<code>FontAwesome</code>就要自己来定义了,借此机会熟悉一下CSS中引入新字体的方法:</p>\r\n<pre><code>@font-face {font-family:&#39;FontAwesome&#39;;src:url(&#39;../amazeui/fonts/fontawesome-webfont.eot?v=4.6.3&#39;);src:url(&#39;../amazeui/fonts/fontawesome-webfont.eot?#iefix&amp;v=4.6.3&#39;) format(&#39;embedded-opentype&#39;), url(&#39;../amazeui/fonts/fontawesome-webfont.woff2?v=4.6.3&#39;) format(&#39;woff2&#39;), url(&#39;../amazeui/fonts/fontawesome-webfont.woff?v=4.6.3&#39;) format(&#39;woff&#39;), url(&#39;../amazeui/fonts/fontawesome-webfont.ttf?v=4.6.3&#39;) format(&#39;truetype&#39;);font-weight:normal;font-style:normal}\r\n</code></pre><p>这些<code>eot</code>、<code>woff</code>、<code>ttf</code>文件是适配不同浏览器差异的,这些图标字体来自于AmazeUI,或者在<a href=\"http://fontawesome.io/icons/\">http://fontawesome.io/icons/</a> 下载,体积在100KB左右,浏览器将在能成功寻找到的第一个字体库后结束寻找,一般将这个<a href=\"https://github.com/font\" title=\"&#64;font\" class=\"at-link\">@font</a>-face放到CSS文件的顶部,免得应用图标时找不到字体。<br>到此为止就设计出「返回顶部」的样式了,下面要写JS来实现其功能:</p>\r\n<pre><code>$(&#39;#goTop&#39;).click(function(){$(&#39;html,body&#39;).animate({scrollTop: &#39;0px&#39;}, 800);return false;});\r\nwindow.onscroll = function () {\r\n if (document.documentElement.scrollTop + document.body.scrollTop &gt; 100) {\r\n document.getElementById(&quot;goTop&quot;).style.display = &quot;block&quot;;\r\n }\r\n else {\r\n document.getElementById(&quot;goTop&quot;).style.display = &quot;none&quot;;\r\n }\r\n}\r\n</code></pre><p>代码相当简单,第一行使用jQuery的语法,在800毫秒内将<code>html</code>标签和<code>body</code>标签移动到顶部0像素,也就是最顶部。<br>第二段是监听页面的滚轮事件的,当页面滚轮距离顶部100像素以内时,隐藏我们的<code>a</code>标签,否则显示<code>a</code>标签。</p>\r\n<p>效果如图所示:<br><img src=\"http://ocsy1jhmk.bkt.clouddn.com/1b4b4438-7f3d-49ab-8b91-8378c97aba0e.gif\" alt=\"\"></p>\r\n','「返回顶部」现在广泛用于各大网站,尤其是页面高度比较高,干货内容比较多的网站,简直风靡一时\r\n如何将「返回顶部」做得美观得体这是个问题,下面跟我一起来设计网站的「返回顶部」图标按钮。\r\n\r\n首先定义一个`a`标签:\r\n```\r\n<a href=\"#\" title=\"回到顶部\" style=\"display:none\" class=\"am-icon-btn am-icon-arrow-up\" id=\"goTop\"></a>\r\n```\r\n使用`display:none`先将其隐藏,这里应用到了AmazeUI的class名,但是我并不想将AmazeUI的css全部引入,毕竟这个页面除了返回顶部,不需要AmazeUI的其他功能。\r\n那就自己来写这几个class的css吧:\r\n```\r\n#goTop{\r\n\tposition:fixed;\r\n\tbottom:20px;\r\n\tright:20px;\r\n\ttext-decoration:none;\r\n}\r\n```\r\n这个很容易理解,固定到屏幕右下方。\r\n```\r\n.am-icon-btn{\r\n\tbox-sizing:border-box;\r\n\tdisplay:inline-block;\r\n\twidth:48px;height:48px;\r\n\tfont-size: 24px;\r\n\tline-height:48px;\r\n\tborder-radius:50%;\r\n\tbackground-color:#CCC;\r\n\tcolor:#555555;\r\n\ttext-align:center\r\n}\r\n```\r\n这个类主要定义的边框,一个灰色背景的圆形按钮\r\n```\r\n.am-icon-arrow-up:before{\r\n\tcontent:\"\\f062\";\r\n\tfont-family: \"FontAwesome\"\r\n}\r\n```\r\n这个是定义图标字体的,并且使用了伪类`before`,表示将会在`a`标签内部的开始位置添加一个字符`\\f062`,这个字符来自于`FontAwesome`字体,对此不太了解的可以参考:http://fontawesome.io/icons/\r\n因为并未使用AmazeUI的整个库,那么`FontAwesome`就要自己来定义了,借此机会熟悉一下CSS中引入新字体的方法:\r\n```\r\n@font-face {font-family:\'FontAwesome\';src:url(\'../amazeui/fonts/fontawesome-webfont.eot?v=4.6.3\');src:url(\'../amazeui/fonts/fontawesome-webfont.eot?#iefix&v=4.6.3\') format(\'embedded-opentype\'), url(\'../amazeui/fonts/fontawesome-webfont.woff2?v=4.6.3\') format(\'woff2\'), url(\'../amazeui/fonts/fontawesome-webfont.woff?v=4.6.3\') format(\'woff\'), url(\'../amazeui/fonts/fontawesome-webfont.ttf?v=4.6.3\') format(\'truetype\');font-weight:normal;font-style:normal}\r\n```\r\n这些`eot`、`woff`、`ttf`文件是适配不同浏览器差异的,这些图标字体来自于AmazeUI,或者在http://fontawesome.io/icons/ 下载,体积在100KB左右,浏览器将在能成功寻找到的第一个字体库后结束寻找,一般将这个@font-face放到CSS文件的顶部,免得应用图标时找不到字体。\r\n到此为止就设计出「返回顶部」的样式了,下面要写JS来实现其功能:\r\n```\r\n$(\'#goTop\').click(function(){$(\'html,body\').animate({scrollTop: \'0px\'}, 800);return false;});\r\nwindow.onscroll = function () {\r\n if (document.documentElement.scrollTop + document.body.scrollTop > 100) {\r\n document.getElementById(\"goTop\").style.display = \"block\";\r\n }\r\n else {\r\n document.getElementById(\"goTop\").style.display = \"none\";\r\n }\r\n}\r\n```\r\n代码相当简单,第一行使用jQuery的语法,在800毫秒内将`html`标签和`body`标签移动到顶部0像素,也就是最顶部。\r\n第二段是监听页面的滚轮事件的,当页面滚轮距离顶部100像素以内时,隐藏我们的`a`标签,否则显示`a`标签。\r\n\r\n效果如图所示:\r\n![](http://ocsy1jhmk.bkt.clouddn.com/1b4b4438-7f3d-49ab-8b91-8378c97aba0e.gif)','2016-09-19 22:08:20',0,NULL,'back-to-top','2016-09-19 22:08:20',2,1,174,95),(125,'gitosc','<script src=\'http://git.oschina.net/newflydd/jblog/widget_preview\'></script>\r\n\r\n<style>\r\n.pro_name a{color: #4183c4;}\r\n.osc_git_title{background-color: #d8e5f1;}\r\n.osc_git_box{background-color: #fafafa;}\r\n.osc_git_box{border-color: #ddd;}\r\n.osc_git_info{color: #666;}\r\n.osc_git_main a{color: #4183c4;}\r\n</style>','<script src=\'http://git.oschina.net/newflydd/jblog/widget_preview\'></script>\r\n\r\n<style>\r\n.pro_name a{color: #4183c4;}\r\n.osc_git_title{background-color: #d8e5f1;}\r\n.osc_git_box{background-color: #fafafa;}\r\n.osc_git_box{border-color: #ddd;}\r\n.osc_git_info{color: #666;}\r\n.osc_git_main a{color: #4183c4;}\r\n</style>','2016-09-24 16:33:59',11,NULL,NULL,'2016-09-24 16:37:50',NULL,1,0,133),(136,'validate','<p><a style=\"display:inline-block;margin-right:10px;\" href=\"http://www.feedvalidator.org/check.cgi?url=http%3A//www.hexcode.cn/rss.xml\"><img src=\"http://ocsy1jhmk.bkt.clouddn.com/d4c1cd5b-f0fa-464f-9065-26c994ae63c4.png\" alt=\"[Valid RSS]\" title=\"Validate my RSS feed\" /></a><a style=\"display:inline-block\" href=\"http://jigsaw.w3.org/css-validator/check/referer\"><br> <img style=\"border:0;width:88px;height:31px\" src=\"http://ocsy1jhmk.bkt.clouddn.com/83aeca1d-a329-4a98-9c1d-ad92437c0e8d.gif\" alt=\"Valid CSS!\" /><br></a></p>\r\n','<a style=\"display:inline-block;margin-right:10px;\" href=\"http://www.feedvalidator.org/check.cgi?url=http%3A//www.hexcode.cn/rss.xml\"><img src=\"http://ocsy1jhmk.bkt.clouddn.com/d4c1cd5b-f0fa-464f-9065-26c994ae63c4.png\" alt=\"[Valid RSS]\" title=\"Validate my RSS feed\" /></a><a style=\"display:inline-block\" href=\"http://jigsaw.w3.org/css-validator/check/referer\">\r\n  <img style=\"border:0;width:88px;height:31px\" src=\"http://ocsy1jhmk.bkt.clouddn.com/83aeca1d-a329-4a98-9c1d-ad92437c0e8d.gif\" alt=\"Valid CSS!\" />\r\n</a>','2016-09-27 22:11:43',11,NULL,NULL,'2016-09-29 22:24:45',NULL,1,0,136),(141,'使用Geetest极验进行人机识别','<h3 id=\"h3-u4EBAu673Au9A8Cu8BC1u7684u5386u53F2\"><a name=\"人机验证的历史\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>人机验证的历史</h3><p>「人机识别」在互联网安全领域有着重要而广泛的作用,其主要作用就是防止暴力破解,暴破密码需要高频次的试探操作,每次试探密码要控制在100毫秒级以内才有意义,因此黑客都是编写与我们HTML结构类似的脚本,加上他们自己的黑客字典来自动攻击,黑客字典来自于已破系统的数据库,比如大名鼎鼎的 <a href=\"http://www.csdn.net\">CSDN</a> ,要注意的是虽然这些字典具有相当高的匹配度,但仍需要多次尝试。<br>引入人机识别后,脚本被感应屏蔽,只有操作者是真正的人才能通过检测,因此可以大大防范暴力破解,同时降低了系统读取数据库的频次,如果做好拦截策略,甚至可以防范一定程度的DDOS攻击。<br>那么如何判定操作者是人还是机器呢,随着互联网技术的交替更迭,这一技术有着长远的发展历史。从一开始歪歪扭扭的字符和数字的验证码(这一方法目前是最不安全的,殊不知现在到处的云API提供图形识别的功能,一次识别只需要几分钱,有的甚至免费识别);到弹出一张图片,让用户快速选择是小猫还是小狗,或是其他什么;还有图片上是数学计算公式,让用户计算一下结果;有的是播放音频文件,让用户锻炼一下听力,等等。。。终极的就是发送验证码到用户手机上了,比如一些支付系统。<br>这些技术要么就是实现起来容易,被破解也容易,要么就是实现起来困难或者会产生额外的成本,破解起来也有难度。今天我想给大家介绍一下远程托管式的人机校验服务。<br>提起第三方的托管人机校验,最厉害的要数Google的 <a href=\"http://www.google.com/recaptcha\">recaptcha</a>,我试用过两天,部署起来确实方便好用,但可惜的是被墙在外面,一般用户几乎加载不了,体验很差。无奈选择了国产的 <a href=\"http://www.geetest.com/\">Geetest</a> 极验服务。</p>\r\n<h3 id=\"h3-u4F7Fu7528u6781u9A8C\"><a name=\"使用极验\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>使用极验</h3><p>官网介绍的极验的验证流程图如下:<br><img src=\"http://www.geetest.com/install/_images/failback-flow-20150703161215.png\" alt=\"\"></p>\r\n<ul>\r\n<li>用户进入登陆页面时,请求本地服务器授予 Geetest 令牌</li><li>服务器在接受这一请求时,使用Geetest提供的多语言开发包,调用授信函数(该函数的参数包括几个注册账号后提供的密钥),这一函数会向极验服务器发送状态请求,获得Geetest认可后构造一个登陆令牌给本地服务器,本地服务器再转发给浏览器。</li><li>如果本地服务器与Geetest服务器连接不畅,不能及时获得Geetest的响应,Geetest官方推荐本地采用备用验证框架,比如验证码什么的,不要影响客户登陆。如果你足够信任Geetest的服务器不宕机,或者不遭受DDOS攻击,那么这一步可以免掉。</li><li>客户端在接受到Geetest令牌后,使用JS显示极验的拖动UI,有三种模式显示:嵌入式、浮动式、弹出式。</li><li>用户在页面上使用鼠标拖动Geetest的图片拖动功能,将拼图拖动到合理区域,完成验证。根据Geetest官网介绍,Geeteset并不仅仅判断用户是否拖动到正确位置,Geetest会同时对用户的拖动轨迹,时间,以及浏览器的Cookie进行检测,利用一些列行为分析的算法最终计算出屏幕面前的是人还是脚本。</li><li><p>当用户的拖动行为得到Geetest的认可时,Geetest会返回一个认证令牌,嵌入到登陆页面的表单中,当用户提交form时,这一串令牌会被到本地服务器接受,并再次通过多语言API进行Geetest服务器的校验。如果认证成功,则放行进行用户层的密码、用户名校验,如果不成功,说明有可能是脚本在操作,需要拦截。</p>\r\n<h3 id=\"h3-u5177u4F53u6B65u9AA4\"><a name=\"具体步骤\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>具体步骤</h3><p>了解了上面整个流程后,我们来编码,首先是登陆页面向本地服务器的令牌请求:</p>\r\n<h4 id=\"h4--\"><a name=\"登陆页面主要代码:\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>登陆页面主要代码:</h4><pre><code>&lt;form action=&quot;/login&quot; id=&quot;login-form&quot; method=&quot;POST&quot; autocomplete=&quot;off&quot; onsubmit=&quot;return checkSubmit();&quot;&gt;\r\n &lt;input type=&quot;hidden&quot; name=&quot;${_csrf.parameterName}&quot; value=&quot;${_csrf.token}&quot;/&gt;\r\n &lt;input type=&quot;text&quot; name=&quot;jblog_username&quot; placeholder=&quot;username&quot;&gt;\r\n &lt;input type=&quot;password&quot; name=&quot;jblog_password&quot; placeholder=&quot;password&quot;&gt;\r\n &lt;div id=&quot;popup-captcha&quot;&gt;&lt;/div&gt;\r\n &lt;button id=&quot;btn-submit&quot;&gt;Login&lt;/button&gt;\r\n&lt;/form&gt;\r\n&lt;script src=&quot;http://static.geetest.com/static/tools/gt.js&quot;&gt;&lt;/script&gt;\r\n&lt;script type=&quot;text/javascript&quot;&gt;\r\n var captchaObj;\r\n $(function(){\r\n $.get(&quot;${rc.contextPath}/getGeetestStep1Data&quot;, function(result){\r\n if(result.data.success == 1){\r\n initGeetest({\r\n gt: result.data.gt,\r\n challenge: result.data.challenge,\r\n product: &quot;popup&quot;,\r\n }, handlerFloat);\r\n }\r\n },&quot;json&quot;);\r\n var handlerFloat = function (me) {\r\n // 将验证码加到id为popup-captcha的元素里\r\n captchaObj = me;\r\n captchaObj.bindOn(&quot;#btn-submit&quot;);\r\n captchaObj.appendTo(&quot;#popup-captcha&quot;);\r\n };\r\n });\r\n function checkSubmit(){\r\n if(captchaObj.getValidate()){\r\n return true;\r\n }else{\r\n captchaObj.show();\r\n return false;\r\n }\r\n }\r\n&lt;/script&gt;\r\n</code></pre><h4 id=\"h4-springmvc-\"><a name=\"SpringMVC中的令牌响应:\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>SpringMVC中的令牌响应:</h4><pre><code>/**\r\n* Geetest验证码输出 \r\n*/\r\n<a href=\"https://github.com/RequestMapping\" title=\"&#64;RequestMapping\" class=\"at-link\">@RequestMapping</a>(value = &quot;/getGeetestStep1Data&quot;, method = RequestMethod.GET) \r\npublic ModelAndView getGeetestStep1Data(HttpSession session) { \r\n Result result = new Result();\r\n\r\n GeetestLib gtSdk = new GeetestLib(BlogConfig.GEETEST_AK, BlogConfig.GEETEST_SK);\r\n\r\n int gtServerStatus = gtSdk.preProcess();\r\n\r\n //将服务器状态设置到session中\r\n session.setAttribute(GeetestLib.SERVER_STATUS_SESSION_KEY, gtServerStatus);//放入session,用于后面的验证\r\n\r\n String resStr = gtSdk.getResponseStr();\r\n\r\n result.setData(resStr);\r\n\r\n return new ModelAndView(&quot;ajaxResult&quot;, &quot;result&quot;, result);\r\n}\r\n</code></pre><h4 id=\"h4--spring-security-\"><a name=\"登陆按钮提交后的Spring Security接受:\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>登陆按钮提交后的Spring Security接受:</h4><pre><code>public class GeetestFilter extends UsernamePasswordAuthenticationFilter{\r\n\r\n <a href=\"https://github.com/Override\" title=\"&#64;Override\" class=\"at-link\">@Override</a>\r\n public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)\r\n throws AuthenticationException {\r\n String username = request.getParameter(&quot;jblog_username&quot;);\r\n String password = request.getParameter(&quot;jblog_password&quot;);\r\n\r\n if(BlogUtils.isEmpty(username, password)){\r\n throw new UsernameNotFoundException(&quot;用户名、密码不能为空!&quot;);\r\n }\r\n\r\n //进行Geetest极验校验\r\n GeetestLib gtSdk = new GeetestLib(BlogConfig.GEETEST_AK, BlogConfig.GEETEST_SK);\r\n\r\n String challenge = request.getParameter(&quot;geetest_challenge&quot;);\r\n String validate = request.getParameter(&quot;geetest_validate&quot;);\r\n String seccode = request.getParameter(&quot;geetest_seccode&quot;);\r\n\r\n Integer status = (Integer) request.getSession().getAttribute(GeetestLib.SERVER_STATUS_SESSION_KEY);\r\n\r\n //根据Geetest的官方说明,如果Geetest服务器宕机,需要切换到本地验证,暂时这边不设本地验证,Geetest宕机直接通过。\r\n if(status == 1){\r\n if(BlogUtils.isEmpty(challenge,validate,seccode) || gtSdk.enhencedValidateRequest(challenge, validate, seccode) == 0 ){\r\n System.out.println(&quot;未通过人机识别&quot;);\r\n request.getSession().setAttribute(&quot;exceptionMsg&quot;, &quot;未通过人机识别&quot;);\r\n throw new UsernameNotFoundException(&quot;人机识别失败,请检查您的行为!&quot;);\r\n }\r\n }\r\n\r\n return super.attemptAuthentication(request, response);\r\n }\r\n}\r\n</code></pre><p>关于最后一段使用Spring Security进行Geetest校验的部分,还有待商榷,因为Spring Security实在是太复杂了,以后我会考虑使用Shiro,或者直接用SpringMVC拦截器进行Geetest的校验。<br>最后制作一张使用Geetest进行登陆的GIF:<br><img src=\"http://ocsy1jhmk.bkt.clouddn.com/7e084b56-654b-4db0-b74b-2668fd7c055e.gif\" alt=\"\"></p>\r\n<h4 id=\"h4-u5176u4ED6\"><a name=\"其他\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>其他</h4><p>与Geetest服务类似还有一些,比如新兴的 <a href=\"https://luosimao.com/service/captcha\">「螺丝帽」</a><br>大致的流程都差不多,就看个人喜好使用了。</p>\r\n</li></ul>\r\n','###人机验证的历史\r\n「人机识别」在互联网安全领域有着重要而广泛的作用,其主要作用就是防止暴力破解,暴破密码需要高频次的试探操作,每次试探密码要控制在100毫秒级以内才有意义,因此黑客都是编写与我们HTML结构类似的脚本,加上他们自己的黑客字典来自动攻击,黑客字典来自于已破系统的数据库,比如大名鼎鼎的 [CSDN](http://www.csdn.net) ,要注意的是虽然这些字典具有相当高的匹配度,但仍需要多次尝试。\r\n引入人机识别后,脚本被感应屏蔽,只有操作者是真正的人才能通过检测,因此可以大大防范暴力破解,同时降低了系统读取数据库的频次,如果做好拦截策略,甚至可以防范一定程度的DDOS攻击。\r\n那么如何判定操作者是人还是机器呢,随着互联网技术的交替更迭,这一技术有着长远的发展历史。从一开始歪歪扭扭的字符和数字的验证码(这一方法目前是最不安全的,殊不知现在到处的云API提供图形识别的功能,一次识别只需要几分钱,有的甚至免费识别);到弹出一张图片,让用户快速选择是小猫还是小狗,或是其他什么;还有图片上是数学计算公式,让用户计算一下结果;有的是播放音频文件,让用户锻炼一下听力,等等。。。终极的就是发送验证码到用户手机上了,比如一些支付系统。\r\n这些技术要么就是实现起来容易,被破解也容易,要么就是实现起来困难或者会产生额外的成本,破解起来也有难度。今天我想给大家介绍一下远程托管式的人机校验服务。\r\n提起第三方的托管人机校验,最厉害的要数Google的 [recaptcha](http://www.google.com/recaptcha),我试用过两天,部署起来确实方便好用,但可惜的是被墙在外面,一般用户几乎加载不了,体验很差。无奈选择了国产的 [Geetest](http://www.geetest.com/) 极验服务。\r\n### 使用极验\r\n官网介绍的极验的验证流程图如下:\r\n![](http://www.geetest.com/install/_images/failback-flow-20150703161215.png)\r\n- 用户进入登陆页面时,请求本地服务器授予 Geetest 令牌\r\n- 服务器在接受这一请求时,使用Geetest提供的多语言开发包,调用授信函数(该函数的参数包括几个注册账号后提供的密钥),这一函数会向极验服务器发送状态请求,获得Geetest认可后构造一个登陆令牌给本地服务器,本地服务器再转发给浏览器。\r\n- 如果本地服务器与Geetest服务器连接不畅,不能及时获得Geetest的响应,Geetest官方推荐本地采用备用验证框架,比如验证码什么的,不要影响客户登陆。如果你足够信任Geetest的服务器不宕机,或者不遭受DDOS攻击,那么这一步可以免掉。\r\n- 客户端在接受到Geetest令牌后,使用JS显示极验的拖动UI,有三种模式显示:嵌入式、浮动式、弹出式。\r\n- 用户在页面上使用鼠标拖动Geetest的图片拖动功能,将拼图拖动到合理区域,完成验证。根据Geetest官网介绍,Geeteset并不仅仅判断用户是否拖动到正确位置,Geetest会同时对用户的拖动轨迹,时间,以及浏览器的Cookie进行检测,利用一些列行为分析的算法最终计算出屏幕面前的是人还是脚本。\r\n- 当用户的拖动行为得到Geetest的认可时,Geetest会返回一个认证令牌,嵌入到登陆页面的表单中,当用户提交form时,这一串令牌会被到本地服务器接受,并再次通过多语言API进行Geetest服务器的校验。如果认证成功,则放行进行用户层的密码、用户名校验,如果不成功,说明有可能是脚本在操作,需要拦截。\r\n### 具体步骤\r\n了解了上面整个流程后,我们来编码,首先是登陆页面向本地服务器的令牌请求:\r\n####登陆页面主要代码:\r\n```\r\n<form action=\"/login\" id=\"login-form\" method=\"POST\" autocomplete=\"off\" onsubmit=\"return checkSubmit();\">\r\n\t<input type=\"hidden\" name=\"${_csrf.parameterName}\" value=\"${_csrf.token}\"/>\r\n\t<input type=\"text\" name=\"jblog_username\" placeholder=\"username\">\r\n\t<input type=\"password\" name=\"jblog_password\" placeholder=\"password\">\r\n\t<div id=\"popup-captcha\"></div>\r\n\t<button id=\"btn-submit\">Login</button>\r\n</form>\r\n<script src=\"http://static.geetest.com/static/tools/gt.js\"></script>\r\n<script type=\"text/javascript\">\r\n\tvar captchaObj;\r\n\t$(function(){\r\n\t\t$.get(\"${rc.contextPath}/getGeetestStep1Data\", function(result){\r\n\t\t\tif(result.data.success == 1){\r\n\t\t\t\tinitGeetest({\r\n\t gt: result.data.gt,\r\n\t challenge: result.data.challenge,\r\n\t product: \"popup\",\r\n\t }, handlerFloat);\r\n\t\t\t}\r\n\t\t},\"json\");\r\n\t\tvar handlerFloat = function (me) {\r\n\t\t\t// 将验证码加到id为popup-captcha的元素里\r\n\t\t\tcaptchaObj = me;\r\n\t\t\tcaptchaObj.bindOn(\"#btn-submit\");\r\n\t\t\tcaptchaObj.appendTo(\"#popup-captcha\");\r\n\t\t};\r\n\t});\r\n\tfunction checkSubmit(){\r\n\t\tif(captchaObj.getValidate()){\r\n\t\t\treturn true;\r\n\t\t}else{\r\n\t\t\tcaptchaObj.show();\r\n\t\t\treturn false;\r\n\t\t}\r\n\t}\r\n</script>\r\n```\r\n####SpringMVC中的令牌响应:\r\n```\r\n/**\r\n * Geetest验证码输出 \r\n */\r\n@RequestMapping(value = \"/getGeetestStep1Data\", method = RequestMethod.GET) \r\npublic ModelAndView getGeetestStep1Data(HttpSession session) { \r\n Result result = new Result();\r\n \r\n GeetestLib gtSdk = new GeetestLib(BlogConfig.GEETEST_AK, BlogConfig.GEETEST_SK);\r\n \r\n int gtServerStatus = gtSdk.preProcess();\r\n \r\n //将服务器状态设置到session中\r\n session.setAttribute(GeetestLib.SERVER_STATUS_SESSION_KEY, gtServerStatus);//放入session,用于后面的验证\r\n \r\n String resStr = gtSdk.getResponseStr();\r\n \r\n result.setData(resStr);\r\n \r\n return new ModelAndView(\"ajaxResult\", \"result\", result);\r\n}\r\n```\r\n####登陆按钮提交后的Spring Security接受:\r\n```\r\npublic class GeetestFilter extends UsernamePasswordAuthenticationFilter{\r\n\t\r\n\t@Override\r\n\tpublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)\r\n\t\t\tthrows AuthenticationException {\r\n\t\tString username = request.getParameter(\"jblog_username\");\r\n\t\tString password = request.getParameter(\"jblog_password\");\r\n\t\t\r\n\t\tif(BlogUtils.isEmpty(username, password)){\r\n\t\t\tthrow new UsernameNotFoundException(\"用户名、密码不能为空!\");\r\n\t\t}\r\n\t\t\r\n\t\t//进行Geetest极验校验\r\n\t\tGeetestLib gtSdk = new GeetestLib(BlogConfig.GEETEST_AK, BlogConfig.GEETEST_SK);\r\n \r\n String challenge = request.getParameter(\"geetest_challenge\");\r\n String validate = request.getParameter(\"geetest_validate\");\r\n String seccode = request.getParameter(\"geetest_seccode\");\r\n \r\n Integer status = (Integer) request.getSession().getAttribute(GeetestLib.SERVER_STATUS_SESSION_KEY);\r\n \r\n //根据Geetest的官方说明,如果Geetest服务器宕机,需要切换到本地验证,暂时这边不设本地验证,Geetest宕机直接通过。\r\n if(status == 1){\r\n\t if(BlogUtils.isEmpty(challenge,validate,seccode) || gtSdk.enhencedValidateRequest(challenge, validate, seccode) == 0 ){\r\n\t \tSystem.out.println(\"未通过人机识别\");\r\n\t \trequest.getSession().setAttribute(\"exceptionMsg\", \"未通过人机识别\");\r\n\t \tthrow new UsernameNotFoundException(\"人机识别失败,请检查您的行为!\");\r\n\t }\r\n }\r\n \r\n\t\treturn super.attemptAuthentication(request, response);\r\n\t}\r\n}\r\n```\r\n关于最后一段使用Spring Security进行Geetest校验的部分,还有待商榷,因为Spring Security实在是太复杂了,以后我会考虑使用Shiro,或者直接用SpringMVC拦截器进行Geetest的校验。\r\n最后制作一张使用Geetest进行登陆的GIF:\r\n![](http://ocsy1jhmk.bkt.clouddn.com/7e084b56-654b-4db0-b74b-2668fd7c055e.gif)\r\n####其他\r\n与Geetest服务类似还有一些,比如新兴的 [「螺丝帽」](https://luosimao.com/service/captcha)\r\n大致的流程都差不多,就看个人喜好使用了。','2016-09-28 10:22:42',0,NULL,'geetest-introduction','2016-09-28 14:20:52',2,1,206,141),(142,'使用Nginx配合免费的七牛帐号打造自己的CDN加速服务','<h3 id=\"h3--nginx-\"><a name=\"初次领略nginx的神奇\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>初次领略nginx的神奇</h3><p>之前有听说过nginx,当时只不过认为是普通的HTTP前置机一样的服务,确实没想这么好用:反向代理、URL转发、负载均衡,统统集中在这不到2M的软件中,真是让人惊叹。</p>\r\n<h3 id=\"h3--nginx\"><a name=\"下载安装nginx\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>下载安装nginx</h3><p>最新版的 nginx 是 <a href=\"http://nginx.org/en/download.html\">nginx-1.11.4</a><br>我在本地PC安装测试的是Windows版,Linux版下载和配置是一样的。<br>下载回来后解压,可以直接鼠标点击<code>nginx.exe</code>运行。(PS:运行之前保证80端口是空闲的)<br>运行后一闪而过,根本不知道发生了什么。这时候请打开浏览器,输入<code>http://localhost</code>,如果看到nginx的欢迎页面,说明刚才双击<code>nginx.exe</code>已经产生了作用,nginx服务器已经在后台帮你默默运行着一个超轻量级的WEB服务器。<br><img src=\"http://ocsy1jhmk.bkt.clouddn.com/65b43290-000a-478f-ba64-740c1c343a28.png\" alt=\"\"></p>\r\n<h3 id=\"h3--nginx-tomcat\"><a name=\"配置nginx实现反向代理Tomcat\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>配置nginx实现反向代理Tomcat</h3><p>OK,目前我们运行的nginx还只能给浏览器响应简单的HTML、CSS、js等静态资源,下面我们通过简单几行配置,让ngnix与处理J2EE的tomcat进行连通,当客户端浏览器访问指定格式的URL时,由nginx统一接管,然后nginx主动访问本地或远程的Tomcat(本地的tomcat需运行在另一个端口上,不要与nginx冲突),得到响应后,再由nginx转发给客户端浏览器,这个流程就叫做<a href=\"http://baike.baidu.com/link?url=382kwU5bvRwNXpO-ivIWDM8tChI9MzP29A_SwBHb0p4IyFWWO4hutQ-uwSsQR2DSXWf0oSYfx1GEHV42qp0YWGbvRVATJyRVwB5NwK4UuG-sY2p_5AdmGETu2k-h50q5\">「反向代理(Reverse Proxy)」</a>。<br>具体实现方法如下:</p>\r\n<ul>\r\n<li>设置Tomcat的端口号,避开80端口,使用8080,or any others you like.设置tomcat的conf文件夹下的<code>server.xml</code>,此处不表。</li><li>对nginx的配置文件<code>nginx.conf</code>进行修改,此配置文件在nginx文件夹中的conf文件夹中:</li></ul>\r\n<pre><code>http {\r\n include mime.types;\r\n default_type application/octet-stream;\r\n\r\n sendfile on;\r\n keepalive_timeout 65;\r\n\r\n server {\r\n listen 80;\r\n server_name localhost;\r\n\r\n #转交所有URL给tomcat\r\n location / {\r\n proxy_pass http://localhost:8080;\r\n }\r\n\r\n #下面这个配置将URL指向resources文件夹的路径全部转发给了远程CDN\r\n location ^~ /resources/ {\r\n rewrite /resources/(.*) http://ocsy1jhmk.bkt.clouddn.com/resources/$1;\r\n expires 7d;\r\n }\r\n }\r\n}\r\n</code></pre><p>原版的<code>nginx.conf</code>文件里面密密麻麻好多注释,第一次使用确实有点绕脑,也有很多名词不曾接触过,我建议立刻马上去除那些注释,去掉那些注释后,真正有用的配置脚本不超过20行,行行重要,字字玑珠。</p>\r\n<pre><code>location / {\r\n proxy_pass http://localhost:8080;\r\n}\r\n</code></pre><p>原版配置文件是用root属性,指向了磁盘路径,这里改成<code>proxy_pass</code>属性,指向domain+端口号组成的URL,表示所有请求使用反向代理,让nginx帮客户端浏览器去<code>http://localhost:8080</code>取数据。</p>\r\n<h3 id=\"h3--cdn-\"><a name=\"将静态资源转移到七牛CDN上\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>将静态资源转移到七牛CDN上</h3><p>你一定注意到了上面的配置文件,我写上了这么一段:</p>\r\n<pre><code>#下面这个配置将URL指向resources文件夹的路径全部转发给了远程CDN\r\nlocation ^~ /resources/ {\r\n rewrite /resources/(.*) http://ocsy1jhmk.bkt.clouddn.com/resources/$1;\r\n expires 7d;\r\n}\r\n</code></pre><p>别着急,听我慢慢道来:<br>我们 <a href=\"http://git.oschina.net/newflydd/jblog\">JBlog</a> 的系统,将所有的js,css,fonts等文件全部放在<code>/resources/</code>文件夹下,当时使用SpringMVC的静态资源语法配置的,但这些资源本质上还是要通过本地Tomcat传输给客户端浏览器的,所有的带宽负载和流量计价都是消耗的本地的,如何将这一部分流量和带宽,转嫁到其他云服务上呢,使用nginx会相当方便。<br>一旦用了nginx,如果你在学SpringMVC时,遇到配置静态资源这一章,可以直接跳过,为什么?因为你配置好nginx后,指定的静态资源压根跑不到tomcat那边去,在nginx这边就帮你转发走了,而且只要你愿意,你可以把这些静态资源全部放到第三方CDN云服务上去加速,这样做是相当有好处的:第一,你的服务器不再传输这些大块头的CSS,JS还有fonts字体文件,节省了带宽,节约了流量;第二,第三方CDN一般带宽会比你买的VPS的带宽高很多,像七牛免费账户有5M以上的带宽给免费用户,还会帮你把文件放置全国多个节点上,让东南西北的浏览器都能极速下载这些静态资源;第三,一般我们很难在Tomcat层级指定某一个CSS文件的缓存策略,但是使用nginx以后,只要你熟悉正则表达式,你可以对任意通配的URL设置独立的缓存策略,这样客户端在访问你动态页面时你可以设置为禁止缓存,但动态页面中嵌入的CSS,你可以独立设置为缓存个一两天。<br>回到上面的配置文件,我们将URL中指向<code>/resources/</code>的所有文件,重定向到一个七牛免费域名中,同时我们使用七牛的同步软件 <a href=\"https://support.qiniu.com/hc/kb/article/68954/\">qrsbox</a>将resources文件夹完整同步到云端,这样,nginx可以第一时间阻断访问本地资源的操作,让用户全部转向云端服务器下载这些静态资源。其中<code>(.*)</code>是正则表达式,表示任意字符出现任意次数,后面的<code>$1</code>是与其对应的占位值,指向了<code>.*</code>所指代的实际值。<br>加速,缓存,节约流量,降低本地服务器的带宽负载,nginx太棒了!<br>nginx还有更多更强大的功能,比如对于分布式服务非常重要的「负载均衡」,我们这里的JBlog项目因为就一台WEB服务器,而且基本上不会有高并发的可能性,遇到DDOS攻击机会也非常小,所以暂时用不到这个高档玩意儿,有兴趣的朋友可以继续研究。</p>\r\n','###初次领略nginx的神奇\r\n之前有听说过nginx,当时只不过认为是普通的HTTP前置机一样的服务,确实没想这么好用:反向代理、URL转发、负载均衡,统统集中在这不到2M的软件中,真是让人惊叹。\r\n###下载安装nginx\r\n最新版的 nginx 是 [nginx-1.11.4](http://nginx.org/en/download.html)\r\n我在本地PC安装测试的是Windows版,Linux版下载和配置是一样的。\r\n下载回来后解压,可以直接鼠标点击`nginx.exe`运行。(PS:运行之前保证80端口是空闲的)\r\n运行后一闪而过,根本不知道发生了什么。这时候请打开浏览器,输入`http://localhost`,如果看到nginx的欢迎页面,说明刚才双击`nginx.exe`已经产生了作用,nginx服务器已经在后台帮你默默运行着一个超轻量级的WEB服务器。\r\n![](http://ocsy1jhmk.bkt.clouddn.com/65b43290-000a-478f-ba64-740c1c343a28.png)\r\n###配置nginx实现反向代理Tomcat\r\nOK,目前我们运行的nginx还只能给浏览器响应简单的HTML、CSS、js等静态资源,下面我们通过简单几行配置,让ngnix与处理J2EE的tomcat进行连通,当客户端浏览器访问指定格式的URL时,由nginx统一接管,然后nginx主动访问本地或远程的Tomcat(本地的tomcat需运行在另一个端口上,不要与nginx冲突),得到响应后,再由nginx转发给客户端浏览器,这个流程就叫做[「反向代理(Reverse Proxy)」](http://baike.baidu.com/link?url=382kwU5bvRwNXpO-ivIWDM8tChI9MzP29A_SwBHb0p4IyFWWO4hutQ-uwSsQR2DSXWf0oSYfx1GEHV42qp0YWGbvRVATJyRVwB5NwK4UuG-sY2p_5AdmGETu2k-h50q5)。\r\n具体实现方法如下:\r\n- 设置Tomcat的端口号,避开80端口,使用8080,or any others you like.设置tomcat的conf文件夹下的`server.xml`,此处不表。\r\n- 对nginx的配置文件`nginx.conf`进行修改,此配置文件在nginx文件夹中的conf文件夹中:\r\n\r\n```\r\nhttp {\r\n include mime.types;\r\n default_type application/octet-stream;\r\n\r\n sendfile on;\r\n keepalive_timeout 65;\r\n\t\r\n server {\r\n listen 80;\r\n server_name localhost;\r\n\t\t\r\n\t\t#转交所有URL给tomcat\r\n location / {\r\n\t\t\tproxy_pass http://localhost:8080;\r\n }\r\n\t\t\r\n\t\t#下面这个配置将URL指向resources文件夹的路径全部转发给了远程CDN\r\n\t\tlocation ^~ /resources/ {\r\n\t\t\trewrite /resources/(.*) http://ocsy1jhmk.bkt.clouddn.com/resources/$1;\r\n\t\t\texpires 7d;\r\n\t\t}\r\n\t}\r\n}\r\n```\r\n原版的`nginx.conf`文件里面密密麻麻好多注释,第一次使用确实有点绕脑,也有很多名词不曾接触过,我建议立刻马上去除那些注释,去掉那些注释后,真正有用的配置脚本不超过20行,行行重要,字字玑珠。\r\n```\r\nlocation / {\r\n\tproxy_pass http://localhost:8080;\r\n}\r\n```\r\n原版配置文件是用root属性,指向了磁盘路径,这里改成`proxy_pass`属性,指向domain+端口号组成的URL,表示所有请求使用反向代理,让nginx帮客户端浏览器去`http://localhost:8080`取数据。\r\n###将静态资源转移到七牛CDN上\r\n你一定注意到了上面的配置文件,我写上了这么一段:\r\n```\r\n#下面这个配置将URL指向resources文件夹的路径全部转发给了远程CDN\r\nlocation ^~ /resources/ {\r\n\trewrite /resources/(.*) http://ocsy1jhmk.bkt.clouddn.com/resources/$1;\r\n\texpires 7d;\r\n}\r\n```\r\n别着急,听我慢慢道来:\r\n我们 [JBlog](http://git.oschina.net/newflydd/jblog) 的系统,将所有的js,css,fonts等文件全部放在`/resources/`文件夹下,当时使用SpringMVC的静态资源语法配置的,但这些资源本质上还是要通过本地Tomcat传输给客户端浏览器的,所有的带宽负载和流量计价都是消耗的本地的,如何将这一部分流量和带宽,转嫁到其他云服务上呢,使用nginx会相当方便。\r\n一旦用了nginx,如果你在学SpringMVC时,遇到配置静态资源这一章,可以直接跳过,为什么?因为你配置好nginx后,指定的静态资源压根跑不到tomcat那边去,在nginx这边就帮你转发走了,而且只要你愿意,你可以把这些静态资源全部放到第三方CDN云服务上去加速,这样做是相当有好处的:第一,你的服务器不再传输这些大块头的CSS,JS还有fonts字体文件,节省了带宽,节约了流量;第二,第三方CDN一般带宽会比你买的VPS的带宽高很多,像七牛免费账户有5M以上的带宽给免费用户,还会帮你把文件放置全国多个节点上,让东南西北的浏览器都能极速下载这些静态资源;第三,一般我们很难在Tomcat层级指定某一个CSS文件的缓存策略,但是使用nginx以后,只要你熟悉正则表达式,你可以对任意通配的URL设置独立的缓存策略,这样客户端在访问你动态页面时你可以设置为禁止缓存,但动态页面中嵌入的CSS,你可以独立设置为缓存个一两天。\r\n回到上面的配置文件,我们将URL中指向`/resources/`的所有文件,重定向到一个七牛免费域名中,同时我们使用七牛的同步软件 [qrsbox](https://support.qiniu.com/hc/kb/article/68954/)将resources文件夹完整同步到云端,这样,nginx可以第一时间阻断访问本地资源的操作,让用户全部转向云端服务器下载这些静态资源。其中`(.*)`是正则表达式,表示任意字符出现任意次数,后面的`$1`是与其对应的占位值,指向了`.*`所指代的实际值。\r\n加速,缓存,节约流量,降低本地服务器的带宽负载,nginx太棒了!\r\nnginx还有更多更强大的功能,比如对于分布式服务非常重要的「负载均衡」,我们这里的JBlog项目因为就一台WEB服务器,而且基本上不会有高并发的可能性,遇到DDOS攻击机会也非常小,所以暂时用不到这个高档玩意儿,有兴趣的朋友可以继续研究。','2016-09-29 12:33:19',0,NULL,'nginx-cdn','2016-09-29 14:43:27',2,1,258,142),(143,'使用XP Embedded构建正版XP嵌入式环境,并打包为wim部署量产usbstick','<h3 id=\"h3-u9700u6C42u5206u6790\"><a name=\"需求分析\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>需求分析</h3><p>公司接到一个任务,为一家德资企业在中国的工厂制作国产的面板系统,面板系统包括软硬件环境:具体包括钣金、x86架构的工业PC、触摸屏及控制按键。<br>其中工业PC中应包括一套正版的XP Embedded嵌入式系统,以及该企业自己的专业软件,并最终制作成一个usb设备的PE系统,由其中的bat文件引导镜像的还原和恢复。<br>这家德资中国工厂原来的面板系统全部由德国进口,造价不菲,入驻中国后一部分中国的管理层期望能将这种面板系统国产化,我公司争取到这个机会。<br>硬件方面这里不提,由我公司硬件和电气工程师负责。<br>操作系统和软件方面主要由我负责,历时一周,基本每天加班到深夜,终于摸索得差不多了,本博文整理一下整个过程,帮助有需要的朋友,也趁着记忆犹新,给自己留一份备案记录。</p>\r\n<h3 id=\"h3-xp-embedded-xp\"><a name=\"XP Embedded构建定制版的XP\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>XP Embedded构建定制版的XP</h3><p>XP Embedded是构建嵌入式XP的开发环境,这个软件已经相当古老,网络上基本没有下载,我公司花钱从一家微软代理商那边买到正版的XP Embedded开发环境和一个序列号。对方表示这套开发环境已经相当老旧,不建议我们使用,推荐使用新版的WES2009,或者更高的WES7及WES8.1来构建windows 7和windows 8.1的嵌入式版本。但客户为保持与德国面板的一致性,并没有同意我们构建更高版本嵌入式Windows系统的提案。顺便提一句,WES2009也是可以构建XP嵌入式的,但微软官网只有evaluation版本,即评估板,评估板不具合法授权,生成的XP系统只有120天试用寿命,桌面右下方还有evaluation的水印,虽然可以强制修改,但对待外资企业还是谨慎一些好。</p>\r\n<p>Windows XP由上万个组件构成,每一个小功能都是细化为一个组件,这些组件可以在XP Embedded中拼装。运行一个最精简的图形界面XP,只需要大概7个组件即可,然而这种系统连桌面右键菜单都不会提供,甚至连CMD命令都无法打开。<br>如果将10000多个组件全部配置上去,XP系统会相当庞大,什么Media Player,IIS,SQL server都在上面,启动也很缓慢。</p>\r\n<p>这就给我们配置嵌入式的XP系统带来困难,因为配置的都能运行,但根本不知道有哪些组件缺失,会对之后的运行造成什么问题。<br>这之间我们配置了不下20个版本的XP,每个版本都要经历配置,分析依赖,构建,安装,初始化的过程,前后需要1个小时左右时间才能看到结果。<br>举两个栗子:<br>我们配置的第7个版本,看上去已经比较完善了,所有硬件驱动都搞定,德国公司的专业软件也能运行,后来发现还是不能用,因为一个「<a href=\"http://baike.baidu.com/view/1495703.htm\">EWF</a>」的组件没有添加,这个组件是德国公司需要的,用来保护C盘,相当于一个C盘的还原卡。组件缺失只能重新打包,我们试着去强制安装EWF,但没有成功,这个功能是嵌入式XP独有的,普通的XP Professional并没有。<br>当我们配置到16个版本时,已经解决到多数问题,甚至都已经打包成release版了,但发现打包还原后,并不能在PE引导还原系统时,初始化Computer Name,后来发现是一个名叫「<a href=\"http://baike.baidu.com/view/843375.htm\">WMIC</a>」的组件没有配置,这个组件提供了从命令行接口和批命令脚本执行系统管理的支持,又得重新打包。</p>\r\n<p>最终生成了一套实现了所有必要功能的Windows XP,该配置文件我上传到360云盘,有这方面需求的朋友可以下载:<br><a href=\"http://ocsy1jhmk.bkt.clouddn.com/xpe.slx\">http://ocsy1jhmk.bkt.clouddn.com/xpe.slx</a> </p>\r\n<h3 id=\"h3-wim-\"><a name=\"WIM镜像的制作\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>WIM镜像的制作</h3><p>我们最终生成了一个XP系统,使用「<a href=\"http://baike.baidu.com/view/9275812.htm\">dism</a>」命令对其打包,之前只知道镜像打包为ISO,用ghost还原,经历了这个项目后,发现打包为wim格式,用dism还原成功率更高,而且wim中可以包含多个index,可以同时还原C盘和D盘,打包速度和还原速度都比ghost快很多。<br>DISM常用的命令如下(参数的大小写不区分):</p>\r\n<pre><code>#将指定分区打包为一个镜像\r\n#将C盘打包成一个镜像,镜像文件为E:\\xpe.wim,镜像别称为xpe,描述为:xpe-release\r\n#Description参数非必要,其余参数必要。\r\nDism /Capture-Image /ImageFile:E:\\xpe.wim /CaptureDir:C:\\ /Name:xpe /Description:xpe-release\r\n\r\n#向已有镜像文件追加index\r\n#向刚才的wim文件追加D盘的备份,别称为data\r\nDism /Append-Image /ImageFile:E:\\xpe.wim /CaptureDir:D:\\ /Name:data /Description:data\r\n\r\n#还原一个镜像到指定分区\r\nDism /Apply-Image /ImageFile:E:\\xpe.wim /Index:1 /ApplyDir:C:\\\r\nDism /Apply-Image /ImageFile:E:\\xpe.wim /Index:2 /ApplyDir:D:\\\r\n</code></pre><p>基本上这三个命令就能解决大部分打包和还原的问题,命令简洁,执行速度快,微软建议用这种方式取代ghost。</p>\r\n<p>DISM命令在WIN7之后的系统都自带。</p>\r\n<h3 id=\"h3-pe-usb-bat-\"><a name=\"PE系统的制作并回调USB存储设备中的bat文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>PE系统的制作并回调USB存储设备中的bat文件</h3><p>PE系统的制作这里不祥解了,网上教程很多,主要是要制作一个精简的PE系统,甚至连GUI图形界面都可以没有,但一定要有两个命令:「<a href=\"http://baike.baidu.com/view/1578663.htm\">diskpart</a>」和「dism」。</p>\r\n<p>diskpart命令用来对磁盘进行管理的,拥有分区、格式化、设置active激活等功能,超级简单也超级强大。<br>常用指令如下:</p>\r\n<pre><code>clean :清除整个硬盘,急剧破坏性\r\nselect :disk/partition/volume 选择磁盘/分区/卷标\r\ncreate partition primary size=5000 :创建分区大小5G\r\nactive :设置选中分区为激活分区\r\nassign letter=c :设置选中分区的盘符为C\r\n</code></pre><p>针对一个量产版的启动U盘,必须要能自动判断是否需要对空白的磁盘进行分区,需要在PE系统启动后自动寻找到U盘设备,并回调其中的bat脚本。</p>\r\n<p>这段bat脚本用来引导用户安装或者还原系统,同时还能按需格式化物理硬盘。为什么不将其放在PE系统内部,因为PE系统封装起来耗时,且没有专业工具无法完成,所以使用回调方式来调用U盘中的引导脚本。</p>\r\n<p>具体的脚本代码我不方便贴出来,涉及到版权问题。<br>大致流程就是使用echo命令构建一个文本菜单,让用户选择备份或者还原,<br>如果备份,就用dism命令将C和D驱动器制作成一个wim放在E盘并拷贝到U盘中,<br>如果还原,就列出U盘中所有可供还原的wim,让用户选择需要还原的镜像,并用dism命令还原。<br>在备份和还原操作之前,需要用diskpart命令分析一下磁盘分区,设定合适的盘符。</p>\r\n<p>网上关于这方面的资料实在太少。<br>如果您也遇到了类似的问题,如果您恰巧能看到这篇文章,有任何疑问可以联系我。<br><a href=\"mailto:newflydd@189.cn\">newflydd@189.cn</a></p>\r\n','###需求分析\r\n公司接到一个任务,为一家德资企业在中国的工厂制作国产的面板系统,面板系统包括软硬件环境:具体包括钣金、x86架构的工业PC、触摸屏及控制按键。\r\n其中工业PC中应包括一套正版的XP Embedded嵌入式系统,以及该企业自己的专业软件,并最终制作成一个usb设备的PE系统,由其中的bat文件引导镜像的还原和恢复。\r\n这家德资中国工厂原来的面板系统全部由德国进口,造价不菲,入驻中国后一部分中国的管理层期望能将这种面板系统国产化,我公司争取到这个机会。\r\n硬件方面这里不提,由我公司硬件和电气工程师负责。\r\n操作系统和软件方面主要由我负责,历时一周,基本每天加班到深夜,终于摸索得差不多了,本博文整理一下整个过程,帮助有需要的朋友,也趁着记忆犹新,给自己留一份备案记录。\r\n\r\n###XP Embedded构建定制版的XP\r\nXP Embedded是构建嵌入式XP的开发环境,这个软件已经相当古老,网络上基本没有下载,我公司花钱从一家微软代理商那边买到正版的XP Embedded开发环境和一个序列号。对方表示这套开发环境已经相当老旧,不建议我们使用,推荐使用新版的WES2009,或者更高的WES7及WES8.1来构建windows 7和windows 8.1的嵌入式版本。但客户为保持与德国面板的一致性,并没有同意我们构建更高版本嵌入式Windows系统的提案。顺便提一句,WES2009也是可以构建XP嵌入式的,但微软官网只有evaluation版本,即评估板,评估板不具合法授权,生成的XP系统只有120天试用寿命,桌面右下方还有evaluation的水印,虽然可以强制修改,但对待外资企业还是谨慎一些好。\r\n\r\nWindows XP由上万个组件构成,每一个小功能都是细化为一个组件,这些组件可以在XP Embedded中拼装。运行一个最精简的图形界面XP,只需要大概7个组件即可,然而这种系统连桌面右键菜单都不会提供,甚至连CMD命令都无法打开。\r\n如果将10000多个组件全部配置上去,XP系统会相当庞大,什么Media Player,IIS,SQL server都在上面,启动也很缓慢。\r\n\r\n这就给我们配置嵌入式的XP系统带来困难,因为配置的都能运行,但根本不知道有哪些组件缺失,会对之后的运行造成什么问题。\r\n这之间我们配置了不下20个版本的XP,每个版本都要经历配置,分析依赖,构建,安装,初始化的过程,前后需要1个小时左右时间才能看到结果。\r\n举两个栗子:\r\n我们配置的第7个版本,看上去已经比较完善了,所有硬件驱动都搞定,德国公司的专业软件也能运行,后来发现还是不能用,因为一个「[EWF](http://baike.baidu.com/view/1495703.htm)」的组件没有添加,这个组件是德国公司需要的,用来保护C盘,相当于一个C盘的还原卡。组件缺失只能重新打包,我们试着去强制安装EWF,但没有成功,这个功能是嵌入式XP独有的,普通的XP Professional并没有。\r\n当我们配置到16个版本时,已经解决到多数问题,甚至都已经打包成release版了,但发现打包还原后,并不能在PE引导还原系统时,初始化Computer Name,后来发现是一个名叫「[WMIC](http://baike.baidu.com/view/843375.htm)」的组件没有配置,这个组件提供了从命令行接口和批命令脚本执行系统管理的支持,又得重新打包。\r\n\r\n最终生成了一套实现了所有必要功能的Windows XP,该配置文件我上传到360云盘,有这方面需求的朋友可以下载:\r\n[http://ocsy1jhmk.bkt.clouddn.com/xpe.slx](http://ocsy1jhmk.bkt.clouddn.com/xpe.slx) \r\n\r\n###WIM镜像的制作\r\n我们最终生成了一个XP系统,使用「[dism](http://baike.baidu.com/view/9275812.htm)」命令对其打包,之前只知道镜像打包为ISO,用ghost还原,经历了这个项目后,发现打包为wim格式,用dism还原成功率更高,而且wim中可以包含多个index,可以同时还原C盘和D盘,打包速度和还原速度都比ghost快很多。\r\nDISM常用的命令如下(参数的大小写不区分):\r\n```\r\n#将指定分区打包为一个镜像\r\n#将C盘打包成一个镜像,镜像文件为E:\\xpe.wim,镜像别称为xpe,描述为:xpe-release\r\n#Description参数非必要,其余参数必要。\r\nDism /Capture-Image /ImageFile:E:\\xpe.wim /CaptureDir:C:\\ /Name:xpe /Description:xpe-release\r\n\r\n#向已有镜像文件追加index\r\n#向刚才的wim文件追加D盘的备份,别称为data\r\nDism /Append-Image /ImageFile:E:\\xpe.wim /CaptureDir:D:\\ /Name:data /Description:data\r\n\r\n#还原一个镜像到指定分区\r\nDism /Apply-Image /ImageFile:E:\\xpe.wim /Index:1 /ApplyDir:C:\\\r\nDism /Apply-Image /ImageFile:E:\\xpe.wim /Index:2 /ApplyDir:D:\\\r\n```\r\n\r\n基本上这三个命令就能解决大部分打包和还原的问题,命令简洁,执行速度快,微软建议用这种方式取代ghost。\r\n\r\nDISM命令在WIN7之后的系统都自带。\r\n\r\n###PE系统的制作并回调USB存储设备中的bat文件\r\nPE系统的制作这里不祥解了,网上教程很多,主要是要制作一个精简的PE系统,甚至连GUI图形界面都可以没有,但一定要有两个命令:「[diskpart](http://baike.baidu.com/view/1578663.htm)」和「dism」。\r\n\r\ndiskpart命令用来对磁盘进行管理的,拥有分区、格式化、设置active激活等功能,超级简单也超级强大。\r\n常用指令如下:\r\n```\r\nclean \t:清除整个硬盘,急剧破坏性\r\nselect \t:disk/partition/volume 选择磁盘/分区/卷标\r\ncreate partition primary size=5000\t:创建分区大小5G\r\nactive\t:设置选中分区为激活分区\r\nassign letter=c \t:设置选中分区的盘符为C\r\n```\r\n\r\n针对一个量产版的启动U盘,必须要能自动判断是否需要对空白的磁盘进行分区,需要在PE系统启动后自动寻找到U盘设备,并回调其中的bat脚本。\r\n\r\n这段bat脚本用来引导用户安装或者还原系统,同时还能按需格式化物理硬盘。为什么不将其放在PE系统内部,因为PE系统封装起来耗时,且没有专业工具无法完成,所以使用回调方式来调用U盘中的引导脚本。\r\n\r\n具体的脚本代码我不方便贴出来,涉及到版权问题。\r\n大致流程就是使用echo命令构建一个文本菜单,让用户选择备份或者还原,\r\n如果备份,就用dism命令将C和D驱动器制作成一个wim放在E盘并拷贝到U盘中,\r\n如果还原,就列出U盘中所有可供还原的wim,让用户选择需要还原的镜像,并用dism命令还原。\r\n在备份和还原操作之前,需要用diskpart命令分析一下磁盘分区,设定合适的盘符。\r\n\r\n网上关于这方面的资料实在太少。\r\n如果您也遇到了类似的问题,如果您恰巧能看到这篇文章,有任何疑问可以联系我。\r\nnewflydd@189.cn','2016-10-14 11:45:11',0,NULL,'build-xpe','2016-10-21 13:40:44',3,1,87,143),(144,'Win32-C++编程:读取ini配置文件,延时启动另一个exe程序,取消命令行窗体','<h3 id=\"h3-u9700u6C42u573Au666F\"><a name=\"需求场景\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>需求场景</h3><p>windows启动后,延时加载另一个exe程序,延时的毫秒数要不断调试得到一个合理值,并且不希望看到命令行窗体。</p>\r\n<h3 id=\"h3-u89E3u51B3u65B9u6848\"><a name=\"解决方案\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>解决方案</h3><p>编写一个C++程序,读取同目录下的ini配置文件,Win32为ini文件格式提供了内置API读写。配置文件中定义了延时毫秒数和运行exe的路径信息。延时指定毫秒数,调用指定exe程序。本人十分讨厌使用VC6.0或者VS2xxxx等IDE编写win32程序,总觉得这些IDE模糊了一些让人捉摸不透的东西,同时又引入了一些花里胡哨的东西,因此使用g++编译。</p>\r\n<h3 id=\"h3-startup-c\"><a name=\"startup.c\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>startup.c</h3><pre><code>#include &lt;stdio.h&gt;\r\n#include &lt;windows.h&gt;\r\n\r\nusing namespace std;\r\n\r\nint main()\r\n{ \r\n int delay = GetPrivateProfileInt( \r\n TEXT(&quot;startrun&quot;) // 指向包含 Section 名称的字符串地址 \r\n ,TEXT(&quot;delay&quot;) // 指向包含 Key 名称的字符串地址 \r\n ,30000 // 如果 Key 值没有找到,则返回缺省的值是多少 \r\n ,TEXT(&quot;.\\\\conf.ini&quot;) // ini 文件的文件名 \r\n );\r\n\r\n char filePath[MAX_PATH]; \r\n GetPrivateProfileString(\r\n TEXT(&quot;startrun&quot;)\r\n ,TEXT(&quot;filepath&quot;)\r\n ,TEXT(&quot;test.exe&quot;)\r\n ,filePath\r\n ,MAX_PATH\r\n ,TEXT(&quot;.\\\\conf.ini&quot;)\r\n ); \r\n\r\n printf(&quot;%d:%s\\n&quot;, delay, filePath);\r\n\r\n Sleep(delay);\r\n\r\n\r\n //一些必备参数设置\r\n STARTUPINFO si;\r\n memset(&amp;si,0,sizeof(STARTUPINFO));//初始化si在内存块中的值(详见memset函数)\r\n si.cb=sizeof(STARTUPINFO);\r\n si.dwFlags=STARTF_USESHOWWINDOW;\r\n si.wShowWindow=SW_SHOW;\r\n PROCESS_INFORMATION pi;//必备参数设置结束\r\n if( !CreateProcess(TEXT(filePath), NULL, NULL, NULL, FALSE, 0, NULL, NULL, &amp;si, &amp;pi )){\r\n exit(1);\r\n }else{\r\n exit(0);\r\n }\r\n}\r\n</code></pre><p>使用g++编译的指令集成在sublime3中,参见:</p>\r\n<pre><code>{\r\n&quot;cmd&quot;: [&quot;g++&quot;, &quot;-std=c++11&quot;, &quot;-mwindows&quot;, &quot;${file}&quot;, &quot;-o&quot;,&quot;${file_path}/${file_base_name}&quot;],\r\n&quot;file_regex&quot;: &quot;^(..[^:]*):([0-9]+):?([0-9]+)?:?(.*)$&quot;,\r\n&quot;working_dir&quot;: &quot;${file_path}&quot;,\r\n&quot;encoding&quot;:&quot;cp936&quot;,\r\n&quot;selector&quot;: &quot;source.c&quot;,\r\n&quot;variants&quot;:\r\n[\r\n{\r\n&quot;name&quot;: &quot;Run&quot;,\r\n&quot;cmd&quot;: [&quot;cmd&quot;,&quot;/C&quot;,&quot;start&quot;,&quot;cmd&quot;,&quot;/c&quot;, &quot;${file_path}/${file_base_name}.exe &amp;pause&quot;]\r\n}\r\n]\r\n}\r\n</code></pre><p>其中的<code>-mwindows</code>参数可以使编译出来的exe去掉黑屏命令行窗体。</p>\r\n','###需求场景\r\nwindows启动后,延时加载另一个exe程序,延时的毫秒数要不断调试得到一个合理值,并且不希望看到命令行窗体。\r\n\r\n###解决方案\r\n编写一个C++程序,读取同目录下的ini配置文件,Win32为ini文件格式提供了内置API读写。配置文件中定义了延时毫秒数和运行exe的路径信息。延时指定毫秒数,调用指定exe程序。本人十分讨厌使用VC6.0或者VS2xxxx等IDE编写win32程序,总觉得这些IDE模糊了一些让人捉摸不透的东西,同时又引入了一些花里胡哨的东西,因此使用g++编译。\r\n\r\n###startup.c\r\n```\r\n#include <stdio.h>\r\n#include <windows.h>\r\n\r\nusing namespace std;\r\n\r\nint main()\r\n{\t\r\n\tint delay = GetPrivateProfileInt( \r\n\t\tTEXT(\"startrun\") // 指向包含 Section 名称的字符串地址 \r\n\t\t,TEXT(\"delay\") // 指向包含 Key 名称的字符串地址 \r\n\t\t,30000 // 如果 Key 值没有找到,则返回缺省的值是多少 \r\n\t\t,TEXT(\".\\\\conf.ini\") // ini 文件的文件名 \r\n\t);\r\n\r\n\tchar filePath[MAX_PATH]; \r\n\tGetPrivateProfileString(\r\n\t\tTEXT(\"startrun\")\r\n\t\t,TEXT(\"filepath\")\r\n\t\t,TEXT(\"test.exe\")\r\n\t\t,filePath\r\n\t\t,MAX_PATH\r\n\t\t,TEXT(\".\\\\conf.ini\")\r\n\t); \r\n\r\n\tprintf(\"%d:%s\\n\", delay, filePath);\r\n\r\n\tSleep(delay);\r\n\r\n\r\n\t//一些必备参数设置\r\n\tSTARTUPINFO si;\r\n\tmemset(&si,0,sizeof(STARTUPINFO));//初始化si在内存块中的值(详见memset函数)\r\n\tsi.cb=sizeof(STARTUPINFO);\r\n\tsi.dwFlags=STARTF_USESHOWWINDOW;\r\n\tsi.wShowWindow=SW_SHOW;\r\n\tPROCESS_INFORMATION pi;//必备参数设置结束\r\n\tif( !CreateProcess(TEXT(filePath), NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi )){\r\n\t\texit(1);\r\n\t}else{\r\n\t\texit(0);\r\n\t}\r\n}\r\n```\r\n\r\n使用g++编译的指令集成在sublime3中,参见:\r\n```\r\n{\r\n\"cmd\": [\"g++\", \"-std=c++11\", \"-mwindows\", \"${file}\", \"-o\",\"${file_path}/${file_base_name}\"],\r\n\"file_regex\": \"^(..[^:]*):([0-9]+):?([0-9]+)?:?(.*)$\",\r\n\"working_dir\": \"${file_path}\",\r\n\"encoding\":\"cp936\",\r\n\"selector\": \"source.c\",\r\n\"variants\":\r\n[\r\n{\r\n\"name\": \"Run\",\r\n\"cmd\": [\"cmd\",\"/C\",\"start\",\"cmd\",\"/c\", \"${file_path}/${file_base_name}.exe &pause\"]\r\n}\r\n]\r\n}\r\n```\r\n其中的`-mwindows`参数可以使编译出来的exe去掉黑屏命令行窗体。','2016-10-21 17:54:18',0,NULL,'run-another-exe','2016-10-21 17:54:54',3,1,43,144),(145,'刚发现一个极好的国内阿里云maven镜像,太赞了!','<p>地址是:<code>http://maven.aliyun.com/nexus/content/groups/public</code></p>\r\n<p>如果像我一样用gradle的话,配置仓库如下:</p>\r\n<pre><code>repositories {\r\n maven {url &quot;http://maven.aliyun.com/nexus/content/groups/public&quot; }\r\n //maven {url &quot;http://uk.maven.org/maven2&quot; }\r\n //mavenLocal()\r\n //mavenCentral()\r\n //jcenter()\r\n}\r\n</code></pre>','地址是:`http://maven.aliyun.com/nexus/content/groups/public`\r\n\r\n如果像我一样用gradle的话,配置仓库如下:\r\n\r\n```\r\nrepositories {\r\n maven {url \"http://maven.aliyun.com/nexus/content/groups/public\" }\r\n //maven {url \"http://uk.maven.org/maven2\" }\r\n //mavenLocal()\r\n //mavenCentral()\r\n //jcenter()\r\n}\r\n```','2016-11-09 16:34:50',0,NULL,'maven-reponsitory','2016-11-09 16:36:38',4,1,32,145);
#
# Structure for table "jblog_comment"
#
DROP TABLE IF EXISTS `jblog_comment`;
CREATE TABLE `jblog_comment` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '评论,对于article(type=0)的文章,可以拥有评论,一对多关系',
`article_id` int(11) NOT NULL COMMENT '文章id,做外键约束',
`nickname` varchar(45) NOT NULL DEFAULT 'nickname' COMMENT '评论者昵称,用于页面显示,不可以为空',
`email` varchar(45) DEFAULT NULL COMMENT '评论者邮箱',
`create_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '评论时间',
`data` varchar(200) DEFAULT NULL COMMENT '非html格式的普通文本,限制在200字符以内,即使用户需要嵌入简短的代码片段也是足够的,不建议在评论里大规模留言,如需要交流可以选择sns方式',
`sns` varchar(60) DEFAULT NULL COMMENT '社交信息,如QQ,微信,微博等,这一块需要研究一下“多说”的API',
`father_id` int(11) DEFAULT NULL COMMENT '自身的嵌套层级关系,评论可以回复,如果为null则表示父节点,如果不为null,则需要指向id,表示该id评论的子节点',
PRIMARY KEY (`id`),
KEY `fk_comment_idx` (`article_id`),
KEY `fk_father_idx` (`father_id`),
CONSTRAINT `fk_comment` FOREIGN KEY (`article_id`) REFERENCES `jblog_article` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_father` FOREIGN KEY (`father_id`) REFERENCES `jblog_comment` (`id`) ON DELETE SET NULL ON UPDATE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
#
# Data for table "jblog_comment"
#
#
# Structure for table "jblog_instruction"
#
DROP TABLE IF EXISTS `jblog_instruction`;
CREATE TABLE `jblog_instruction` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`table_name` varchar(45) NOT NULL DEFAULT 'jblog_instruction',
`column_name` varchar(45) DEFAULT NULL,
`instruction` text,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='系统表及字段说明';
#
# Data for table "jblog_instruction"
#
INSERT INTO `jblog_instruction` VALUES (1,'jblog_article','type','文档类型:\r\n0:正式文章\r\n1:无标题头部文字\r\n2:无标题底部文字\r\n3:外部数据,引入url获取外部数据作为data\r\n11:自定义widget插件\r\n12:widgetTags插件\r\n13:widget存档插件\r\n'),(2,'jblog_article','state','文档状态: \r\n0:编辑状态,不显示在页面上,显示在后台列表 \r\n1:发布 \r\n2:删除,不显示在前台和后台列表,显示在后台垃圾箱'),(3,'jblog_system','duoshuo_id','本系统使用了多说的评论组件\n这个字段需要保存多说的用户ID\n请访问多说网站进行访问\nhttp://duoshuo.com/');
#
# Structure for table "jblog_menu"
#
DROP TABLE IF EXISTS `jblog_menu`;
CREATE TABLE `jblog_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '页面上的菜单表',
`title` varchar(45) NOT NULL COMMENT '显示名称',
`url_name` varchar(45) DEFAULT NULL COMMENT 'url中的英文标识,此项可以为空,如果为空则显示url值,url_name和url不能同时为空。',
`is_left` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否是靠左显示\n1:靠左\n0:靠右',
`url` varchar(80) DEFAULT NULL COMMENT '完整的URL信息,适用于外部链接,如果url_name为空,则前台显示url信息。',
`show` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否在前台显示,处于设计状态的菜单项可以选择暂时不显示\n1:显示\n0:不显示',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
#
# Data for table "jblog_menu"
#
INSERT INTO `jblog_menu` VALUES (0,'index','index',b'1',NULL,b'1'),(1,'关于丁丁','about',b'1',NULL,b'1'),(2,'简历','profile',b'1',NULL,b'1'),(3,'链接','links',b'1',NULL,b'1'),(4,'推荐书单','booklist',b'1',NULL,b'1'),(5,'碎语','jiwai',b'1',NULL,b'1'),(6,'日志列表','list',b'1',NULL,b'1'),(7,'广告','ad',b'1',NULL,b'1'),(8,'RSS','rss.xml',b'0',NULL,b'1'),(9,'微博',NULL,b'0','http://weibo.com/newflypig',b'1');
#
# Structure for table "jblog_menu_article"
#
DROP TABLE IF EXISTS `jblog_menu_article`;
CREATE TABLE `jblog_menu_article` (
`jblog_menu_id` int(11) NOT NULL,
`jblog_article_id` int(11) NOT NULL,
PRIMARY KEY (`jblog_menu_id`,`jblog_article_id`),
KEY `fk_jblog_menu_has_jblog_archive_jblog_archive1_idx` (`jblog_article_id`),
KEY `fk_jblog_menu_has_jblog_archive_jblog_menu_idx` (`jblog_menu_id`),
CONSTRAINT `fk_jblog_menu_has_jblog_archive_jblog_archive1` FOREIGN KEY (`jblog_article_id`) REFERENCES `jblog_article` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_jblog_menu_has_jblog_archive_jblog_menu` FOREIGN KEY (`jblog_menu_id`) REFERENCES `jblog_menu` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
#
# Data for table "jblog_menu_article"
#
INSERT INTO `jblog_menu_article` VALUES (0,3),(0,13),(0,15),(0,25),(0,29),(0,30),(0,32),(0,47),(0,48),(0,50),(0,52),(0,69),(0,86),(0,87),(0,89),(0,94),(0,95),(0,141),(0,142),(0,143),(0,144),(0,145),(1,3),(6,47),(6,48),(6,50),(6,69),(6,89);
#
# Structure for table "jblog_system"
#
DROP TABLE IF EXISTS `jblog_system`;
CREATE TABLE `jblog_system` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '系统配置项,键值对形式,key唯一',
`jkey` varchar(45) NOT NULL,
`jvalue` text NOT NULL,
`append` varchar(45) DEFAULT NULL COMMENT '一些次要项,辅助项,扩展项信息',
PRIMARY KEY (`id`),
UNIQUE KEY `key_UNIQUE` (`jkey`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;
#
# Data for table "jblog_system"
#
INSERT INTO `jblog_system` VALUES (1,'username','newflypig',NULL),(2,'password','c0facafc7b95fbc144f98fe67f5575ba',NULL),(3,'blog_name','丁丁的博客',NULL),(4,'blog_record','苏ICP备15060831号',NULL),(5,'blog_metas','<meta name=\"baidu-site-verification\" content=\"xFue6Gixrf\" />\n<meta property=\"wb:webmaster\" content=\"14f9418f317b8cdd\" />\n<meta name=\"360-site-verification\" content=\"4762245949e203d90923085fb1dd08d4\" />\n<meta name=\"msvalidate.01\" content=\"B6FA70DDC0481B1F2FDB70402B8AEA0B\" />\n<meta name=\"google-site-verification\" content=\"lLfFtcdCRe4kR_NesI88Rm9yqyLmffhu4wmjWmfhu0Y\" />',NULL),(6,'blog_url','www.hexcode.cn',NULL),(7,'blog_js','<script src=\"http://s4.cnzz.com/z_stat.php?id=1260233739&web_id=1260233739\" language=\"JavaScript\"></script>\n<script>\n(function(){\n var bp = document.createElement(\'script\');\n var curProtocol = window.location.protocol.split(\':\')[0];\n if (curProtocol === \'https\') {\n bp.src = \'https://zz.bdstatic.com/linksubmit/push.js\'; \n }\n else {\n bp.src = \'http://push.zhanzhang.baidu.com/push.js\';\n }\n var s = document.getElementsByTagName(\"script\")[0];\n s.parentNode.insertBefore(bp, s);\n})();\n</script>',NULL),(8,'blog_description','丁丁的博客,一个全栈工程师的博客,关注各类编程技术,包括并不限于JAVA,数据库,WEB前端,C/C++。',NULL),(9,'blog_keywords','JBlog,丁丁,全栈工程师,个人博客,JAVA,数据库,WEB前端,HTML5,编程,算法',NULL),(10,'qiniu_access_key','P4-Q61nC8OSYU0nLI7gDM75-EXb7MwE3UVs8_52h',NULL),(11,'qiniu_secret_key','pla_vYEqjgGonRYyN1oBo-vtFFsbAkvYGgYeSoa-',NULL),(12,'duoshuo_id','newflydd',NULL),(13,'geetest_ak','d9176d27e2e4130fbcc7ee7d749603c4',NULL),(14,'geetest_sk','85281a69ccd16ffb6c7e8506fad3f666',NULL),(15,'blog_email','newflydd@189.cn',NULL),(16,'guest_username','guest',NULL),(17,'guest_password','guest',NULL);
#
# Structure for table "jblog_tags_t"
#
DROP TABLE IF EXISTS `jblog_tags_t`;
CREATE TABLE `jblog_tags_t` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '当文章属性为0,即正式文章时,文章拥有多对多的标签属性',
`title` varchar(45) NOT NULL COMMENT '标签名称',
`url_name` varchar(45) NOT NULL DEFAULT 'url_name' COMMENT 'url名称,纯英文',
PRIMARY KEY (`id`),
UNIQUE KEY `title_UNIQUE` (`title`)
) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8;
#
# Data for table "jblog_tags_t"
#
INSERT INTO `jblog_tags_t` VALUES (1,'JAVA','java'),(2,'Spring','spring'),(18,'番茄土豆','番茄土豆'),(19,'JBlog','JBlog'),(21,'七牛','七牛'),(22,'Logo','Logo'),(23,'代码测试','代码测试'),(24,'线程','线程'),(26,'AmazeUI','AmazeUI'),(32,'editor.md','editormd'),(33,'jQuery','jQuery'),(34,'CSS','CSS'),(35,'JavaScript','JavaScript'),(36,'geetest','geetest'),(37,'SpringMVC','SpringMVC'),(38,'nginx','nginx'),(39,'xpe','xpe'),(40,'embedded','embedded'),(41,'dism','dism'),(42,'diskpart','diskpart'),(43,'win32','win32'),(44,'c++','c++'),(45,'gradle','gradle'),(46,'maven','maven'),(47,'阿里云','阿里云');
#
# Structure for table "jblog_article_tags"
#
DROP TABLE IF EXISTS `jblog_article_tags`;
CREATE TABLE `jblog_article_tags` (
`jblog_article_id` int(11) NOT NULL,
`jblog_tags_id` int(11) NOT NULL,
PRIMARY KEY (`jblog_article_id`,`jblog_tags_id`),
KEY `fk_jblog_article_has_jblog_tags_jblog_tags1_idx` (`jblog_tags_id`),
KEY `fk_jblog_article_has_jblog_tags_jblog_article1_idx` (`jblog_article_id`),
CONSTRAINT `fk_jblog_article_has_jblog_tags_jblog_article1` FOREIGN KEY (`jblog_article_id`) REFERENCES `jblog_article` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_jblog_article_has_jblog_tags_jblog_tags1` FOREIGN KEY (`jblog_tags_id`) REFERENCES `jblog_tags_t` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
#
# Data for table "jblog_article_tags"
#
INSERT INTO `jblog_article_tags` VALUES (3,1),(48,19),(48,21),(48,22),(50,18),(50,19),(52,19),(52,23),(52,24),(69,19),(69,26),(86,1),(86,19),(86,21),(86,32),(87,1),(87,19),(87,32),(89,1),(89,2),(89,19),(94,1),(94,19),(95,19),(95,33),(95,34),(95,35),(141,36),(141,37),(142,19),(142,21),(142,38),(143,39),(143,40),(143,41),(143,42),(144,43),(144,44),(145,45),(145,46),(145,47);
#
# Structure for table "jblog_tips"
#
DROP TABLE IF EXISTS `jblog_tips`;
CREATE TABLE `jblog_tips` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`message` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_tip_message` (`message`)
) ENGINE=InnoDB AUTO_INCREMENT=261 DEFAULT CHARSET=utf8;
#
# Data for table "jblog_tips"
#
INSERT INTO `jblog_tips` VALUES (104,'编程就是算法和数据结构,算法和数据结构是编程的灵魂。'),(105,'代码是程序员的朋友,虽然没有热情,但是非常忠实。'),(106,'尽管少写那么多代码,但省下来的时间又在哪里呢?'),(107,'编程之久除了算法和数据结构,什么也不属于我们。'),(109,'编程中最没用的东西是源代码,最有用的东西是算法和数据结构。'),(110,'编程是一种美德,是促使一个人不断向上发展的一种原动力。'),(111,'算法和数据结构就是编程的一个重要部分,你若失掉了算法和数据结构,你就把一切都失掉了。'),(112,'代码是最为耐心、最能忍耐和最令人愉快的伙伴,在任何艰难困苦的时刻,它都不会抛弃你。'),(113,'对代码不满足,是任何真正有天才的程序员的根本特征。'),(117,'调试的错误就是编程给你最好的东西,因为在每个错误上面都标志着前进的一步。'),(120,'当你还不能写出自己满意的程序时,你就不要去睡觉。'),(123,'算法和数据结构是程序的第一秘诀,缺之算法和数据结构是编程的最大原因。'),(124,'程序员之所以犯错误,不是因为他们不懂,而是因为他们自以为什么都懂。'),(128,'有些代码不应该被忘记,也没有源代码不应该被记住。'),(133,'假如编程易懂得,那么程序员就不会热情地写出注释,也不会有得到编程的快乐。'),(148,'程序员的一生时间90%是用在编程上,而剩余的10%是活在世界上。'),(160,'有编过程的人的代码,比那些无知的人使用的软件更有价值。');
#
# View "jblog_archive_v"
#
DROP VIEW IF EXISTS `jblog_archive_v`;
CREATE
ALGORITHM = UNDEFINED
VIEW `jblog_archive_v`
AS
SELECT
`archive`.`id`, `archive`.`title`, `archive`.`url_name`, `count`(`article`.`id`)` AS `count`
FROM
(`jblog_archive_t` `archive`
JOIN (
SELECT
`ja`.`id`, `ja`.`archive_id`
FROM
`jblog_article` `ja`
WHERE
((`ja`.`type` = 0) AND (`ja`.`state` = 1))
) `article` ON ((`archive`.`id` = `article`.`archive_id`)))
GROUP BY
`archive`.`id`;
#
# View "jblog_tags_v"
#
DROP VIEW IF EXISTS `jblog_tags_v`;
CREATE
ALGORITHM = UNDEFINED
VIEW `jblog_tags_v`
AS
SELECT
`jt`.`id`, `jt`.`title`, `jt`.`url_name`, `count`(`jat`.`jblog_tags_id`)` AS `count`
FROM
((`jblog_tags_t` `jt`
JOIN `jblog_article_tags` `jat` ON ((`jt`.`id` = `jat`.`jblog_tags_id`)))
JOIN `jblog_article` `ja` ON ((`ja`.`id` = `jat`.`jblog_article_id`)))
WHERE
((`ja`.`type` = 0) AND (`ja`.`state` = 1))
GROUP BY
`jt`.`id`;
#
# Procedure "buildURL4SEO"
#
DROP PROCEDURE IF EXISTS `buildURL4SEO`;
CREATE PROCEDURE `buildURL4SEO`()
BEGIN
delete from dd_temp;
insert into dd_temp(id,url_name)
select ja.id, ja.url_name from jblog_article ja
where ja.state = 1
and ja.type = 0
;
alter table dd_temp MODIFY COLUMN url_name varchar(120);
update dd_temp d set d.url_name = d.id where d.url_name is null;
update dd_temp d set d.url_name = CONCAT("http://www.hexcode.cn/article/show/" ,d.url_name);
COMMIT;
select url_name from dd_temp;
END;
#
# Procedure "cleanup"
#
DROP PROCEDURE IF EXISTS `cleanup`;
CREATE PROCEDURE `cleanup`()
BEGIN
SET FOREIGN_KEY_CHECKS = 0;
update jblog_article ja set ja.archive_id = null;
truncate table jblog_archive_t;
insert into jblog_archive_t(title,url_name)
select distinct DATE_FORMAT( ja.create_dt, '%Y年%c月'),DATE_FORMAT( ja.create_dt, '%Y%m') from jblog_article ja
where ja.type = 0;
update jblog_article ja
set ja.archive_id = (
select jat.id from jblog_archive_t jat
where jat.title = DATE_FORMAT( ja.create_dt, '%Y年%c月')
)
where ja.type = 0;
SET FOREIGN_KEY_CHECKS = 1;
commit;
delete from jblog_tags_t where id not in(
select DISTINCT jat.jblog_tags_id from jblog_article_tags jat
);
commit;
END;
Java
1
https://gitee.com/newflydd/jblog.git
git@gitee.com:newflydd/jblog.git
newflydd
jblog
jblog
jblog2-3

搜索帮助