├── .gitattributes ├── src ├── main │ ├── resources │ │ ├── templates │ │ │ ├── mails │ │ │ │ └── welcome.html │ │ │ ├── news.vm │ │ │ ├── footer.html │ │ │ ├── letterDetail.html │ │ │ ├── header.html │ │ │ ├── letter.html │ │ │ ├── detail.html │ │ │ └── home.html │ │ ├── static │ │ │ ├── fonts │ │ │ │ ├── Flat-UI-Icons.eot │ │ │ │ ├── Flat-UI-Icons.ttf │ │ │ │ ├── Flat-UI-Icons.woff │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ └── fontawesome-webfont.woff │ │ │ ├── images │ │ │ │ ├── res │ │ │ │ │ ├── qrcode.png │ │ │ │ │ └── app-iphone-frame.png │ │ │ │ └── img │ │ │ │ │ └── pop-close.png │ │ │ ├── scripts │ │ │ │ └── main │ │ │ │ │ ├── base │ │ │ │ │ ├── util.js │ │ │ │ │ ├── event.js │ │ │ │ │ ├── upload.js │ │ │ │ │ └── base.js │ │ │ │ │ ├── site │ │ │ │ │ ├── test.js │ │ │ │ │ ├── detail.js │ │ │ │ │ └── home.js │ │ │ │ │ ├── util │ │ │ │ │ └── action.js │ │ │ │ │ └── component │ │ │ │ │ ├── popup.js │ │ │ │ │ ├── popupUpload.js │ │ │ │ │ ├── upload.js │ │ │ │ │ ├── component.js │ │ │ │ │ └── popupLogin.js │ │ │ └── styles │ │ │ │ └── font-awesome.min.css │ │ ├── toolbox.xml │ │ ├── application.properties │ │ ├── com │ │ │ └── nowcoder │ │ │ │ └── dao │ │ │ │ └── NewsDao.xml │ │ └── mybatis-config.xml │ └── java │ │ └── com │ │ └── nowcoder │ │ ├── model │ │ ├── EntityType.java │ │ ├── ViewObject.java │ │ ├── HostHolder.java │ │ ├── LoginTicket.java │ │ ├── User.java │ │ ├── Comment.java │ │ ├── Message.java │ │ └── News.java │ │ ├── async │ │ ├── EventHandler.java │ │ ├── EventType.java │ │ ├── EventProducer.java │ │ ├── handler │ │ │ ├── LikeHandler.java │ │ │ └── LoginExceptionHandler.java │ │ ├── EventModel.java │ │ └── EventConsumer.java │ │ ├── ToutiaoApplication.java │ │ ├── controller │ │ ├── SettingController.java │ │ ├── LikeController.java │ │ ├── HomeController.java │ │ ├── MessageController.java │ │ ├── LoginController.java │ │ └── NewsController.java │ │ ├── util │ │ ├── RedisKeyUtil.java │ │ ├── MailSender.java │ │ ├── ToutiaoUtil.java │ │ └── JedisAdapter.java │ │ ├── service │ │ ├── CommentService.java │ │ ├── MessageService.java │ │ ├── LikeService.java │ │ ├── NewsService.java │ │ ├── QiniuService.java │ │ └── UserService.java │ │ ├── configuration │ │ └── ToutiaoWebConfiguration.java │ │ ├── dao │ │ ├── LoginTicketDao.java │ │ ├── UserDao.java │ │ ├── CommentDao.java │ │ ├── NewsDao.java │ │ └── MessageDao.java │ │ └── interceptor │ │ ├── LoginRequiredInterceptor.java │ │ └── PassportInterceptor.java └── test │ ├── java │ └── com │ │ └── nowcoder │ │ ├── ToutiaoApplicationTests.java │ │ ├── JedisTests.java │ │ └── InitDatabaseTests.java │ └── resources │ └── init-schema.sql ├── .gitignore ├── pom.xml └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-language=java 2 | -------------------------------------------------------------------------------- /src/main/resources/templates/mails/welcome.html: -------------------------------------------------------------------------------- 1 | 你好$username,你的登陆有问题! -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | /.idea/ 3 | /mvnw 4 | /mvnw.cmd 5 | /toutiao.iml 6 | /README.md 7 | -------------------------------------------------------------------------------- /src/main/resources/static/fonts/Flat-UI-Icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Franciswyyy/toutiao/HEAD/src/main/resources/static/fonts/Flat-UI-Icons.eot -------------------------------------------------------------------------------- /src/main/resources/static/fonts/Flat-UI-Icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Franciswyyy/toutiao/HEAD/src/main/resources/static/fonts/Flat-UI-Icons.ttf -------------------------------------------------------------------------------- /src/main/resources/static/images/res/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Franciswyyy/toutiao/HEAD/src/main/resources/static/images/res/qrcode.png -------------------------------------------------------------------------------- /src/main/resources/static/fonts/Flat-UI-Icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Franciswyyy/toutiao/HEAD/src/main/resources/static/fonts/Flat-UI-Icons.woff -------------------------------------------------------------------------------- /src/main/resources/static/images/img/pop-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Franciswyyy/toutiao/HEAD/src/main/resources/static/images/img/pop-close.png -------------------------------------------------------------------------------- /src/main/resources/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Franciswyyy/toutiao/HEAD/src/main/resources/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/main/resources/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Franciswyyy/toutiao/HEAD/src/main/resources/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/main/resources/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Franciswyyy/toutiao/HEAD/src/main/resources/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/main/resources/static/images/res/app-iphone-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Franciswyyy/toutiao/HEAD/src/main/resources/static/images/res/app-iphone-frame.png -------------------------------------------------------------------------------- /src/main/resources/toolbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | date 4 | application 5 | org.apache.velocity.tools.generic.DateTool 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/model/EntityType.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.model; 2 | 3 | 4 | public class EntityType { 5 | public static int ENTITY_NEWS = 1; 6 | public static int ENTITY_COMMENT = 2; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/async/EventHandler.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.async; 2 | 3 | import java.util.List; 4 | 5 | public interface EventHandler { 6 | 7 | void doHandle(EventModel model); 8 | List getSupportEventTypes(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/ToutiaoApplication.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ToutiaoApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ToutiaoApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/async/EventType.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.async; 2 | 3 | 4 | public enum EventType { 5 | LIKE(0), 6 | COMMENT(1), 7 | LOGIN(2), 8 | MAIL(3); 9 | 10 | private int value; 11 | 12 | EventType(int value) { 13 | this.value = value; 14 | } 15 | 16 | public int getValue() { 17 | return value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/base/util.js: -------------------------------------------------------------------------------- 1 | (function (window, undefined) { 2 | var Util = Base.createClass('main.base.Util'); 3 | $.extend(Util, { 4 | isEmail: fIsEmail 5 | }); 6 | 7 | function fIsEmail(sEmail) { 8 | sEmail = $.trim(sEmail); 9 | return sEmail && /^([a-zA-Z0-9_\.\-\+])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/.test(sEmail); 10 | } 11 | })(window); -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:mysql://localhost:3306/toutiao?useUnicode=true&characterEncoding=utf8&useSSL=false 2 | spring.datasource.username=root 3 | spring.datasource.password=root 4 | mybatis.config-location=classpath:mybatis-config.xml 5 | #logging.level.root=DEBUG 6 | spring.velocity.suffix=.html 7 | #Velocity默认的后缀是vm 8 | spring.velocity.cache=false 9 | spring.velocity.toolbox-config-location=toolbox.xml -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/model/ViewObject.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.model; 2 | 3 | 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | //视图展示的一个对象 8 | public class ViewObject { 9 | private Map objs = new HashMap(); 10 | public void set(String key, Object value) { 11 | objs.put(key, value); 12 | } 13 | 14 | public Object get(String key) { 15 | return objs.get(key); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/controller/SettingController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.ResponseBody; 6 | 7 | 8 | //目的,要登陆之后才能登这个页面,否则跳转 9 | @Controller 10 | public class SettingController { 11 | @RequestMapping("/setting") 12 | @ResponseBody 13 | public String setting() { 14 | return "Setting:OK"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/model/HostHolder.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.model; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | @Component 6 | public class HostHolder { 7 | //用来存用户的,每个线程存自己的, 线程本地变量。 8 | private static ThreadLocal users = new ThreadLocal(); 9 | 10 | public User getUser() { 11 | return users.get(); 12 | } 13 | 14 | public void setUser(User user) { 15 | users.set(user); 16 | } 17 | 18 | public void clear() { 19 | users.remove();; 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/site/test.js: -------------------------------------------------------------------------------- 1 | (function (window, undefined) { 2 | var PopupLogin = Base.getClass('main.component.PopupLogin'); 3 | 4 | Base.ready({ 5 | initialize: fInitialize 6 | }); 7 | 8 | function fInitialize() { 9 | PopupLogin.show({ 10 | listeners: { 11 | login: function () { 12 | alert('登录'); 13 | }, 14 | register: function () { 15 | alert('注册'); 16 | } 17 | } 18 | }); 19 | } 20 | 21 | })(window); -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/ToutiaoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.SpringApplicationConfiguration; 6 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 7 | import org.springframework.test.context.web.WebAppConfiguration; 8 | 9 | @RunWith(SpringJUnit4ClassRunner.class) 10 | @SpringApplicationConfiguration(classes = ToutiaoApplication.class) 11 | @WebAppConfiguration 12 | public class ToutiaoApplicationTests { 13 | 14 | @Test 15 | public void contextLoads() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/com/nowcoder/dao/NewsDao.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | news 6 | id,title, link, image, like_count, comment_count,created_date,user_id 7 | 8 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/async/EventProducer.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.async; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.nowcoder.util.JedisAdapter; 5 | import com.nowcoder.util.RedisKeyUtil; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | public class EventProducer { 11 | @Autowired 12 | JedisAdapter jedisAdapter; 13 | 14 | public boolean fireEvent(EventModel model) { 15 | try { 16 | String json = JSONObject.toJSONString(model); 17 | String key = RedisKeyUtil.getEventQueueKey(); 18 | jedisAdapter.lpush(key, json); 19 | return true; 20 | } catch (Exception e) { 21 | return false; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/util/RedisKeyUtil.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.util; 2 | 3 | 4 | // 字段的规范 5 | public class RedisKeyUtil { 6 | private static String SPLIT = ":"; 7 | private static String BIZ_LIKE = "LIKE"; 8 | private static String BIZ_DISLIKE = "DISLIKE"; 9 | private static String BIZ_EVENT = "EVENT"; 10 | 11 | public static String getEventQueueKey() { 12 | return BIZ_EVENT; 13 | } 14 | 15 | public static String getLikeKey(int entityId, int entityType) { 16 | return BIZ_LIKE + SPLIT + String.valueOf(entityType) + SPLIT + String.valueOf(entityId); 17 | } 18 | 19 | public static String getDisLikeKey(int entityId, int entityType) { 20 | return BIZ_DISLIKE + SPLIT + String.valueOf(entityType) + SPLIT + String.valueOf(entityId); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.service; 2 | 3 | import com.nowcoder.dao.CommentDao; 4 | import com.nowcoder.model.Comment; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.List; 11 | 12 | 13 | @Service 14 | public class CommentService { 15 | 16 | 17 | @Autowired 18 | CommentDao commentDao; 19 | 20 | public List getCommentsByEntity(int entityId, int entityType) { 21 | return commentDao.selectByEntity(entityId, entityType); 22 | } 23 | 24 | public int addComment(Comment comment) { 25 | return commentDao.addComment(comment); 26 | } 27 | 28 | public int getCommentCount(int entityId, int entityType) { 29 | return commentDao.getCommentCount(entityId, entityType); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/configuration/ToutiaoWebConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.configuration; 2 | 3 | import com.nowcoder.interceptor.LoginRequiredInterceptor; 4 | import com.nowcoder.interceptor.PassportInterceptor; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 8 | 9 | public class ToutiaoWebConfiguration extends WebMvcConfigurerAdapter { 10 | @Autowired 11 | PassportInterceptor passportInterceptor; 12 | 13 | @Autowired 14 | LoginRequiredInterceptor loginRequiredInterceptor; 15 | 16 | @Override 17 | public void addInterceptors(InterceptorRegistry registry) { 18 | registry.addInterceptor(passportInterceptor); 19 | registry.addInterceptor(loginRequiredInterceptor).addPathPatterns("/setting*"); //只对这个页面有效 20 | super.addInterceptors(registry); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/dao/LoginTicketDao.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.dao; 2 | 3 | import com.nowcoder.model.LoginTicket; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | @Mapper 7 | public interface LoginTicketDao { 8 | //ticket的操作 1.登录,则插入一个ticket 2.浏览,则查看ticket 3.更新状态,登出的时候 9 | String TABLE_NAME = "login_ticket"; 10 | String INSERT_FIELDS = " user_id, expired, status, ticket "; 11 | String SELECT_FIELDS = " id, " + INSERT_FIELDS; 12 | 13 | @Insert({"insert into ", TABLE_NAME, "(", INSERT_FIELDS, 14 | ") values (#{userId},#{expired},#{status},#{ticket})"}) 15 | int addTicket(LoginTicket ticket); 16 | 17 | 18 | @Select({"select ", SELECT_FIELDS, " from ", TABLE_NAME, " where ticket=#{ticket}"}) 19 | LoginTicket selectByTicket(String ticket); 20 | 21 | //指定两个参数 22 | @Update({"update ", TABLE_NAME, " set status=#{status} where ticket=#{ticket}"}) 23 | void updateStatus(@Param("ticket") String ticket, @Param("status") int status); 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/dao/UserDao.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.dao; 2 | 3 | import com.nowcoder.model.User; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | @Mapper 7 | public interface UserDao { 8 | //定义变量,为了之后的sql语句好写 9 | String TABLE_NAME = "user"; 10 | String INSET_FIELDS = " name, password, salt, head_url "; 11 | String SELECT_FIELDS = " id, name, password, salt, head_url"; 12 | 13 | @Insert({"insert into ", TABLE_NAME, "(", INSET_FIELDS, 14 | ") values (#{name},#{password},#{salt},#{headUrl})"}) 15 | //#会自己去从user里面去找 16 | int addUser(User user); 17 | 18 | 19 | @Select({"select ", SELECT_FIELDS, " from ", TABLE_NAME, " where id=#{id}"}) 20 | User selectById(int id); 21 | 22 | @Select({"select ", SELECT_FIELDS, " from ", TABLE_NAME, " where name=#{name}"}) 23 | User selectByName(String name); 24 | 25 | @Update({"update ", TABLE_NAME, " set password=#{password} where id=#{id}"}) 26 | void updatePassword(User user); 27 | 28 | @Delete({"delete from ", TABLE_NAME, " where id=#{id}"}) 29 | void deleteById(int id); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/templates/news.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 |
 4 | Hello VM.
 5 | 
 6 | ## 你看不到我
 7 | 
 8 | #*
 9 | 这里都看不到
10 | *#
11 | 
12 | value1:$!{value1}
13 | $!{value2}
14 | ${value3}
15 | 
16 | #foreach ($color in $colors)
17 | Color $!{foreach.index}/$!{foreach.count}: $!{color}
18 | #end
19 | 
20 | #foreach($key in $map.keySet())
21 | Number $!{foreach.index}/$!{foreach.count}: $!{key} $map.get($key)
22 | #end
23 | 
24 | #foreach($kv in $map.entrySet())
25 | Number $!{foreach.index}/$!{foreach.count}: $!{kv.key} $!{kv.value}
26 | #end
27 | 
28 | 
29 | User:$!{user.name}
30 | User:$!{user.getName()}
31 | 
32 | #set($title = "nowcoder")
33 | Include: #include("header.vm") 
34 | Parse:#parse("header.vm") 35 | 36 | #macro (render_color, $color, $index) 37 | Color By Macro $index, $color 38 | #end 39 | 40 | #foreach ($color in $colors) 41 | #render_color($color, $foreach.index) 42 | #end 43 | 44 | #set($hello = "hello") 45 | #set($hworld1 = "$!{hello} world") 46 | #set($hworld2 = '$!{hello} world') 47 | 48 | hworld1:$hworld1 49 | hworld2:$hworld2 50 | 51 | $!{colors.size()} 52 | 53 |
54 | 55 | -------------------------------------------------------------------------------- /src/main/resources/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/model/LoginTicket.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.model; 2 | 3 | import java.util.Date; 4 | 5 | public class LoginTicket { 6 | private int id; 7 | private int userId; 8 | private Date expired; //生存时间 9 | private int status;// 0有效,1无效 10 | private String ticket; //生成的ticket 11 | 12 | public String getTicket() { 13 | return ticket; 14 | } 15 | 16 | public void setTicket(String ticket) { 17 | this.ticket = ticket; 18 | } 19 | 20 | public int getId() { 21 | return id; 22 | } 23 | 24 | public void setId(int id) { 25 | this.id = id; 26 | } 27 | 28 | public int getUserId() { 29 | return userId; 30 | } 31 | 32 | public void setUserId(int userId) { 33 | this.userId = userId; 34 | } 35 | 36 | public Date getExpired() { 37 | return expired; 38 | } 39 | 40 | public void setExpired(Date expired) { 41 | this.expired = expired; 42 | } 43 | 44 | public int getStatus() { 45 | return status; 46 | } 47 | 48 | public void setStatus(int status) { 49 | this.status = status; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/JedisTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder; 2 | 3 | import com.nowcoder.model.User; 4 | import com.nowcoder.util.JedisAdapter; 5 | import org.apache.commons.lang.builder.ToStringBuilder; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.SpringApplicationConfiguration; 10 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 11 | 12 | @RunWith(SpringJUnit4ClassRunner.class) 13 | @SpringApplicationConfiguration(classes = ToutiaoApplication.class) 14 | public class JedisTests { 15 | @Autowired 16 | JedisAdapter jedisAdapter; 17 | 18 | @Test 19 | public void testObject() { 20 | User user = new User(); 21 | user.setHeadUrl("http://image.nowcoder.com/head/100t.png"); 22 | user.setName("user1"); 23 | user.setPassword("pwd");; 24 | user.setSalt("salt"); 25 | 26 | jedisAdapter.setObject("user1xx", user); 27 | 28 | User u = jedisAdapter.getObject("user1xx", User.class); 29 | 30 | System.out.println(ToStringBuilder.reflectionToString(u)); 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.service; 2 | 3 | import com.nowcoder.dao.MessageDao; 4 | import com.nowcoder.model.Message; 5 | import org.apache.ibatis.annotations.Param; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.List; 10 | 11 | 12 | @Service 13 | public class MessageService { 14 | @Autowired 15 | private MessageDao messageDao; 16 | 17 | public int addMessage(Message message) { 18 | return messageDao.addMessage(message); 19 | } 20 | 21 | public List getConversationList(int userId, int offset, int limit) { 22 | // conversation的总条数存在id里 23 | return messageDao.getConversationList(userId, offset, limit); 24 | } 25 | 26 | public List getConversationDetail(String conversationId, int offset, int limit) { 27 | // conversation的总条数存在id里 28 | return messageDao.getConversationDetail(conversationId, offset, limit); 29 | } 30 | 31 | public int getUnreadCount(int userId, String conversationId) { 32 | return messageDao.getConversationUnReadCount(userId, conversationId); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/dao/CommentDao.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.dao; 2 | 3 | import com.nowcoder.model.Comment; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * 评论的 10 | * 增加评论 11 | * 评论看一下实体 12 | * 实体总共多少条 13 | */ 14 | @Mapper 15 | public interface CommentDao { 16 | String TABLE_NAME = " comment "; 17 | String INSERT_FIELDS = " user_id, content, created_date, entity_id, entity_type, status "; 18 | String SELECT_FIELDS = " id, " + INSERT_FIELDS; 19 | 20 | //返回int看成功与否 21 | @Insert({"insert into ", TABLE_NAME, "(", INSERT_FIELDS, 22 | ") values (#{userId},#{content},#{createdDate},#{entityId},#{entityType},#{status})"}) 23 | int addComment(Comment comment); 24 | 25 | @Select({"select ", SELECT_FIELDS, " from ", TABLE_NAME, " where entity_type=#{entityType} and entity_id=#{entityId} order by id desc "}) 26 | List selectByEntity(@Param("entityId") int entityId, @Param("entityType") int entityType); 27 | 28 | @Select({"select count(id) from ", TABLE_NAME, " where entity_type=#{entityType} and entity_id=#{entityId}"}) 29 | int getCommentCount(@Param("entityId") int entityId, @Param("entityType") int entityType); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/model/User.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.model; 2 | 3 | 4 | public class User { 5 | private int id; 6 | private String name; 7 | private String password; 8 | private String salt; 9 | private String headUrl; 10 | 11 | public User() { 12 | 13 | } 14 | public User(String name) { 15 | this.name = name; 16 | this.password = ""; 17 | this.salt = ""; 18 | this.headUrl = ""; 19 | } 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | public void setName(String name) { 26 | this.name = name; 27 | } 28 | 29 | public String getPassword() { 30 | return password; 31 | } 32 | 33 | public void setPassword(String password) { 34 | this.password = password; 35 | } 36 | 37 | public String getSalt() { 38 | return salt; 39 | } 40 | 41 | public void setSalt(String salt) { 42 | this.salt = salt; 43 | } 44 | 45 | public String getHeadUrl() { 46 | return headUrl; 47 | } 48 | 49 | public void setHeadUrl(String headUrl) { 50 | this.headUrl = headUrl; 51 | } 52 | 53 | public int getId() { 54 | return id; 55 | } 56 | 57 | public void setId(int id) { 58 | this.id = id; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/dao/NewsDao.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.dao; 2 | 3 | import com.nowcoder.model.News; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | import java.util.List; 7 | 8 | @Mapper 9 | public interface NewsDao { 10 | String TABLE_NAME = "news"; 11 | String INSERT_FIELDS = " title, link, image, like_count, comment_count, created_date, user_id "; 12 | String SELECT_FIELDS = " id, " + INSERT_FIELDS; 13 | 14 | @Insert({"insert into ", TABLE_NAME, "(", INSERT_FIELDS, 15 | ") values (#{title},#{link},#{image},#{likeCount},#{commentCount},#{createdDate},#{userId})"}) 16 | int addNews(News news); 17 | 18 | @Select({"select ", SELECT_FIELDS , " from ", TABLE_NAME, " where id=#{id}"}) 19 | News getById(int id); 20 | 21 | //咨询是一批批的,所有可能要用到分页, 通过xml来写sql语句,同样的包名建立xml文件 22 | List selectByUserIdAndOffset(@Param("userId") int userId, @Param("offset") int offset, 23 | @Param("limit") int limit); 24 | 25 | @Update({"update ", TABLE_NAME, " set comment_count = #{commentCount} where id=#{id}"}) 26 | int updateCommentCount(@Param("id") int id, @Param("commentCount") int commentCount); 27 | 28 | @Update({"update ", TABLE_NAME, " set like_count = #{likeCount} where id=#{id}"}) 29 | int updateLikeCount(@Param("id") int id, @Param("likeCount") int likeCount); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/async/handler/LikeHandler.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.async.handler; 2 | 3 | import com.nowcoder.async.EventHandler; 4 | import com.nowcoder.async.EventModel; 5 | import com.nowcoder.async.EventType; 6 | import com.nowcoder.model.Message; 7 | import com.nowcoder.model.User; 8 | import com.nowcoder.service.MessageService; 9 | import com.nowcoder.service.UserService; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.Arrays; 14 | import java.util.Date; 15 | import java.util.List; 16 | 17 | 18 | @Component 19 | public class LikeHandler implements EventHandler { 20 | @Autowired 21 | MessageService messageService; 22 | 23 | @Autowired 24 | UserService userService; 25 | 26 | @Override 27 | public void doHandle(EventModel model) { 28 | Message message = new Message(); 29 | message.setFromId(3); 30 | message.setToId(model.getEntityOwnerId()); 31 | //message.setToId(model.getActorId()); 32 | User user = userService.getUser(model.getActorId()); 33 | message.setContent("用户" + user.getName() 34 | + "赞了你的资讯,http://127.0.0.1:8080/news/" + model.getEntityId()); 35 | message.setCreatedDate(new Date()); 36 | messageService.addMessage(message); 37 | } 38 | 39 | @Override 40 | public List getSupportEventTypes() { 41 | return Arrays.asList(EventType.LIKE); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/async/handler/LoginExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.async.handler; 2 | 3 | import com.nowcoder.async.EventHandler; 4 | import com.nowcoder.async.EventModel; 5 | import com.nowcoder.async.EventType; 6 | import com.nowcoder.model.Message; 7 | import com.nowcoder.service.MessageService; 8 | import com.nowcoder.util.MailSender; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.*; 13 | 14 | 15 | @Component 16 | public class LoginExceptionHandler implements EventHandler { 17 | @Autowired 18 | MessageService messageService; 19 | 20 | @Autowired 21 | MailSender mailSender; 22 | 23 | 24 | @Override 25 | public void doHandle(EventModel model) { 26 | // 判断是否有异常登陆 27 | Message message = new Message(); 28 | message.setToId(model.getActorId()); 29 | message.setContent("你上次的登陆ip异常"); 30 | message.setFromId(3); 31 | message.setCreatedDate(new Date()); 32 | messageService.addMessage(message); 33 | 34 | 35 | Map map = new HashMap(); 36 | map.put("username", model.getExt("username")); 37 | mailSender.sendWithHTMLTemplate(model.getExt("email"), "登陆异常", "mails/welcome.html", 38 | map); 39 | } 40 | 41 | @Override 42 | public List getSupportEventTypes() { 43 | return Arrays.asList(EventType.LOGIN); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/interceptor/LoginRequiredInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.interceptor; 2 | 3 | 4 | import com.nowcoder.model.HostHolder; 5 | import com.nowcoder.model.LoginTicket; 6 | import com.nowcoder.model.User; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.servlet.HandlerInterceptor; 10 | import org.springframework.web.servlet.ModelAndView; 11 | 12 | import javax.servlet.http.Cookie; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.util.Date; 16 | 17 | 18 | @Component 19 | public class LoginRequiredInterceptor implements HandlerInterceptor { 20 | 21 | @Autowired 22 | private HostHolder hostHolder; 23 | 24 | @Override 25 | public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { 26 | if (hostHolder.getUser() == null) { 27 | httpServletResponse.sendRedirect("/?pop=1"); 28 | return false; 29 | } 30 | return true; 31 | } 32 | 33 | @Override 34 | public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { 35 | } 36 | 37 | @Override 38 | public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/model/Comment.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.model; 2 | 3 | import java.util.Date; 4 | 5 | //评论实体类 6 | public class Comment { 7 | private int id; 8 | private int userId; 9 | private int entityId; 10 | private int entityType; 11 | private String content; 12 | private Date createdDate; 13 | private int status; 14 | 15 | public int getId() { 16 | return id; 17 | } 18 | 19 | public void setId(int id) { 20 | this.id = id; 21 | } 22 | 23 | public int getUserId() { 24 | return userId; 25 | } 26 | 27 | public void setUserId(int userId) { 28 | this.userId = userId; 29 | } 30 | 31 | public int getEntityId() { 32 | return entityId; 33 | } 34 | 35 | public void setEntityId(int entityId) { 36 | this.entityId = entityId; 37 | } 38 | 39 | public int getEntityType() { 40 | return entityType; 41 | } 42 | 43 | public void setEntityType(int entityType) { 44 | this.entityType = entityType; 45 | } 46 | 47 | public String getContent() { 48 | return content; 49 | } 50 | 51 | public void setContent(String content) { 52 | this.content = content; 53 | } 54 | 55 | public Date getCreatedDate() { 56 | return createdDate; 57 | } 58 | 59 | public void setCreatedDate(Date createdDate) { 60 | this.createdDate = createdDate; 61 | } 62 | 63 | public int getStatus() { 64 | return status; 65 | } 66 | 67 | public void setStatus(int status) { 68 | this.status = status; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/model/Message.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.model; 2 | 3 | import java.util.Date; 4 | 5 | 6 | public class Message { 7 | private int id; 8 | private int fromId; 9 | private int toId; 10 | private String content; 11 | private Date createdDate; 12 | private int hasRead; 13 | private String conversationId; 14 | 15 | public int getId() { 16 | return id; 17 | } 18 | 19 | public void setId(int id) { 20 | this.id = id; 21 | } 22 | 23 | public int getFromId() { 24 | return fromId; 25 | } 26 | 27 | public void setFromId(int fromId) { 28 | this.fromId = fromId; 29 | } 30 | 31 | public int getToId() { 32 | return toId; 33 | } 34 | 35 | public void setToId(int toId) { 36 | this.toId = toId; 37 | } 38 | 39 | public String getContent() { 40 | return content; 41 | } 42 | 43 | public void setContent(String content) { 44 | this.content = content; 45 | } 46 | 47 | public Date getCreatedDate() { 48 | return createdDate; 49 | } 50 | 51 | public void setCreatedDate(Date createdDate) { 52 | this.createdDate = createdDate; 53 | } 54 | 55 | public int getHasRead() { 56 | return hasRead; 57 | } 58 | 59 | public void setHasRead(int hasRead) { 60 | this.hasRead = hasRead; 61 | } 62 | 63 | public String getConversationId() { 64 | return conversationId; 65 | } 66 | 67 | public void setConversationId(String conversationId) { 68 | this.conversationId = conversationId; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/resources/templates/footer.html: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | 14 | 15 | 16 |

牛客网

17 |

程序员的首选学习分享平台

18 | 19 |
20 | 21 | 38 |
39 | 40 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/model/News.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.model; 2 | 3 | import java.util.Date; 4 | 5 | //咨询 6 | public class News { 7 | 8 | private int id; 9 | 10 | private String title; 11 | 12 | private String link; 13 | 14 | private String image; 15 | 16 | private int likeCount; 17 | 18 | private int commentCount; 19 | 20 | private Date createdDate; 21 | 22 | private int userId; 23 | 24 | public int getId() { 25 | return id; 26 | } 27 | 28 | public void setId(int id) { 29 | this.id = id; 30 | } 31 | 32 | public String getTitle() { 33 | return title; 34 | } 35 | 36 | public void setTitle(String title) { 37 | this.title = title; 38 | } 39 | 40 | public String getLink() { 41 | return link; 42 | } 43 | 44 | public void setLink(String link) { 45 | this.link = link; 46 | } 47 | 48 | public String getImage() { 49 | return image; 50 | } 51 | 52 | public void setImage(String image) { 53 | this.image = image; 54 | } 55 | 56 | public int getLikeCount() { 57 | return likeCount; 58 | } 59 | 60 | public void setLikeCount(int likeCount) { 61 | this.likeCount = likeCount; 62 | } 63 | 64 | public int getCommentCount() { 65 | return commentCount; 66 | } 67 | 68 | public void setCommentCount(int commentCount) { 69 | this.commentCount = commentCount; 70 | } 71 | 72 | public Date getCreatedDate() { 73 | return createdDate; 74 | } 75 | 76 | public void setCreatedDate(Date createdDate) { 77 | this.createdDate = createdDate; 78 | } 79 | 80 | public int getUserId() { 81 | return userId; 82 | } 83 | 84 | public void setUserId(int userId) { 85 | this.userId = userId; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/base/event.js: -------------------------------------------------------------------------------- 1 | (function (window, undefined) { 2 | var Event = Base.createClass('main.base.Event'); 3 | $.extend(Event, { 4 | on: fOn, 5 | emit: fEmit, 6 | unbind: fUnbind, 7 | unbindAll: fUnbindAll 8 | }); 9 | 10 | function fOn(sName, fCb) { 11 | var that = this; 12 | if (!Base.isString(sName) || !Base.isFunction(fCb)) { 13 | return; 14 | } 15 | 16 | that._cep = that._cep || {}; 17 | that._cep[sName] = that._cep[sName] || []; 18 | that._cep[sName].push(fCb); 19 | } 20 | 21 | function fEmit(sName) { 22 | var that = this; 23 | if (!that._cep || !that._cep[sName]) { 24 | return; 25 | } 26 | 27 | var aArg = [].slice.call(arguments, 1); 28 | $.each(that._cep[sName], function (_, fCb) { 29 | fCb.apply(that, aArg); 30 | }); 31 | } 32 | 33 | function fUnbind(sName, fCb) { 34 | var that = this; 35 | if (!that._cep || !that._cep[sName]) { 36 | return; 37 | } 38 | 39 | if (!fCb) { 40 | that._cep[sName].length = 0; 41 | delete that._cep[sName]; 42 | return; 43 | } 44 | 45 | var oPoll = that._cep; 46 | var aCb = oPoll[sName]; 47 | for (var i = aCb.length - 1; i >= 0; i--) { 48 | if (aCb[i] === fCb) { 49 | aCb.splice(i, 1); 50 | } 51 | } 52 | aCb.length === 0 && (delete oPoll[sName]); 53 | } 54 | 55 | function fUnbindAll() { 56 | var that = this; 57 | var oPoll = that._cep; 58 | $.each(oPoll, function (sKey) { 59 | delete oPoll[sKey]; 60 | }); 61 | } 62 | })(window); -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/dao/MessageDao.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.dao; 2 | 3 | import com.nowcoder.model.Comment; 4 | import com.nowcoder.model.Message; 5 | import org.apache.ibatis.annotations.Insert; 6 | import org.apache.ibatis.annotations.Mapper; 7 | import org.apache.ibatis.annotations.Param; 8 | import org.apache.ibatis.annotations.Select; 9 | 10 | import java.util.List; 11 | 12 | 13 | @Mapper 14 | public interface MessageDao { 15 | String TABLE_NAME = " message "; 16 | String INSERT_FIELDS = " from_id, to_id, content, has_read, conversation_id, created_date "; 17 | String SELECT_FIELDS = " id, " + INSERT_FIELDS; 18 | 19 | @Insert({"insert into ", TABLE_NAME, "(", INSERT_FIELDS, 20 | ") values (#{fromId},#{toId},#{content},#{hasRead},#{conversationId},#{createdDate})"}) 21 | int addMessage(Message message); 22 | 23 | @Select({"select ", INSERT_FIELDS, " ,count(id) as id from ( select * from ", TABLE_NAME, " where from_id=#{userId} or to_id=#{userId} order by id desc) tt group by conversation_id order by id desc limit #{offset},#{limit}"}) 24 | List getConversationList(@Param("userId") int userId, @Param("offset") int offset, @Param("limit") int limit); 25 | 26 | @Select({"select count(id) from ", TABLE_NAME, " where has_read = 0 and to_id=#{userId} and conversation_id=#{conversationId}"}) 27 | int getConversationUnReadCount(@Param("userId") int userId, @Param("conversationId") String conversationId); 28 | 29 | @Select({"select count(id) from ", TABLE_NAME, " where has_read = 0 and to_id=#{userId}"}) 30 | int getConversationTotalCount(@Param("userId") int userId, @Param("conversationId") String conversationId); 31 | 32 | @Select({"select ", SELECT_FIELDS, " from ", TABLE_NAME, " where conversation_id=#{conversationId} order by id desc limit #{offset},#{limit}"}) 33 | List getConversationDetail(@Param("conversationId") String conversationId, @Param("offset") int offset, @Param("limit") int limit); 34 | } -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/service/LikeService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.service; 2 | 3 | import com.nowcoder.util.JedisAdapter; 4 | import com.nowcoder.util.RedisKeyUtil; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | public class LikeService { 10 | 11 | @Autowired 12 | JedisAdapter jedisAdapter; 13 | 14 | 15 | 16 | /** 17 | * 判断该用户是否喜欢,可以用在资讯或者评论,通用话的设计。 18 | * 如果喜欢返回1,不喜欢返回-1,否则返回0 19 | * @param userId 20 | * @param entityType 21 | * @param entityId 22 | * @return 23 | */ 24 | public int getLikeStatus(int userId, int entityType, int entityId){ 25 | String likeKey = RedisKeyUtil.getLikeKey(entityId, entityType); 26 | if(jedisAdapter.sismember(likeKey, String.valueOf(userId))) { 27 | return 1; 28 | } 29 | String disLikeKey = RedisKeyUtil.getDisLikeKey(entityId, entityType); 30 | return jedisAdapter.sismember(disLikeKey, String.valueOf(userId)) ? -1 : 0; 31 | } 32 | 33 | public long like(int userId, int entityType, int entityId) { 34 | // 在喜欢集合里增加 35 | String likeKey = RedisKeyUtil.getLikeKey(entityId, entityType); 36 | jedisAdapter.sadd(likeKey, String.valueOf(userId)); 37 | // 从反对里删除 38 | String disLikeKey = RedisKeyUtil.getDisLikeKey(entityId, entityType); 39 | jedisAdapter.srem(disLikeKey, String.valueOf(userId)); 40 | return jedisAdapter.scard(likeKey); 41 | } 42 | 43 | public long disLike(int userId, int entityType, int entityId) { 44 | // 在反对集合里增加 45 | String disLikeKey = RedisKeyUtil.getDisLikeKey(entityId, entityType); 46 | jedisAdapter.sadd(disLikeKey, String.valueOf(userId)); 47 | // 从喜欢里删除 48 | String likeKey = RedisKeyUtil.getLikeKey(entityId, entityType); 49 | jedisAdapter.srem(likeKey, String.valueOf(userId)); 50 | return jedisAdapter.scard(likeKey); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/async/EventModel.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.async; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class EventModel { 7 | private EventType type; 8 | private int actorId; 9 | private int entityType; 10 | private int entityId; 11 | private int entityOwnerId; 12 | private Map exts = new HashMap(); 13 | 14 | public String getExt(String key) { 15 | return exts.get(key); 16 | } 17 | 18 | public EventModel setExt(String key, String value) { 19 | exts.put(key, value); 20 | return this; 21 | } 22 | 23 | public EventModel(EventType type) { 24 | this.type = type; 25 | } 26 | 27 | public EventModel() { 28 | 29 | } 30 | 31 | public EventType getType() { 32 | return type; 33 | } 34 | 35 | public EventModel setType(EventType type) { 36 | this.type = type; 37 | return this; 38 | } 39 | 40 | public int getActorId() { 41 | return actorId; 42 | } 43 | 44 | public EventModel setActorId(int actorId) { 45 | this.actorId = actorId; 46 | return this; 47 | } 48 | 49 | public int getEntityType() { 50 | return entityType; 51 | } 52 | 53 | public EventModel setEntityType(int entityType) { 54 | this.entityType = entityType; 55 | return this; 56 | } 57 | 58 | public int getEntityId() { 59 | return entityId; 60 | } 61 | 62 | public EventModel setEntityId(int entityId) { 63 | this.entityId = entityId; 64 | return this; 65 | } 66 | 67 | public int getEntityOwnerId() { 68 | return entityOwnerId; 69 | } 70 | 71 | public EventModel setEntityOwnerId(int entityOwnerId) { 72 | this.entityOwnerId = entityOwnerId; 73 | return this; 74 | } 75 | 76 | public Map getExts() { 77 | return exts; 78 | } 79 | 80 | public void setExts(Map exts) { 81 | this.exts = exts; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/resources/init-schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `user`; 2 | CREATE TABLE `user` ( 3 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 4 | `name` varchar(64) NOT NULL DEFAULT '', 5 | `password` varchar(128) NOT NULL DEFAULT '', 6 | `salt` varchar(32) NOT NULL DEFAULT '', 7 | `head_url` varchar(256) NOT NULL DEFAULT '', 8 | PRIMARY KEY (`id`), 9 | UNIQUE KEY `name` (`name`) 10 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 11 | 12 | DROP TABLE IF EXISTS `news`; 13 | CREATE TABLE `news` ( 14 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 15 | `title` varchar(128) NOT NULL DEFAULT '', 16 | `link` varchar(256) NOT NULL DEFAULT '', 17 | `image` varchar(256) NOT NULL DEFAULT '', 18 | `like_count` int NOT NULL, 19 | `comment_count` int NOT NULL, 20 | `created_date` datetime NOT NULL, 21 | `user_id` int(11) NOT NULL, 22 | PRIMARY KEY (`id`) 23 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 24 | 25 | DROP TABLE IF EXISTS `login_ticket`; 26 | CREATE TABLE `login_ticket` ( 27 | `id` INT NOT NULL AUTO_INCREMENT, 28 | `user_id` INT NOT NULL, 29 | `ticket` VARCHAR(45) NOT NULL, 30 | `expired` DATETIME NOT NULL, 31 | `status` INT NULL DEFAULT 0, 32 | PRIMARY KEY (`id`), 33 | UNIQUE INDEX `ticket_UNIQUE` (`ticket` ASC)); 34 | 35 | DROP TABLE IF EXISTS `comment`; 36 | CREATE TABLE `comment` ( 37 | `id` INT NOT NULL AUTO_INCREMENT, 38 | `content` TEXT NOT NULL, 39 | `user_id` INT NOT NULL, 40 | `entity_id` INT NOT NULL, 41 | `entity_type` INT NOT NULL, 42 | `created_date` DATETIME NOT NULL, 43 | `status` INT NOT NULL DEFAULT 0, 44 | PRIMARY KEY (`id`), 45 | INDEX `entity_index` (`entity_id` ASC, `entity_type` ASC) 46 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 47 | 48 | DROP TABLE IF EXISTS `message`; 49 | CREATE TABLE `message` ( 50 | `id` INT NOT NULL AUTO_INCREMENT, 51 | `from_id` INT NULL, 52 | `to_id` INT NULL, 53 | `content` TEXT NULL, 54 | `created_date` DATETIME NULL, 55 | `has_read` INT NULL, 56 | `conversation_id` VARCHAR(45) NOT NULL, 57 | PRIMARY KEY (`id`), 58 | INDEX `conversation_index` (`conversation_id` ASC), 59 | INDEX `created_date` (`created_date` ASC)) 60 | ENGINE = InnoDB 61 | DEFAULT CHARACTER SET = utf8; -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/service/NewsService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.service; 2 | 3 | import com.nowcoder.dao.NewsDao; 4 | import com.nowcoder.model.News; 5 | import com.nowcoder.util.ToutiaoUtil; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.multipart.MultipartFile; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.nio.file.Files; 13 | import java.nio.file.StandardCopyOption; 14 | import java.util.List; 15 | import java.util.UUID; 16 | 17 | 18 | 19 | @Service 20 | public class NewsService { 21 | @Autowired 22 | private NewsDao newsDao; 23 | 24 | 25 | public List getLatestNews(int userId, int offset, int limit) { 26 | return newsDao.selectByUserIdAndOffset(userId, offset, limit); 27 | } 28 | 29 | public int addNews(News news) { 30 | newsDao.addNews(news); 31 | return news.getId(); 32 | } 33 | 34 | public News getById(int newsId) { 35 | return newsDao.getById(newsId); 36 | } 37 | 38 | 39 | public String saveImage(MultipartFile file) throws IOException { 40 | // xxx.xxx.jpg 41 | int dotPos = file.getOriginalFilename().lastIndexOf("."); 42 | if (dotPos < 0) { 43 | return null; 44 | } 45 | String fileExt = file.getOriginalFilename().substring(dotPos + 1).toLowerCase(); 46 | if (!ToutiaoUtil.isFileAllowed(fileExt)) { 47 | return null; 48 | } 49 | 50 | //文件名全部重新命名,加上最后的扩展名 51 | String fileName = UUID.randomUUID().toString().replaceAll("-", "") + "." + fileExt; 52 | Files.copy(file.getInputStream(), new File(ToutiaoUtil.IMAGE_DIR + fileName).toPath(), 53 | StandardCopyOption.REPLACE_EXISTING); 54 | 55 | 56 | return ToutiaoUtil.TOUTIAO_DOMAIN + "image?name=" + fileName; 57 | } 58 | 59 | public int updateCommentCount(int id, int count) { 60 | return newsDao.updateCommentCount(id, count); 61 | } 62 | 63 | public int updateLikeCount(int id, int count) { 64 | return newsDao.updateLikeCount(id, count); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/util/action.js: -------------------------------------------------------------------------------- 1 | (function (window, undefined) { 2 | var Action = Base.createClass('main.util.Action'); 3 | $.extend(Action, { 4 | like: fLike, 5 | dislike: fDislike, 6 | post: fPost 7 | }); 8 | 9 | /** 10 | * 喜欢 11 | * @param {Object} oConf 12 | * @param {String} oConf.newsId 对象id 13 | * @param {Function} oConf.call 成功回调 14 | * @param {Function} oConf.error 失败回调 15 | * @param {Function} oConf.always 操作的回调 16 | */ 17 | function fLike(oConf) { 18 | var that = this; 19 | that.post({ 20 | url: '/like', 21 | data: {newsId: oConf.newsId}, 22 | call: oConf.call, 23 | error: oConf.error, 24 | always: oConf.always 25 | }); 26 | } 27 | 28 | /** 29 | * 不喜欢 30 | * @param {Object} oConf 31 | * @param {String} oConf.newsId 对象id 32 | * @param {Function} oConf.call 成功回调 33 | * @param {Function} oConf.error 失败回调 34 | * @param {Function} oConf.always 操作的回调 35 | */ 36 | function fDislike(oConf) { 37 | var that = this; 38 | that.post({ 39 | url: '/dislike', 40 | data: {newsId: oConf.newsId}, 41 | call: oConf.call, 42 | error: oConf.error, 43 | always: oConf.always 44 | }); 45 | } 46 | 47 | /** 48 | * 简单的 ajax 请求封装 49 | * @param {Object} oConf 50 | * @param {String} oConf.method 请求类型 51 | * @param {String} oConf.url 请求连接 52 | * @param {Object} oConf.data 发送参数 53 | * @param {Function} oConf.call 成功回调 54 | * @param {Function} oConf.error 失败回调 55 | * @param {Function} oConf.always 操作的回调 56 | */ 57 | function fPost(oConf) { 58 | var that = this; 59 | $.ajax({ 60 | method: oConf.method || 'POST', 61 | url: oConf.url, 62 | dataType: 'json', 63 | data: oConf.data 64 | }).done(function (oResult) { 65 | var nCode = oResult.code; 66 | nCode === 0 && oConf.call && oConf.call(oResult); 67 | nCode !== 0 && oConf.error && oConf.error(oResult); 68 | }).fail(oConf.error).always(oConf.always); 69 | } 70 | 71 | 72 | })(window); -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/controller/LikeController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.controller; 2 | 3 | import com.nowcoder.async.EventModel; 4 | import com.nowcoder.async.EventProducer; 5 | import com.nowcoder.async.EventType; 6 | import com.nowcoder.model.EntityType; 7 | import com.nowcoder.model.HostHolder; 8 | import com.nowcoder.model.News; 9 | import com.nowcoder.service.LikeService; 10 | import com.nowcoder.service.NewsService; 11 | import com.nowcoder.util.ToutiaoUtil; 12 | import org.apache.ibatis.annotations.Param; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Controller; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestMethod; 17 | import org.springframework.web.bind.annotation.ResponseBody; 18 | 19 | /** 20 | * Created by nowcoder on 2016/7/13. 21 | */ 22 | @Controller 23 | public class LikeController { 24 | @Autowired 25 | LikeService likeService; 26 | 27 | @Autowired 28 | HostHolder hostHolder; 29 | 30 | @Autowired 31 | NewsService newsService; 32 | 33 | @Autowired 34 | EventProducer eventProducer; 35 | 36 | @RequestMapping(path = {"/like"}, method = {RequestMethod.GET, RequestMethod.POST}) 37 | @ResponseBody 38 | public String like(@Param("newId") int newsId) { 39 | long likeCount = likeService.like(hostHolder.getUser().getId(), EntityType.ENTITY_NEWS, newsId); 40 | // 更新喜欢数 41 | News news = newsService.getById(newsId); 42 | newsService.updateLikeCount(newsId, (int) likeCount); 43 | 44 | eventProducer.fireEvent(new EventModel(EventType.LIKE) 45 | .setActorId(hostHolder.getUser().getId()).setEntityId(newsId) 46 | .setEntityType(EntityType.ENTITY_NEWS).setEntityOwnerId(news.getUserId())); 47 | 48 | 49 | return ToutiaoUtil.getJSONString(0, String.valueOf(likeCount)); 50 | } 51 | 52 | @RequestMapping(path = {"/dislike"}, method = {RequestMethod.GET, RequestMethod.POST}) 53 | @ResponseBody 54 | public String dislike(@Param("newId") int newsId) { 55 | long likeCount = likeService.disLike(hostHolder.getUser().getId(), EntityType.ENTITY_NEWS, newsId); 56 | // 更新喜欢数 57 | newsService.updateLikeCount(newsId, (int) likeCount); 58 | return ToutiaoUtil.getJSONString(0, String.valueOf(likeCount)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/service/QiniuService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.service; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.nowcoder.util.ToutiaoUtil; 5 | import com.qiniu.common.QiniuException; 6 | import com.qiniu.http.Response; 7 | import com.qiniu.storage.UploadManager; 8 | import com.qiniu.util.Auth; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.web.multipart.MultipartFile; 13 | 14 | import java.io.IOException; 15 | import java.util.UUID; 16 | 17 | 18 | // 优点。 可以缩图(实时,在线)。 有对图片基本处理的功能,大头像,小头像 19 | @Service 20 | public class QiniuService { 21 | private static final Logger logger = LoggerFactory.getLogger(QiniuService.class); 22 | //设置好账号的ACCESS_KEY和SECRET_KEY 23 | String ACCESS_KEY = "NPrMRGK9a58EZ8PaEG4QdKHrgAZC10dG4IQ1oZsF"; 24 | String SECRET_KEY = "xTJQza3fbLgYa-xmkWckuVeXiAm1ciMas5VNg1sX"; 25 | //要上传的空间 26 | String bucketname = "nowcoder"; 27 | 28 | //密钥配置 29 | Auth auth = Auth.create(ACCESS_KEY, SECRET_KEY); 30 | //创建上传对象 31 | UploadManager uploadManager = new UploadManager(); 32 | 33 | private static String QINIU_IMAGE_DOMAIN = "http://p6csghz8n.bkt.clouddn.com/"; 34 | 35 | //简单上传,使用默认策略,只需要设置上传的空间名就可以了 36 | public String getUpToken() { 37 | return auth.uploadToken(bucketname); 38 | } 39 | 40 | public String saveImage(MultipartFile file) throws IOException { 41 | try { 42 | int dotPos = file.getOriginalFilename().lastIndexOf("."); 43 | if (dotPos < 0) { 44 | return null; 45 | } 46 | String fileExt = file.getOriginalFilename().substring(dotPos + 1).toLowerCase(); 47 | if (!ToutiaoUtil.isFileAllowed(fileExt)) { 48 | return null; 49 | } 50 | 51 | String fileName = UUID.randomUUID().toString().replaceAll("-", "") + "." + fileExt; 52 | //调用put方法上传 53 | Response res = uploadManager.put(file.getBytes(), fileName, getUpToken()); 54 | //打印返回的信息 55 | if (res.isOK() && res.isJson()) { 56 | return QINIU_IMAGE_DOMAIN + JSONObject.parseObject(res.bodyString()).get("key"); 57 | } else { 58 | logger.error("七牛异常:" + res.bodyString()); 59 | return null; 60 | } 61 | } catch (QiniuException e) { 62 | // 请求失败时打印的异常的信息 63 | logger.error("七牛异常:" + e.getMessage()); 64 | return null; 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/resources/templates/letterDetail.html: -------------------------------------------------------------------------------- 1 | #parse("header.html") 2 |
3 |
4 |
    5 | #foreach($msg in $messages) 6 |
  • 7 | 8 | 头像 9 | 10 |
    11 |
    12 |
    13 |
    14 |

    $date.format('yyyy-MM-dd HH:mm:ss', $!{msg.message.createdDate})

    15 | 删除 16 |
    17 |

    18 | $!{msg.message.content} 19 |

    20 |
    21 |
    22 |
  • 23 | #end 24 |
25 | 26 |
27 | 68 |
69 | #parse("footer.html") -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/base/upload.js: -------------------------------------------------------------------------------- 1 | (function (window, undefined) { 2 | var Upload = Base.createClass('main.base.Upload'); 3 | $.extend(Upload, { 4 | support: fSupport, 5 | uploadFile: fUploadFile, 6 | uploadBlob: fUploadBlob, 7 | uploadFormData: fUploadFormData, 8 | parseJSON: fParseJSON 9 | }); 10 | 11 | function fSupport() { 12 | return !!window.FormData && !!window.XMLHttpRequest && !!window.JSON && !!window.addEventListener; 13 | } 14 | 15 | function fUploadFile(oFile, oConf) { 16 | var that = this; 17 | var oData = new FormData(); 18 | oData.append(oConf.name || 'file', oFile); 19 | that.uploadFormData(oData, oConf); 20 | } 21 | 22 | function fUploadBlob(oBlob, sFileName, oConf) { 23 | var that = this; 24 | var oData = new FormData(); 25 | oData.append(oConf.name || 'file', oBlob, sFileName); 26 | that.uploadFormData(oData, oConf); 27 | } 28 | 29 | /** 30 | * 上传数据 31 | * @param {Object} oData FormData 对象 32 | * @param {Object} oConf 配置参数 33 | * @param {String} oConf.name 上传数据的name 34 | * @param {Object} oConf.data 其他额外需要发送给服务器的数据 35 | * @param {Function} oConf.progress 上传进度回调 36 | * @param {Function} oConf.call 上传成功回调 37 | * @param {Function} oConf.error 上传失败回调 38 | */ 39 | function fUploadFormData(oData, oConf) { 40 | var that = this; 41 | var XMLHttpRequest = window.XMLHttpRequest; 42 | $.each(oConf.data, function (sKey, sVal) { 43 | sKey && sVal && oData.append(sKey, sVal); 44 | }); 45 | var oXhr = new XMLHttpRequest(); 46 | // 进度 47 | oXhr.upload.addEventListener('progress', function(oEvent) { 48 | oConf.progress && oConf.progress.call(null, Math.round(oEvent.loaded * 100 / (oEvent.total || 1)), oEvent.loaded, oEvent.total); 49 | }, false); 50 | // 结果 51 | oXhr.onreadystatechange = function(oEvent) { 52 | if (oXhr.readyState !== 4) { 53 | return; 54 | } 55 | 56 | if (oXhr.status == 200) { 57 | var oResult = that.parseJSON(oXhr.responseText); 58 | var fCb = oConf[oResult.code === 0 ? 'call' : 'error']; 59 | fCb && fCb.call(null, oResult); 60 | } else { 61 | oConf.error && oConf.error.call(that, {msg: '出现错误,请重试'}); 62 | } 63 | }; 64 | oXhr.open('POST', oConf.url, true); 65 | oXhr.send(oData); 66 | } 67 | 68 | function fParseJSON(sStr) { 69 | var oResult = {}; 70 | try { 71 | oResult = JSON.parse(sStr); 72 | } catch (e) {} 73 | return oResult; 74 | } 75 | })(window); -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/util/MailSender.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.util; 2 | 3 | import org.apache.velocity.app.VelocityEngine; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.InitializingBean; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.mail.javamail.JavaMailSenderImpl; 9 | import org.springframework.mail.javamail.MimeMessageHelper; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.ui.velocity.VelocityEngineUtils; 12 | 13 | import javax.mail.internet.InternetAddress; 14 | import javax.mail.internet.MimeMessage; 15 | import javax.mail.internet.MimeUtility; 16 | import java.util.Map; 17 | import java.util.Properties; 18 | 19 | 20 | @Service 21 | public class MailSender implements InitializingBean { 22 | private static final Logger logger = LoggerFactory.getLogger(MailSender.class); 23 | private JavaMailSenderImpl mailSender; 24 | 25 | @Autowired 26 | private VelocityEngine velocityEngine; 27 | 28 | public boolean sendWithHTMLTemplate(String to, String subject, 29 | String template, Map model) { 30 | try { 31 | String nick = MimeUtility.encodeText("牛客中级课"); 32 | InternetAddress from = new InternetAddress(nick + ""); 33 | MimeMessage mimeMessage = mailSender.createMimeMessage(); 34 | MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage); 35 | String result = VelocityEngineUtils 36 | .mergeTemplateIntoString(velocityEngine, template, "UTF-8", model); 37 | mimeMessageHelper.setTo(to); 38 | mimeMessageHelper.setFrom(from); 39 | mimeMessageHelper.setSubject(subject); 40 | mimeMessageHelper.setText(result, true); 41 | mailSender.send(mimeMessage); 42 | return true; 43 | } catch (Exception e) { 44 | logger.error("发送邮件失败" + e.getMessage()); 45 | return false; 46 | } 47 | } 48 | 49 | @Override 50 | public void afterPropertiesSet() throws Exception { 51 | mailSender = new JavaMailSenderImpl(); 52 | mailSender.setUsername("course@nowcoder.com"); 53 | mailSender.setPassword("NKnk66"); 54 | mailSender.setHost("smtp.exmail.qq.com"); 55 | mailSender.setPort(465); 56 | mailSender.setProtocol("smtps"); 57 | mailSender.setDefaultEncoding("utf8"); 58 | Properties javaMailProperties = new Properties(); 59 | javaMailProperties.put("mail.smtp.ssl.enable", true); 60 | mailSender.setJavaMailProperties(javaMailProperties); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/controller/HomeController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.controller; 2 | 3 | import com.nowcoder.model.EntityType; 4 | import com.nowcoder.model.HostHolder; 5 | import com.nowcoder.model.News; 6 | import com.nowcoder.model.ViewObject; 7 | import com.nowcoder.service.LikeService; 8 | import com.nowcoder.service.NewsService; 9 | import com.nowcoder.service.UserService; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Controller; 12 | import org.springframework.ui.Model; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RequestMethod; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | 22 | @Controller 23 | public class HomeController { 24 | @Autowired 25 | NewsService newsService; 26 | 27 | @Autowired 28 | UserService userService; 29 | 30 | @Autowired 31 | LikeService likeService; 32 | 33 | @Autowired 34 | HostHolder hostHolder; 35 | 36 | 37 | private List getNews(int userId, int offset, int limit) { 38 | List newsList = newsService.getLatestNews(userId, offset, limit); 39 | int localUserId = hostHolder.getUser() != null ? hostHolder.getUser().getId() : 0; 40 | List vos = new ArrayList<>(); 41 | for (News news : newsList) { 42 | ViewObject vo = new ViewObject(); 43 | vo.set("news", news); 44 | vo.set("user", userService.getUser(news.getUserId())); 45 | if (localUserId != 0) { 46 | vo.set("like", likeService.getLikeStatus(localUserId, EntityType.ENTITY_NEWS, news.getId())); 47 | } else { 48 | vo.set("like", 0); 49 | } 50 | vos.add(vo); 51 | } 52 | return vos; 53 | } 54 | 55 | @RequestMapping(path = {"/", "/index"}, method = {RequestMethod.GET, RequestMethod.POST}) 56 | public String index(Model model, 57 | @RequestParam(value = "pop", defaultValue = "0") int pop) { 58 | model.addAttribute("vos", getNews(0, 0, 10)); 59 | if (hostHolder.getUser() != null) { 60 | pop = 0; 61 | } 62 | model.addAttribute("pop", pop); 63 | return "home"; 64 | } 65 | 66 | @RequestMapping(path = {"/user/{userId}"}, method = {RequestMethod.GET, RequestMethod.POST}) 67 | public String userIndex(Model model, @PathVariable("userId") int userId) { 68 | model.addAttribute("vos", getNews(userId, 0, 10)); 69 | return "home"; 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.nowcoder 7 | toutiao 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | toutiao 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.3.5.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-aop 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-velocity 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-web 38 | 39 | 40 | mysql 41 | mysql-connector-java 42 | runtime 43 | 44 | 45 | org.mybatis.spring.boot 46 | mybatis-spring-boot-starter 47 | 1.1.1 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-devtools 52 | 53 | 54 | com.alibaba 55 | fastjson 56 | 1.2.13 57 | 58 | 59 | com.qiniu 60 | qiniu-java-sdk 61 | 7.1.1 62 | 63 | 64 | javax.mail 65 | mail 66 | 1.4.7 67 | 68 | 69 | redis.clients 70 | jedis 71 | 2.8.0 72 | 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-starter-test 77 | test 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/util/ToutiaoUtil.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.util; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.nowcoder.controller.LoginController; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.security.MessageDigest; 9 | import java.util.Map; 10 | 11 | 12 | public class ToutiaoUtil { 13 | private static final Logger logger = LoggerFactory.getLogger(ToutiaoUtil.class); 14 | 15 | public static String TOUTIAO_DOMAIN = "http://127.0.0.1:8080/"; 16 | public static String IMAGE_DIR = "D:/upload/"; 17 | public static String[] IMAGE_FILE_EXTD = new String[] {"png", "bmp", "jpg", "jpeg"}; 18 | 19 | //判断是否是图片名 20 | public static boolean isFileAllowed(String fileName) { 21 | for (String ext : IMAGE_FILE_EXTD) { 22 | if (ext.equals(fileName)) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | } 28 | 29 | 30 | //正常返回0,异常为非0 31 | //把结果生成json串 32 | public static String getJSONString(int code) { 33 | JSONObject json = new JSONObject(); 34 | json.put("code", code); 35 | return json.toJSONString(); 36 | } 37 | 38 | 39 | //那边是map,要把map里每个都转成json串 40 | public static String getJSONString(int code, Map map) { 41 | JSONObject json = new JSONObject(); 42 | json.put("code", code); 43 | for (Map.Entry entry : map.entrySet()) { 44 | json.put(entry.getKey(), entry.getValue()); 45 | } 46 | return json.toJSONString(); 47 | } 48 | 49 | //也可以直接放消息 50 | public static String getJSONString(int code, String msg) { 51 | JSONObject json = new JSONObject(); 52 | json.put("code", code); 53 | json.put("msg", msg); 54 | return json.toJSONString(); 55 | } 56 | 57 | 58 | 59 | 60 | //md5都有公开的算法 61 | public static String MD5(String key) { 62 | char hexDigits[] = { 63 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' 64 | }; 65 | try { 66 | byte[] btInput = key.getBytes(); 67 | // 获得MD5摘要算法的 MessageDigest 对象 68 | MessageDigest mdInst = MessageDigest.getInstance("MD5"); 69 | // 使用指定的字节更新摘要 70 | mdInst.update(btInput); 71 | // 获得密文 72 | byte[] md = mdInst.digest(); 73 | // 把密文转换成十六进制的字符串形式 74 | int j = md.length; 75 | char str[] = new char[j * 2]; 76 | int k = 0; 77 | for (int i = 0; i < j; i++) { 78 | byte byte0 = md[i]; 79 | str[k++] = hexDigits[byte0 >>> 4 & 0xf]; 80 | str[k++] = hexDigits[byte0 & 0xf]; 81 | } 82 | return new String(str); 83 | } catch (Exception e) { 84 | logger.error("生成MD5失败", e); 85 | return null; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/resources/templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 头条资讯 - 牛客网 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/interceptor/PassportInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.interceptor; 2 | 3 | import com.nowcoder.dao.LoginTicketDao; 4 | import com.nowcoder.dao.UserDao; 5 | import com.nowcoder.model.HostHolder; 6 | import com.nowcoder.model.LoginTicket; 7 | import com.nowcoder.model.User; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.HandlerInterceptor; 11 | import org.springframework.web.servlet.ModelAndView; 12 | import sun.rmi.runtime.Log; 13 | 14 | import javax.servlet.http.Cookie; 15 | import javax.servlet.http.HttpServletRequest; 16 | import javax.servlet.http.HttpServletResponse; 17 | import java.util.Date; 18 | 19 | // 登陆验证 20 | @Component 21 | public class PassportInterceptor implements HandlerInterceptor { 22 | 23 | @Autowired 24 | private LoginTicketDao loginTicketDao; 25 | 26 | @Autowired 27 | private UserDao userDao; 28 | 29 | @Autowired 30 | private HostHolder hostHolder; 31 | 32 | @Override 33 | public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { 34 | //从cookie里取ticket来判断, 因为ticket可能是伪造的 35 | String ticket = null; 36 | if (httpServletRequest.getCookies() != null) { 37 | for (Cookie cookie : httpServletRequest.getCookies()) { 38 | if (cookie.getName().equals("ticket")) { 39 | ticket = cookie.getValue(); 40 | break; 41 | } 42 | } 43 | } 44 | 45 | if(ticket != null){ 46 | LoginTicket loginTicket = loginTicketDao.selectByTicket(ticket); 47 | //用户没登录, 在这之前,说明过期了, 状态不等于0, 也是过期的 48 | if (loginTicket == null || loginTicket.getExpired().before(new Date()) || loginTicket.getStatus() != 0) { 49 | return true; //就直接过去了, 50 | } 51 | 52 | //如果我知道有这个人,就需要记录下来这个人了。用户保存起来 53 | User user = userDao.selectById(loginTicket.getUserId()); 54 | // httpServletRequest.setAttribute //不可取,因为后面service还要用到这个用户,随时要知道这个线程调用的用户是谁 55 | hostHolder.setUser(user); //把这次请求的这个用户存起来 56 | } 57 | //return false; //请求之前返回false就直接退出来了,不用请求了 58 | return true; 59 | } 60 | 61 | @Override 62 | public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { 63 | //在渲染之前,把用户存储进来,则在渲染的页面上就可以用user了 64 | //在页面上,如果有用户,则显示名字,没有的话则显示登陆 65 | if (modelAndView != null && hostHolder.getUser() != null) { 66 | modelAndView.addObject("user", hostHolder.getUser()); 67 | } 68 | } 69 | 70 | @Override 71 | public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { 72 | //收尾工作 73 | hostHolder.clear(); //否则每次登陆都放进去就要炸了 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/async/EventConsumer.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.async; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.nowcoder.util.JedisAdapter; 5 | import com.nowcoder.util.RedisKeyUtil; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.BeansException; 9 | import org.springframework.beans.factory.InitializingBean; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.context.ApplicationContext; 12 | import org.springframework.context.ApplicationContextAware; 13 | 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | public class EventConsumer implements InitializingBean, ApplicationContextAware { 20 | private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class); 21 | private Map> config = new HashMap>(); 22 | private ApplicationContext applicationContext; 23 | 24 | @Autowired 25 | JedisAdapter jedisAdapter; 26 | 27 | @Override 28 | public void afterPropertiesSet() throws Exception { 29 | Map beans = applicationContext.getBeansOfType(EventHandler.class); 30 | if (beans != null) { 31 | for (Map.Entry entry : beans.entrySet()) { 32 | List eventTypes = entry.getValue().getSupportEventTypes(); 33 | for (EventType type : eventTypes) { 34 | if (!config.containsKey(type)) { 35 | config.put(type, new ArrayList()); 36 | } 37 | config.get(type).add(entry.getValue()); 38 | } 39 | } 40 | } 41 | 42 | Thread thread = new Thread(new Runnable() { 43 | @Override 44 | public void run() { 45 | while (true) { 46 | String key = RedisKeyUtil.getEventQueueKey(); 47 | List events = jedisAdapter.brpop(0, key); //一直等待取时间 48 | for (String message : events) { 49 | if (message.equals(key)) { 50 | continue; 51 | } 52 | 53 | EventModel eventModel = JSON.parseObject(message, EventModel.class); 54 | if (!config.containsKey(eventModel.getType())) { 55 | logger.error("不能识别的事件"); 56 | continue; 57 | } 58 | 59 | for (EventHandler handler : config.get(eventModel.getType())) { 60 | handler.doHandle(eventModel); 61 | } 62 | } 63 | } 64 | } 65 | }); 66 | thread.start(); 67 | } 68 | 69 | @Override 70 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 71 | this.applicationContext = applicationContext; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/site/detail.js: -------------------------------------------------------------------------------- 1 | (function (window, undefined) { 2 | var PopupLogin = Base.getClass('main.component.PopupLogin'); 3 | var PopupUpload = Base.getClass('main.component.PopupUpload'); 4 | var ActionUtil = Base.getClass('main.util.Action'); 5 | 6 | Base.ready({ 7 | initialize: fInitialize, 8 | binds: { 9 | //.表示class #表示id 10 | 'click .js-login': fClickLogin, 11 | 'click .js-share': fClickShare 12 | }, 13 | events: { 14 | 'click button.click-like': fClickLike, 15 | 'click button.click-dislike': fClickDisLike 16 | } 17 | }); 18 | 19 | function fInitialize() { 20 | if (window.loginpop > 0) { 21 | fClickLogin(); 22 | } 23 | } 24 | function fClickShare() { 25 | var that = this; 26 | PopupUpload.show({ 27 | listeners: { 28 | done: function () { 29 | //alert('login'); 30 | window.location.reload(); 31 | } 32 | } 33 | }); 34 | } 35 | function fClickLogin() { 36 | var that = this; 37 | PopupLogin.show({ 38 | listeners: { 39 | login: function () { 40 | //alert('login'); 41 | window.location.reload(); 42 | }, 43 | register: function () { 44 | //alert('reg'); 45 | window.location.reload(); 46 | } 47 | } 48 | }); 49 | } 50 | 51 | function fClickLike(oEvent) { 52 | var that = this; 53 | var oEl = $(oEvent.currentTarget); 54 | var sId = $.trim(oEl.attr('data-id')); 55 | // 已经操作过 || 不存在Id || 正在提交 ,则忽略 56 | if (oEl.hasClass('pressed') || !sId || that.actioning) { 57 | return; 58 | } 59 | that.actioning = true; 60 | ActionUtil.like({ 61 | newsId: sId, 62 | call: function (oResult) { 63 | oEl.find('span.count').html(oResult.msg); 64 | oEl.addClass('pressed'); 65 | oEl.parent().find('.click-dislike').removeClass('pressed'); 66 | }, 67 | error: function () { 68 | alert('出现错误,请重试'); 69 | }, 70 | always: function () { 71 | that.actioning = false; 72 | } 73 | }); 74 | } 75 | 76 | function fClickDisLike(oEvent) { 77 | var that = this; 78 | var oEl = $(oEvent.currentTarget); 79 | var sId = $.trim(oEl.attr('data-id')); 80 | // 已经操作过 || 不存在Id || 正在提交 ,则忽略 81 | if (oEl.hasClass('pressed') || !sId || that.actioning) { 82 | return; 83 | } 84 | that.actioning = true; 85 | ActionUtil.dislike({ 86 | newsId: sId, 87 | call: function (oResult) { 88 | oEl.addClass('pressed'); 89 | var oLikeBtn = oEl.parent().find('.click-like'); 90 | oLikeBtn.removeClass('pressed'); 91 | oLikeBtn.find('span.count').html(oResult.msg); 92 | }, 93 | error: function () { 94 | alert('出现错误,请重试'); 95 | }, 96 | always: function () { 97 | that.actioning = false; 98 | } 99 | }); 100 | } 101 | 102 | })(window); -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/site/home.js: -------------------------------------------------------------------------------- 1 | (function (window, undefined) { 2 | var PopupLogin = Base.getClass('main.component.PopupLogin'); 3 | var PopupUpload = Base.getClass('main.component.PopupUpload'); 4 | var ActionUtil = Base.getClass('main.util.Action'); 5 | 6 | Base.ready({ 7 | initialize: fInitialize, 8 | binds: { 9 | //.表示class #表示id 10 | 'click .js-login': fClickLogin, 11 | 'click .js-share': fClickShare 12 | }, 13 | events: { 14 | 'click button.click-like': fClickLike, 15 | 'click button.click-dislike': fClickDisLike 16 | } 17 | }); 18 | 19 | function fInitialize() { 20 | if (window.loginpop > 0) { 21 | fClickLogin(); 22 | } 23 | } 24 | function fClickShare() { 25 | var that = this; 26 | PopupUpload.show({ 27 | listeners: { 28 | done: function () { 29 | //alert('login'); 30 | window.location.reload(); 31 | } 32 | } 33 | }); 34 | } 35 | function fClickLogin() { 36 | var that = this; 37 | PopupLogin.show({ 38 | listeners: { 39 | login: function () { 40 | //alert('login'); 41 | window.location.reload(); 42 | }, 43 | register: function () { 44 | //alert('reg'); 45 | window.location.reload(); 46 | } 47 | } 48 | }); 49 | } 50 | 51 | function fClickLike(oEvent) { 52 | var that = this; 53 | var oEl = $(oEvent.currentTarget); 54 | var sId = $.trim(oEl.attr('data-id')); 55 | // 已经操作过 || 不存在Id || 正在提交 ,则忽略 56 | if (oEl.hasClass('pressed') || !sId || that.actioning) { 57 | return; 58 | } 59 | that.actioning = true; 60 | ActionUtil.like({ 61 | newsId: sId, 62 | call: function (oResult) { 63 | oEl.find('span.count').html(oResult.msg); 64 | oEl.addClass('pressed'); 65 | oEl.parent().find('.click-dislike').removeClass('pressed'); 66 | }, 67 | error: function () { 68 | alert('出现错误,请重试'); 69 | }, 70 | always: function () { 71 | that.actioning = false; 72 | } 73 | }); 74 | } 75 | 76 | function fClickDisLike(oEvent) { 77 | var that = this; 78 | var oEl = $(oEvent.currentTarget); 79 | var sId = $.trim(oEl.attr('data-id')); 80 | // 已经操作过 || 不存在Id || 正在提交 ,则忽略 81 | if (oEl.hasClass('pressed') || !sId || that.actioning) { 82 | return; 83 | } 84 | that.actioning = true; 85 | ActionUtil.dislike({ 86 | newsId: sId, 87 | call: function (oResult) { 88 | oEl.addClass('pressed'); 89 | var oLikeBtn = oEl.parent().find('.click-like'); 90 | oLikeBtn.removeClass('pressed'); 91 | oLikeBtn.find('span.count').html(oResult.msg); 92 | }, 93 | error: function () { 94 | alert('出现错误,请重试'); 95 | }, 96 | always: function () { 97 | that.actioning = false; 98 | } 99 | }); 100 | } 101 | 102 | })(window); -------------------------------------------------------------------------------- /src/test/java/com/nowcoder/InitDatabaseTests.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder; 2 | 3 | import com.nowcoder.dao.CommentDao; 4 | import com.nowcoder.dao.LoginTicketDao; 5 | import com.nowcoder.dao.NewsDao; 6 | import com.nowcoder.dao.UserDao; 7 | import com.nowcoder.model.*; 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.SpringApplicationConfiguration; 13 | import org.springframework.test.context.jdbc.Sql; 14 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 15 | import org.springframework.test.context.web.WebAppConfiguration; 16 | 17 | import java.util.Date; 18 | import java.util.Random; 19 | 20 | @RunWith(SpringJUnit4ClassRunner.class) 21 | @SpringApplicationConfiguration(classes = ToutiaoApplication.class) 22 | @Sql("/init-schema.sql") 23 | public class InitDatabaseTests { 24 | @Autowired 25 | UserDao userDAO; 26 | 27 | @Autowired 28 | NewsDao newsDao; 29 | 30 | @Autowired 31 | LoginTicketDao loginTicketDao; 32 | 33 | @Autowired 34 | CommentDao commentDao; 35 | 36 | @Test 37 | public void initData() { 38 | Random random = new Random(); 39 | for (int i = 0; i < 11; ++i) { 40 | User user = new User(); 41 | user.setHeadUrl(String.format("http://images.nowcoder.com/head/%dt.png", random.nextInt(1000))); 42 | user.setName(String.format("USER%d", i)); 43 | user.setPassword(""); 44 | user.setSalt(""); 45 | userDAO.addUser(user); 46 | 47 | //资讯 48 | 49 | News news = new News(); 50 | news.setCommentCount(i); 51 | Date date = new Date(); 52 | date.setTime(date.getTime() + 1000*3600*5*i); 53 | news.setCreatedDate(date); 54 | news.setImage(String.format("http://images.nowcoder.com/head/%dm.png", random.nextInt(1000))); 55 | news.setLikeCount(i+1); 56 | news.setUserId(i+1); 57 | news.setTitle(String.format("TITLE{%d}", i)); 58 | news.setLink(String.format("http://www.nowcoder.com/%d.html", i)); 59 | newsDao.addNews(news); 60 | 61 | 62 | // 给每个资讯插入3个评论 63 | for(int j = 0; j < 3; ++j) { 64 | Comment comment = new Comment(); 65 | comment.setUserId(i+1); 66 | comment.setCreatedDate(new Date()); 67 | comment.setStatus(0); 68 | comment.setContent("这里是一个评论啊!" + String.valueOf(j)); 69 | comment.setEntityId(news.getId()); 70 | comment.setEntityType(EntityType.ENTITY_NEWS); 71 | commentDao.addComment(comment); 72 | } 73 | 74 | user.setPassword("newapssword"); 75 | userDAO.updatePassword(user); 76 | 77 | 78 | 79 | 80 | LoginTicket ticket = new LoginTicket(); 81 | ticket.setStatus(0); 82 | ticket.setUserId(i + 1); 83 | ticket.setExpired(date); 84 | ticket.setTicket(String.format("TICKET%d", i+1)); 85 | loginTicketDao.addTicket(ticket); 86 | 87 | loginTicketDao.updateStatus(ticket.getTicket(), 2); 88 | } 89 | 90 | Assert.assertEquals(1, loginTicketDao.selectByTicket("TICKET1").getUserId()); 91 | Assert.assertEquals(2, loginTicketDao.selectByTicket("TICKET1").getStatus()); 92 | 93 | /* Assert.assertEquals("newapssword", userDAO.selectById(1).getPassword()); 94 | userDAO.deleteById(1); 95 | Assert.assertNull(userDAO.selectById(1));*/ 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.service; 2 | 3 | import com.nowcoder.dao.LoginTicketDao; 4 | import com.nowcoder.dao.UserDao; 5 | import com.nowcoder.model.LoginTicket; 6 | import com.nowcoder.model.User; 7 | import com.nowcoder.util.ToutiaoUtil; 8 | import org.apache.commons.lang.StringUtils; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.*; 13 | 14 | @Service 15 | public class UserService { 16 | 17 | @Autowired 18 | private UserDao userDao; 19 | 20 | @Autowired 21 | private LoginTicketDao loginTicketDao; 22 | 23 | public Map register(String username, String password){ 24 | Map map = new HashMap(); 25 | if(StringUtils.isBlank(username)){ 26 | map.put("msgname", "用户名不能为空"); 27 | } 28 | 29 | if (StringUtils.isBlank(password)) { 30 | map.put("msgpwd", "密码不能为空"); 31 | return map; 32 | } 33 | 34 | User user = userDao.selectByName(username); 35 | 36 | if(user != null){ 37 | map.put("msgname", "用户名已经被注册"); 38 | return map; 39 | } 40 | 41 | user = new User(); 42 | user.setName(username); 43 | // user.setPassword(password); 不能这样明文保存,还要加盐 44 | user.setSalt(UUID.randomUUID().toString().substring(0,5)); 45 | String head = String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)); 46 | user.setHeadUrl(head); 47 | user.setPassword(ToutiaoUtil.MD5(password+user.getSalt())); 48 | userDao.addUser(user); 49 | 50 | //服务器下发一个token 51 | String ticket = addLoginTicket(user.getId()); 52 | map.put("ticket", ticket); 53 | 54 | return map; 55 | } 56 | 57 | public Map login(String username, String password) { 58 | Map map = new HashMap(); 59 | if (StringUtils.isBlank(username)) { 60 | map.put("msgname", "用户名不能为空"); 61 | return map; 62 | } 63 | 64 | if (StringUtils.isBlank(password)) { 65 | map.put("msgpwd", "密码不能为空"); 66 | return map; 67 | } 68 | 69 | User user = userDao.selectByName(username); 70 | 71 | if (user == null) { 72 | map.put("msgname", "用户名不存在"); 73 | return map; 74 | } 75 | 76 | if (!ToutiaoUtil.MD5(password+user.getSalt()).equals(user.getPassword())) { 77 | map.put("msgpwd", "密码不正确"); 78 | return map; 79 | } 80 | 81 | //登录成功了就要下发一个ticket给用户 82 | String ticket = addLoginTicket(user.getId()); 83 | map.put("ticket", ticket); 84 | 85 | return map; 86 | 87 | } 88 | 89 | //与user关联,一有用户登录就下发一个ticket给用户 90 | private String addLoginTicket(int userId){ 91 | LoginTicket ticket = new LoginTicket(); 92 | ticket.setUserId(userId); 93 | Date date = new Date(); 94 | date.setTime(date.getTime() + 1000*3600*24); //24小时有效期 95 | ticket.setExpired(date); 96 | ticket.setStatus(0); 97 | ticket.setTicket(UUID.randomUUID().toString().replaceAll("-", "")); //uuid是有-的 98 | loginTicketDao.addTicket(ticket); 99 | return ticket.getTicket(); 100 | } 101 | 102 | public void logout(String ticket) { 103 | loginTicketDao.updateStatus(ticket, 1); 104 | } 105 | 106 | public User getUser(int id){ 107 | return userDao.selectById(id); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/resources/templates/letter.html: -------------------------------------------------------------------------------- 1 | #parse("header.html") 2 |
3 |
4 | 40 | 41 |
42 | 83 |
84 | #parse("footer.html") -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/controller/MessageController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.controller; 2 | 3 | import com.nowcoder.model.*; 4 | import com.nowcoder.service.*; 5 | import com.nowcoder.util.ToutiaoUtil; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Date; 15 | import java.util.List; 16 | 17 | 18 | @Controller 19 | public class MessageController { 20 | private static final Logger logger = LoggerFactory.getLogger(MessageController.class); 21 | 22 | @Autowired 23 | HostHolder hostHolder; 24 | 25 | @Autowired 26 | UserService userService; 27 | 28 | @Autowired 29 | MessageService messageService; 30 | 31 | @RequestMapping(path = {"/msg/detail"}, method = {RequestMethod.GET}) 32 | public String conversationDetail(Model model, @RequestParam("conversationId") String conversationId) { 33 | try { 34 | List messages = new ArrayList<>(); 35 | // 展示10条消息 36 | List conversationList = messageService.getConversationDetail(conversationId, 0, 10); 37 | for (Message msg : conversationList) { 38 | ViewObject vo = new ViewObject(); 39 | vo.set("message", msg); 40 | User user = userService.getUser(msg.getFromId()); 41 | if (user == null) { 42 | continue; 43 | } 44 | vo.set("headUrl", user.getHeadUrl()); 45 | vo.set("userName", user.getName()); 46 | messages.add(vo); 47 | } 48 | model.addAttribute("messages", messages); 49 | return "letterDetail"; 50 | } catch (Exception e) { 51 | logger.error("获取站内信列表失败" + e.getMessage()); 52 | } 53 | return "letterDetail"; 54 | } 55 | 56 | @RequestMapping(path = {"/msg/list"}, method = {RequestMethod.GET}) 57 | public String conversationList(Model model) { 58 | try { 59 | int localUserId = hostHolder.getUser().getId(); 60 | List conversations = new ArrayList<>(); 61 | List conversationList = messageService.getConversationList(localUserId, 0, 10); 62 | for (Message msg : conversationList) { 63 | ViewObject vo = new ViewObject(); 64 | vo.set("conversation", msg); 65 | // 交互的人是谁,可能是from也可能是to。能知道对方的Id是多少? 66 | int targetId = msg.getFromId() == localUserId ? msg.getToId() : msg.getFromId(); 67 | User user = userService.getUser(targetId); 68 | vo.set("headUrl", user.getHeadUrl()); 69 | vo.set("userName", user.getName()); 70 | vo.set("targetId", targetId); 71 | vo.set("totalCount", msg.getId()); 72 | vo.set("unreadCount", messageService.getUnreadCount(localUserId, msg.getConversationId())); 73 | conversations.add(vo); 74 | } 75 | model.addAttribute("conversations", conversations); 76 | return "letter"; 77 | } catch (Exception e) { 78 | logger.error("获取站内信列表失败" + e.getMessage()); 79 | } 80 | return "letter"; 81 | } 82 | 83 | @RequestMapping(path = {"/msg/addMessage"}, method = {RequestMethod.GET, RequestMethod.POST}) 84 | @ResponseBody 85 | public String addMessage(@RequestParam("fromId") int fromId, 86 | @RequestParam("toId") int toId, 87 | @RequestParam("content") String content) { 88 | Message msg = new Message(); 89 | msg.setContent(content); 90 | msg.setCreatedDate(new Date()); 91 | msg.setToId(toId); 92 | msg.setFromId(fromId); 93 | msg.setConversationId(fromId < toId ? String.format("%d_%d", fromId, toId) : 94 | String.format("%d_%d", toId, fromId)); 95 | messageService.addMessage(msg); 96 | return ToutiaoUtil.getJSONString(msg.getId()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/controller/LoginController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.controller; 2 | 3 | import com.nowcoder.async.EventModel; 4 | import com.nowcoder.async.EventProducer; 5 | import com.nowcoder.async.EventType; 6 | import com.nowcoder.service.UserService; 7 | import com.nowcoder.util.ToutiaoUtil; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Controller; 12 | import org.springframework.ui.Model; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import javax.servlet.http.Cookie; 16 | import javax.servlet.http.HttpServletResponse; 17 | import java.util.Map; 18 | 19 | @Controller 20 | public class LoginController { 21 | private static final Logger logger = LoggerFactory.getLogger(LoginController.class); 22 | 23 | @Autowired 24 | UserService userService; 25 | 26 | @Autowired 27 | EventProducer eventProducer; 28 | 29 | @RequestMapping(path = {"/reg/"}, method = {RequestMethod.GET, RequestMethod.POST}) 30 | @ResponseBody 31 | public String reg(Model model, @RequestParam("username") String username, 32 | @RequestParam("password") String password, 33 | @RequestParam(value="rember", defaultValue = "0") int rememberme, 34 | HttpServletResponse response){ 35 | try { 36 | Map map = userService.register(username, password); 37 | if (map.containsKey("ticket")) { 38 | // 包含ticket登陆成功,否则失败 39 | Cookie cookie = new Cookie("ticket", map.get("ticket").toString()); 40 | cookie.setPath("/"); 41 | if (rememberme > 0) { 42 | cookie.setMaxAge(3600*24*5); 43 | } 44 | response.addCookie(cookie); 45 | return ToutiaoUtil.getJSONString(0, "注册成功"); 46 | } else { 47 | return ToutiaoUtil.getJSONString(1, map); 48 | } 49 | 50 | }catch (Exception e){ 51 | logger.error("注册异常" + e.getMessage()); 52 | return ToutiaoUtil.getJSONString(1, "注册异常"); 53 | } 54 | 55 | } 56 | 57 | 58 | @RequestMapping(path = {"/login/"}, method = {RequestMethod.GET, RequestMethod.POST}) 59 | @ResponseBody 60 | public String login(Model model, @RequestParam("username") String username, 61 | @RequestParam("password") String password, 62 | @RequestParam(value="rember", defaultValue = "0") int rememberme, 63 | HttpServletResponse response) { 64 | 65 | try{ 66 | Map map = userService.login(username, password); 67 | //登录成功是有ticket的 68 | if(map.containsKey("ticket")){ 69 | Cookie cookie = new Cookie("ticket", map.get("ticket").toString()); 70 | cookie.setPath("/"); //设置路径是全站有效的 71 | 72 | //如果有renmber,则cookie 的有效时间长一点,否则的话浏览器关闭就没了 73 | if (rememberme > 0) { 74 | cookie.setMaxAge(3600*24*5); 75 | } 76 | response.addCookie(cookie); 77 | 78 | eventProducer.fireEvent(new EventModel(EventType.LOGIN) 79 | .setActorId((int) map.get("userId")) 80 | .setExt("username", username).setExt("email", "zjuyxy@qq.com")); 81 | 82 | return ToutiaoUtil.getJSONString(0, "登录成功" + map); 83 | }else{ 84 | return ToutiaoUtil.getJSONString(1, map); 85 | } 86 | 87 | }catch (Exception e){ 88 | logger.error("注册异常" + e.getMessage()); 89 | return ToutiaoUtil.getJSONString(1, "注册异常"); 90 | } 91 | } 92 | 93 | //如果写了responsebody则是在页面上显示字符串, 不写就是跳转页面 94 | @RequestMapping(path = {"/logout/"}, method = {RequestMethod.GET, RequestMethod.POST}) 95 | //@ResponseBody 96 | public String logout(@CookieValue("ticket") String ticket) { 97 | userService.logout(ticket); 98 | return "redirect:/"; //登出返回是首页 99 | 100 | //return ToutiaoUtil.getJSONString(0, "成功退出来了 "+ticket); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # toutiao 2 | 仿照今日头条的主页toutiao.com做的一个Java web项目。使用SpringBoot+Mybatis+velocity开发。 3 | 4 | ### 内容包括 5 | 6 | - 前期准备 7 | 8 | - 用户注册登录管理以及使用token 9 | 10 | - 资讯发布,图片上传,资讯首页(上传图片至云存储) 11 | 12 | - 评论中心,站内信 13 | 14 | - Redis入门以及Redis实现赞踩功能 15 | 16 | - 构建异步框架 17 | 18 | 19 | 20 | ## 前期准备 21 | 22 | ``` 23 | 创建git仓库,本地配置idea并测试pull和push。 24 | 创建SpringBoot工程,导入Web,Velocity和Aop的包。 25 | 生成Maven项目,pom.xml包含上述依赖。 26 | 27 | AOP和IOC 28 | IOC解决对象实例化以及依赖传递问题,解耦。 29 | AOP解决纵向切面问题,主要实现日志和权限控制功能。 30 | aspect实现切面,并且使用logger来记录日志,用该切面的切面方法来监听controller。 31 | 32 | 配置 33 | springboot使用1.4.0 34 | mybatis-spring-boot-starter使用1.2.1 35 | mysql-connector-java使用5.1.6 36 | 写好静态文件html css和js。并且注意需要配置 37 | spring.velocity.suffix=.html 保证跳转请求转发到html上 38 | spring.velocity.toolbox-config-location=toolbox.xml 39 | 40 | 两个小工具 41 | ViewObject:方便传递任何数据到 42 | VelocityDateTool:velocity自带工具类 43 | 44 | ``` 45 | 46 | **基本流程** 47 | ``` 48 | 创建基本的controller,service和model层。 49 | 50 | controller中使用注解配置,requestmapping,responsebody基本可以解决请求转发以及响应内容的渲染。 51 | 52 | 使用pathvariable和requestparam传递参数。 53 | 54 | 使用velocity编写页面模板,注意其中的语法使用。常用$!{}和${} 55 | 56 | 使用http规范下的httpservletrequest和httpservletresponse来封装请求和相响应,使用封装好的session和cookie对象。 57 | 58 | 使用重定向的redirectview和统一异常处理器exceptionhandler 59 | ``` 60 | 61 | 62 | 63 | ## 数据库表 64 | 65 | ``` 66 | user(is, name, passowrd, salt, head_url) 67 | news(id, title, link, image, like_count, comment_count, create_date, user_id) 68 | login_ticket(id ,user_id, ticket, expired, status) 69 | comment(id, content, user_id, entity_id, entity_type, create_date, status) 70 | message(id, from_id, to_id, content, create_date, has_read, conversation_id) 71 | ``` 72 | 73 | ## 用户注册登录与Token 74 | 75 | **UserController** 76 | ``` 77 | 数据库表Login_ticket来存储ticket字段,每次客户端登录或注册时服务器会下发一个ticket下发给客户端,服务器生成 78 | 的这个ticket是根据用户id随机生成的UUid,过期时间以及有效状态,而客户端则将这个ticket设置为Cookies,这样客户端每 79 | 次访问页面都是一个带token的http请求(登录状态下的访问页面)。 80 | 81 | 使用拦截器HandlerInterceptor可以用来拦截所有用户请求,来判断是否有效的ticket,有的话则写入ThreadLocal,供全 82 | 局任意位置获取用户信息。在处理前来判断是否有效,在渲染前把数据装入model供前端页面渲染,在所有处理完后清除ThreadLocal 83 | 里本地的用户。写完拦截器后需要在该条链路上注册该拦截器,这样在执行该条链路时就可以回调了。 84 | 85 | 该ticket功能类似session,通过cookie写回浏览器,浏览器请求再通过cookie传递,区别是该字段在数据库中,并且可 86 | 以用在移动端。 87 | 88 | 工具类的使用:Json工具类,md5加密为密码加salt,Json数据传输。 89 | 90 | 数据安全性保障方法:https使用公钥加密私钥解密,比如支付宝的密码加密,单点登录验证,验证码机制等。 91 | 92 | ``` 93 | 94 | ## 图片上传与云SDK存储 95 | 96 | **NewsController** 97 | ``` 98 | 图片的本地上传:post请求,content-type类型是multipart类型,当有多个图片或参数时,post会使用content-type的 99 | boundary来分割开,后端Spring接受用同样参数来接受File.图片的一般上传流程,比较简单。最后返回的是这个图片的URI。 100 | 101 | 图片的展示,如果用get获取时,一般展示的是页面,而要读取图片的话(二进制流),则添加一个头Content-type为image 102 | ,来告诉客户端是一个image,有包装好的工具类StreamUtils.copy(Input, Response.outputStream)。 103 | 104 | 使用云SDK上传图片,比较容易实现,好处在于CDN内容分发,缩图服务等。这样每次网站发布就不需要加载静态文件了。 105 | ``` 106 | 107 | 108 | ## 评论和消息中心 109 | 110 | **NewsController,MessageController** 111 | ``` 112 | 对于评论表的字段设计,要考虑到以后的复用,不仅仅是评论资讯,而是评论本身呢? 113 | 资讯的显示评论,通过该资讯id来中查找评论,并按时间顺序打印显示出来。 114 | 115 | 消息中心:有两个页面,一个消息列表页(所有conversation_id),一个消息详情页(同一个conversation_id)。 116 | 消息通信是两个用户之间发一条消息,把from_id和to_id按从小到大排好序,通过该from-to来存消息。 117 | 消息列表页是一个复杂的SQL语句,要求分组后的conversation_id取到最新的那条,并依次按最新的输出。 118 | 消息详情页是只需要展示同一个conversation_id的Message就可以了。 119 | 120 | 并没有新增技术点,前后交互的业务逻辑比较复杂。 121 | ``` 122 | 123 | 124 | ## 点赞点踩功能 125 | 126 | **LikeController** 127 | ``` 128 | 熟悉Redis基本的数据结构,知道使用Jedis api等。了解Redis在微博中大量应用。 129 | 130 | 创建Redis池,将Jedis的api封装好成工具,供其他层调用。 131 | 132 | 根据需求来确定key字段,每一条资讯都有一个like集合和dislike集合,这样分别存userid。 133 | 查询直接在Reis上查找,而不用每次都去刷数据库,缓解了数据库的压力,在一定时间内,再去刷新数据库中的likecount字段。 134 | ``` 135 | 136 | 137 | ## 异步框架 138 | 139 | **async** 140 | ``` 141 | 大的服务项目要必须是要实现服务化,异步化~ 这两者是同步实行的。当一个事件或者卡IO,并且并不需要实时显示,可以把其生 142 | 成一个事件,并抛给队列,并不需要立即实现,节约了时间。如点了一个赞,后续可能是积分增加,站内通知,积分排行榜一些列事件 143 | 都需要变化,但是用户只需要知道成功点了一个赞,后续操作可以延迟一些。这就是异步的需要实现的东西了。 144 | 145 | Redis的list可以当成一个事件队列,将对象事件放进去取出来必然要涉及到事件的序列化。进而用EventModel来包装一个事件,开 146 | 发中事件生产抛给队列,而消费则从队列取时间,不同的事件有实现不同的EventHandler,类似消费-生产模式。注意:消费者专门用一个 147 | 线程去处理,这个线程是阻塞的(当队列没有会一直等待)。 148 | 149 | 代码实现:在发生突发情况,队列中的事件还是存在的,消费者在初始化时要把在队列中的事件给整理成一个类似查找表。 150 | 151 | 添加了邮件发送功能,引用mail依赖,并配置好自己的邮箱配置,写在业务逻辑代码中。 152 | 153 | 了解了Hacker News,Reddit,StackOverflow,IMDB的一些排序算法~ 154 | ``` 155 | -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/component/popup.js: -------------------------------------------------------------------------------- 1 | /** 2 | var oPopup = new Popup({ 3 | title: String, 标题 4 | content: String, 内容 5 | width: Number, 宽度 6 | close: Function, 关闭的回调 7 | hasNoHeader: Boolean, true 没有头部 8 | }); 9 | */ 10 | (function (window, undefined) { 11 | var Popup = Base.createClass('main.component.Popup'); 12 | var Component = Base.getClass('main.component.Component'); 13 | Base.mix(Popup, Component, { 14 | zIndex: 100, 15 | _tpl: [ 16 | '
', 17 | '
', 18 | '', 19 | '

#{title}

', 20 | '
', 21 | '
#{content}
', 22 | '
'].join(''), 23 | listeners: [{ 24 | name: 'render', 25 | type: 'custom', 26 | handler: function () { 27 | var that = this; 28 | var oConf = that.rawConfig; 29 | var oEl = that.getEl(); 30 | // 常用元素 31 | that.contentEl = oEl.find('div.pop-content'); 32 | // 调整大小 33 | oEl.outerWidth(oConf.width || 520); 34 | oConf.height && that.contentEl.outerHeight(oConf.height); 35 | // 禁止body滚动 36 | that.forbidScroll(document.body); 37 | // 创建遮罩层 38 | that.initMask(); 39 | // 调整z-index 40 | oEl.css('zIndex', Popup.zIndex++); 41 | // 去掉头部 42 | oConf.hasNoHeader && oEl.find('div.pop-title').remove(); 43 | // 位置居中 44 | that.fixPosition(); 45 | // 绑定窗口变化事件 46 | that.resizeCb = Base.bind(that.fixPosition, that); 47 | $(window).resize(that.resizeCb); 48 | } 49 | }, { 50 | name: 'click .js-close', 51 | handler: function () { 52 | var that = this; 53 | that.close(); 54 | } 55 | }] 56 | }, { 57 | initialize: fInitialize, 58 | initMask: fInitMask, 59 | fixPosition: fFixPosition, 60 | close: fClose, 61 | getData: fGetData 62 | }); 63 | 64 | function fInitialize(oConf) { 65 | var that = this; 66 | var oBody = $(document.body); 67 | oConf.renderTo = oBody; 68 | that.isForbidScroll = oBody.css('overflow-y') === 'hidden'; 69 | Popup.superClass.initialize.apply(that, arguments); 70 | } 71 | 72 | function fInitMask() { 73 | var that = this; 74 | var oConf = that.rawConfig; 75 | if (!that.maskEl) { 76 | that.maskEl = $('
'); 77 | oConf.renderTo.append(that.maskEl); 78 | } 79 | } 80 | 81 | function fFixPosition() { 82 | var that = this; 83 | var oEl = that.getEl(); 84 | var oWin = $(window); 85 | var oDoc = $(document); 86 | var nElWidth = oEl.width(); 87 | var nElHeight = oEl.height(); 88 | var nWinWidth = oWin.width(); 89 | var nWinHeight = oWin.height(); 90 | var nScrollTop = Math.max(oWin.scrollTop() || oDoc.scrollTop()); 91 | // 调整元素大小 92 | oEl.css({ 93 | left: nWinWidth > nElWidth ? (nWinWidth - nElWidth) / 2 : 0, 94 | top: (nWinHeight > nElHeight ? (nWinHeight - nElHeight) / 2 : 0) + nScrollTop 95 | }); 96 | // 调整遮罩层大小 97 | that.maskEl.css({ 98 | width: '100%', 99 | height: nWinHeight, 100 | top: nScrollTop 101 | }); 102 | } 103 | 104 | function fClose(bNoEmit) { 105 | var that = this; 106 | // 移除文件 107 | var oEl = that.getEl(); 108 | oEl.remove(); 109 | // 启动滚动 110 | !that.isForbidScroll && that.forbidScroll(document.body, false); 111 | // 移除遮罩层 112 | that.maskEl && that.maskEl.remove(); 113 | // 取消窗口变化事件 114 | $(window).unbind('resize', that.resizeCb); 115 | !bNoEmit && that.emit('close'); 116 | } 117 | 118 | function fGetData(oConf) { 119 | var that = this; 120 | return { 121 | title: oConf.title || '提示', 122 | content: oConf.content 123 | }; 124 | } 125 | 126 | })(window); -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/component/popupUpload.js: -------------------------------------------------------------------------------- 1 | /** 2 | var oPopupUpload = new PopupUpload({ 3 | 4 | }); 5 | */ 6 | (function (window) { 7 | var PopupUpload = Base.createClass('main.component.PopupUpload'); 8 | var Popup = Base.getClass('main.component.Popup'); 9 | var Upload = Base.getClass('main.component.Upload'); 10 | var Component = Base.getClass('main.component.Component'); 11 | var Util = Base.getClass('main.base.Util'); 12 | 13 | Base.mix(PopupUpload, Component, { 14 | _tpl: [ 15 | '
', 16 | '
', 17 | '
', 18 | '', 19 | '
', 20 | '上传图片', 21 | '
', 22 | '
', 23 | '
', 24 | '
', 25 | '
', 26 | '
', 27 | '', 28 | '
', 29 | '
', 30 | '
'].join(''), 31 | listeners: [{ 32 | name: 'render', 33 | type: 'custom', 34 | handler: function () { 35 | var that = this; 36 | var oEl = that.getEl(); 37 | var oUploadBtn = oEl.find('a.js-upload-btn'); 38 | new Upload({ 39 | targetEl: oUploadBtn, 40 | url: '/uploadImage/', 41 | check: function (oFile, sType, nFileSize) { 42 | var sMsg = nFileSize === 0 ? '文件大小不能为0' : /image/gi.test(sType || '') ? '' : '文件格式不正确'; 43 | sMsg && alert(sMsg); 44 | return !sMsg; 45 | }, 46 | call: function (oResult) { 47 | var sUrl = $.trim(oResult.msg); 48 | if (oResult.code !== 0) { 49 | return alert('出现错误,请重试'); 50 | } 51 | that.image = sUrl; 52 | that.showImage(sUrl); 53 | } 54 | }); 55 | } 56 | }, { 57 | name: 'click input.js-submit', 58 | handler: function () { 59 | var that = this; 60 | var oEl = that.getEl(); 61 | var sTitle = $.trim(oEl.find('input.js-title').val()); 62 | var sLink = $.trim(oEl.find('input.js-link').val()); 63 | if (!sTitle) { 64 | return alert('标题不能为空'); 65 | } 66 | if (!sLink) { 67 | return alert('链接不能为空'); 68 | } 69 | if (!that.image) { 70 | return alert('图片不能为空'); 71 | } 72 | if (that.requesting) { 73 | return; 74 | } 75 | that.requesting = true; 76 | $.ajax({ 77 | url: '/user/addNews/', 78 | method: 'post', 79 | data: {image: that.image, title: sTitle, link: sLink}, 80 | dataType: 'json' 81 | }).done(function (oResult) { 82 | that.emit('done'); 83 | }).fail(function (oResult) { 84 | alert('出现错误,请重试'); 85 | }).always(function () { 86 | that.requesting = false; 87 | }); 88 | } 89 | }], 90 | show: fStaticShow 91 | }, { 92 | initialize: fInitialize, 93 | showImage: fShowImage 94 | }); 95 | 96 | function fStaticShow(oConf) { 97 | var that = this; 98 | var oLogin = new PopupUpload(oConf); 99 | var oPopup = new Popup({ 100 | title: '分享', 101 | width: 700, 102 | content: oLogin.html() 103 | }); 104 | oLogin._popup = oPopup; 105 | Component.setEvents(); 106 | } 107 | 108 | function fInitialize(oConf) { 109 | var that = this; 110 | delete oConf.renderTo; 111 | PopupUpload.superClass.initialize.apply(that, arguments); 112 | } 113 | 114 | function fShowImage(sUrl) { 115 | var that = this; 116 | var oEl = that.getEl(); 117 | var sHtml = [ 118 | '
', 119 | '', 120 | '
', 121 | '', 122 | '
'].join(''); 123 | oEl.find('div.letter-pic-box').remove(); 124 | oEl.find('div.js-image-container').prepend(sHtml); 125 | } 126 | 127 | })(window); -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/util/JedisAdapter.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.util; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.InitializingBean; 7 | import org.springframework.stereotype.Service; 8 | import redis.clients.jedis.*; 9 | 10 | import java.util.List; 11 | 12 | 13 | @Service 14 | public class JedisAdapter implements InitializingBean { 15 | private static final Logger logger = LoggerFactory.getLogger(JedisAdapter.class); 16 | 17 | private Jedis jedis = null; 18 | private JedisPool pool = null; 19 | 20 | @Override 21 | public void afterPropertiesSet() throws Exception { 22 | pool = new JedisPool("localhost", 6379); 23 | } 24 | 25 | public String get(String key) { 26 | Jedis jedis = null; 27 | try { 28 | jedis = pool.getResource(); 29 | return jedis.get(key); 30 | } catch (Exception e) { 31 | logger.error("发生异常" + e.getMessage()); 32 | return null; 33 | } finally { 34 | if (jedis != null) { 35 | jedis.close(); 36 | } 37 | } 38 | } 39 | 40 | public void set(String key, String value) { 41 | Jedis jedis = null; 42 | try { 43 | jedis = pool.getResource(); 44 | jedis.set(key, value); 45 | } catch (Exception e) { 46 | logger.error("发生异常" + e.getMessage()); 47 | } finally { 48 | if (jedis != null) { 49 | jedis.close(); 50 | } 51 | } 52 | } 53 | 54 | public long sadd(String key, String value) { 55 | Jedis jedis = null; 56 | try { 57 | jedis = pool.getResource(); 58 | return jedis.sadd(key, value); 59 | } catch (Exception e) { 60 | logger.error("发生异常" + e.getMessage()); 61 | return 0; 62 | } finally { 63 | if (jedis != null) { 64 | jedis.close(); 65 | } 66 | } 67 | } 68 | 69 | public long srem(String key, String value) { 70 | Jedis jedis = null; 71 | try { 72 | jedis = pool.getResource(); 73 | return jedis.srem(key, value); 74 | } catch (Exception e) { 75 | logger.error("发生异常" + e.getMessage()); 76 | return 0; 77 | } finally { 78 | if (jedis != null) { 79 | jedis.close(); 80 | } 81 | } 82 | } 83 | 84 | public boolean sismember(String key, String value) { 85 | Jedis jedis = null; 86 | try { 87 | jedis = pool.getResource(); 88 | return jedis.sismember(key, value); 89 | } catch (Exception e) { 90 | logger.error("发生异常" + e.getMessage()); 91 | return false; 92 | } finally { 93 | if (jedis != null) { 94 | jedis.close(); 95 | } 96 | } 97 | } 98 | 99 | public long scard(String key) { 100 | Jedis jedis = null; 101 | try { 102 | jedis = pool.getResource(); 103 | return jedis.scard(key); 104 | } catch (Exception e) { 105 | logger.error("发生异常" + e.getMessage()); 106 | return 0; 107 | } finally { 108 | if (jedis != null) { 109 | jedis.close(); 110 | } 111 | } 112 | } 113 | 114 | public void setex(String key, String value) { 115 | // 验证码, 防机器注册,记录上次注册时间,有效期3天 116 | Jedis jedis = null; 117 | try { 118 | jedis = pool.getResource(); 119 | jedis.setex(key, 10, value); 120 | } catch (Exception e) { 121 | logger.error("发生异常" + e.getMessage()); 122 | } finally { 123 | if (jedis != null) { 124 | jedis.close(); 125 | } 126 | } 127 | } 128 | 129 | public long lpush(String key, String value) { 130 | Jedis jedis = null; 131 | try { 132 | jedis = pool.getResource(); 133 | return jedis.lpush(key, value); 134 | } catch (Exception e) { 135 | logger.error("发生异常" + e.getMessage()); 136 | return 0; 137 | } finally { 138 | if (jedis != null) { 139 | jedis.close(); 140 | } 141 | } 142 | } 143 | 144 | public List brpop(int timeout, String key) { 145 | Jedis jedis = null; 146 | try { 147 | jedis = pool.getResource(); 148 | return jedis.brpop(timeout, key); 149 | } catch (Exception e) { 150 | logger.error("发生异常" + e.getMessage()); 151 | return null; 152 | } finally { 153 | if (jedis != null) { 154 | jedis.close(); 155 | } 156 | } 157 | } 158 | 159 | public void setObject(String key, Object obj) { 160 | set(key, JSON.toJSONString(obj)); 161 | } 162 | 163 | public T getObject(String key, Class clazz) { 164 | String value = get(key); 165 | if (value != null) { 166 | return JSON.parseObject(value, clazz); 167 | } 168 | return null; 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/component/upload.js: -------------------------------------------------------------------------------- 1 | /** 2 | var oUpload = new Upload({ 3 | targetEl: Object, 上传按钮 4 | name: String, 文件的name 5 | url: String, 上传路径 6 | data: Object, 发送的参数 7 | check: Function, 文件检查 8 | call: Function, 上传成功回调 9 | error: Function, 上传失败回调 10 | progress: Function, 上传进度回调 11 | }); 12 | */ 13 | (function (window, undefined) { 14 | var Upload = Base.createClass('main.component.Upload'); 15 | var BaseUpload = Base.getClass('main.base.Upload'); 16 | var Popup = Base.getClass('main.component.Popup'); 17 | var Component = Base.getClass('main.component.Component'); 18 | 19 | Base.mix(Upload, Component, { 20 | _tpl: '
', 21 | listeners: [{ 22 | name: 'render', 23 | type: 'custom', 24 | handler: function () { 25 | var that = this; 26 | var oEl = that.getEl(); 27 | that.fileEl = oEl.find('input.js-upload'); 28 | that.initTarget(); 29 | that.initInput(); 30 | } 31 | }] 32 | }, { 33 | initialize: fInitialize, 34 | initTarget: fInitTarget, 35 | initInput: fInitInput, 36 | clear: fClear 37 | }); 38 | 39 | function fInitialize(oConf) { 40 | var that = this; 41 | var oTargetEl = $(oConf.targetEl); 42 | oTargetEl.css('position', 'relative'); 43 | oConf.renderTo = oTargetEl; 44 | Upload.superClass.initialize.apply(that, arguments); 45 | } 46 | 47 | function fInitTarget() { 48 | var that = this; 49 | var oConf = that.rawConfig; 50 | var oEl = that.getEl(); 51 | var oTargetEl = $(oConf.targetEl); 52 | var nWidth = oTargetEl.outerWidth(); 53 | var nHeight = oTargetEl.outerHeight(); 54 | // 位置 55 | oEl.css({top:0, left: 0, width: nWidth, height: nHeight}); 56 | // 调整大小 57 | that.fileEl.outerWidth(nWidth); 58 | that.fileEl.outerHeight(nHeight); 59 | } 60 | 61 | function fInitInput() { 62 | var that = this; 63 | var oConf = that.rawConfig; 64 | var oIpt = that.fileEl; 65 | oIpt.on('change', function (oEvent) { 66 | var aFiles = oIpt.get(0).files; 67 | var oFile = aFiles[0]; 68 | if (!oFile) { 69 | return; 70 | } 71 | var sType = oFile.type; 72 | var nFileSize = oFile.size; 73 | // 检查是否能上传 74 | if (oConf.check && !oConf.check.call(that, oFile, sType, nFileSize)) { 75 | that.clear(); 76 | return; 77 | } 78 | // 上传文件 79 | var oPopup; 80 | BaseUpload.uploadFile(oFile, { 81 | name: oConf.name, 82 | url: oConf.url, 83 | data: oConf.data, 84 | call: function () { 85 | oPopup && oPopup.close(); 86 | oConf.call && oConf.call.apply(that, arguments); 87 | }, 88 | error: function () { 89 | oPopup && oPopup.close(); 90 | alert('出现错误,请重试'); 91 | oConf.error && oConf.error.apply(that, arguments); 92 | }, 93 | progress: function (nProgress) { 94 | if (!oPopup) { 95 | oPopup = new Popup({ 96 | content: '
正在上传:' + nProgress + '%
', 97 | hasNoHeader: true 98 | }); 99 | } 100 | oPopup.getEl().find('div.js-progress').html('正在上传:' + nProgress + '%'); 101 | oConf.progress && oConf.progress.apply(that, arguments); 102 | } 103 | }); 104 | that.clear(); 105 | }); 106 | 107 | // 进入提示条 108 | var oPopup; 109 | new BaseUpload({ 110 | input: that.fileEl, 111 | check: oConf.check, 112 | url: oConf.url, 113 | name: oConf.name, 114 | data: oConf.data, 115 | call: function () { 116 | oPopup && oPopup.close(); 117 | oConf.call && oConf.call.apply(that, arguments); 118 | }, 119 | error: function () { 120 | oPopup && oPopup.close(); 121 | alert('出现错误,请重试'); 122 | oConf.error && oConf.error.apply(that, arguments); 123 | }, 124 | progress: function (nProgress) { 125 | if (!oPopup) { 126 | oPopup = new Popup({ 127 | content: '
正在上传:' + nProgress + '%
', 128 | hasNoHeader: true 129 | }); 130 | } 131 | oPopup.getEl().find('div.js-progress').html('正在上传:' + nProgress + '%'); 132 | oConf.progress && oConf.progress.apply(that, arguments); 133 | } 134 | }); 135 | } 136 | 137 | function fClear() { 138 | var that = this; 139 | that.fileEl.val(''); 140 | } 141 | 142 | })(window); -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/component/component.js: -------------------------------------------------------------------------------- 1 | (function (window, undefined) { 2 | var Component = Base.createClass('main.component.Component'); 3 | var Event = Base.getClass('main.base.Event'); 4 | $.extend(Component, { 5 | _cIndex: 1, 6 | _domQueue: [], 7 | _tpl: '
', 8 | setEvents: fStaticSetEvents 9 | }); 10 | 11 | $.extend(Component.prototype, Event, { 12 | initialize: fInitialize, 13 | render: fRender, 14 | getEl: fGetEl, 15 | html: fHtml, 16 | getData: fGetData, 17 | // 禁止滚动 18 | forbidScroll: fForbidScroll, 19 | // 重写emit 20 | emit: fEmit, 21 | // 内部方法 22 | _setCustomEvent: _fSetCustomEvent, 23 | _setDomEvent: _fSetDomEvent 24 | }); 25 | 26 | function fStaticSetEvents() { 27 | var that = this; 28 | var aQueue = Component._domQueue; 29 | var oQueue; 30 | while (aQueue.length) { 31 | oQueue = aQueue.shift(); 32 | oQueue._setDomEvent(); 33 | oQueue.emit('render'); 34 | } 35 | } 36 | 37 | function fInitialize(oConf) { 38 | var that = this; 39 | that.rawConfig = oConf; 40 | that.domId = 'jsCpn' + (Component._cIndex++); 41 | that._setCustomEvent(); 42 | Component._domQueue.push(that); 43 | oConf.renderTo && that.render(); 44 | } 45 | 46 | function fRender() { 47 | var that = this; 48 | var oConf = that.rawConfig; 49 | var oRenderTo = $(oConf.renderTo); 50 | var sRenderBy = oConf.renderBy || 'append'; 51 | var oEl = that.getEl(); 52 | oRenderTo[sRenderBy](oEl); 53 | that._setDomEvent(); 54 | that.emit('render'); 55 | } 56 | 57 | function fGetEl() { 58 | var that = this; 59 | if (that.$el) { 60 | return that.$el; 61 | } 62 | var oEl = $('#' + that.domId); 63 | if (oEl.get(0)) { 64 | that.$el = oEl; 65 | return oEl; 66 | } 67 | 68 | var sHtml = that.html(); 69 | that.$el = $(sHtml); 70 | return that.$el; 71 | } 72 | 73 | function fHtml() { 74 | var that = this; 75 | var oConf = that.rawConfig; 76 | var oConstructor = that.constructor; 77 | var sTpl = oConstructor._tpl || Component._tpl; 78 | var oData = that.getData(that.rawConfig); 79 | var sHtml = Base.tpl(sTpl, oData); 80 | // id 和 class 81 | /* jshint ignore:start */ 82 | sHtml = sHtml.replace(/^(\<\w+)([ \>])/, '$1' + ' id="' + that.domId + '"$2'); 83 | /* jshint ignore:end */ 84 | sHtml = sHtml.replace('class="', 'class="' + (oConf.cls || '') + ' '); 85 | return sHtml; 86 | } 87 | 88 | function fGetData(oConf) { 89 | return oConf; 90 | } 91 | 92 | function fForbidScroll(oEl, bForbid) { 93 | $(oEl).css('overflow', bForbid === false ? 'auto' : 'hidden'); 94 | } 95 | 96 | function fEmit(sName) { 97 | var that = this; 98 | if (sName === 'render') { 99 | if (that.rendered) { 100 | return; 101 | } 102 | that.rendered = true; 103 | } 104 | Event.emit.apply(that, arguments); 105 | } 106 | 107 | function _fSetCustomEvent() { 108 | var that = this; 109 | if (that._setedCustomEvent) { 110 | return; 111 | } 112 | that._setedCustomEvent = true; 113 | var oConf = that.rawConfig; 114 | var oConstructor = that.constructor; 115 | $.each(oConstructor.listeners, function (_, oEvent) { 116 | oEvent.type === 'custom' && oEvent.name && oEvent.handler && that.on(oEvent.name, oEvent.handler); 117 | }); 118 | $.each(oConf.listeners, function (sName, fCb) { 119 | Base.isFunction(fCb) && that.on(sName, fCb); 120 | }); 121 | } 122 | 123 | function _fSetDomEvent() { 124 | var that = this; 125 | if (that._setedDomEvent) { 126 | return; 127 | } 128 | that._setedDomEvent = true; 129 | var oConf = that.rawConfig; 130 | var oEl = that.getEl(); 131 | var oConstructor = that.constructor; 132 | // 构造器上的事件 133 | $.each(oConstructor.listeners, function (_, oEvent) { 134 | oEvent.type !== 'custom' && _fBind(oEvent.name, oEvent); 135 | }); 136 | // 配置上面的事件 137 | $.each(oConf.listeners, function (sName, oEvent) { 138 | Base.isObject(oEvent) && _fBind(sName, oEvent); 139 | }); 140 | // 删除dom事件队列 141 | for (var i = Component._domQueue.length - 1; i >= 0; i--) { 142 | if (Component._domQueue[i] === that) { 143 | Component._domQueue.splice(i, 1); 144 | } 145 | } 146 | function _fBind(sName, oEvent) { 147 | var aMatch = sName.match(/^(\S+)\s*(.*)$/); 148 | var sEvent = $.trim(aMatch[1]); 149 | var sSelector = $.trim(aMatch[2]); 150 | var fHandler = oEvent.handler; 151 | if (Base.isFunction(fHandler)) { 152 | if (sSelector) { 153 | oEvent.type === 'bind' && oEl.find(sSelector).on(sEvent, Base.bind(fHandler, that)); 154 | oEvent.type !== 'bind' && oEl.on(sEvent, sSelector, Base.bind(fHandler, that)); 155 | } else { 156 | oEl.on(sEvent, Base.bind(fHandler, that)); 157 | } 158 | } 159 | } 160 | } 161 | 162 | })(window); -------------------------------------------------------------------------------- /src/main/java/com/nowcoder/controller/NewsController.java: -------------------------------------------------------------------------------- 1 | package com.nowcoder.controller; 2 | 3 | import com.nowcoder.model.*; 4 | import com.nowcoder.service.CommentService; 5 | import com.nowcoder.service.NewsService; 6 | import com.nowcoder.service.QiniuService; 7 | import com.nowcoder.service.UserService; 8 | import com.nowcoder.util.ToutiaoUtil; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.ui.Model; 14 | import org.springframework.util.StreamUtils; 15 | import org.springframework.web.bind.annotation.*; 16 | import org.springframework.web.multipart.MultipartFile; 17 | import org.springframework.web.util.HtmlUtils; 18 | 19 | import javax.servlet.http.HttpServletResponse; 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.util.ArrayList; 23 | import java.util.Date; 24 | import java.util.List; 25 | 26 | @Controller 27 | public class NewsController { 28 | private static final Logger logger = LoggerFactory.getLogger(NewsController.class); 29 | 30 | @Autowired 31 | NewsService newsService; 32 | 33 | @Autowired 34 | UserService userService; 35 | 36 | @Autowired 37 | QiniuService qiniuService; 38 | 39 | @Autowired 40 | CommentService commentService; 41 | 42 | @Autowired 43 | HostHolder hostHolder; 44 | 45 | 46 | @RequestMapping(path = {"/news/{newsId}"},method = {RequestMethod.GET}) 47 | public String newsDetail(@PathVariable("newsId") int newsId, Model model){ 48 | News news = newsService.getById(newsId); 49 | if(news != null){ 50 | // 这条资讯关联的所有评论都找出来 51 | List comments = commentService.getCommentsByEntity(news.getId(), EntityType.ENTITY_NEWS); 52 | List commentVOs = new ArrayList(); 53 | for (Comment comment : comments) { 54 | ViewObject commentVO = new ViewObject(); 55 | commentVO.set("comment", comment); 56 | commentVO.set("user", userService.getUser(comment.getUserId())); 57 | commentVOs.add(commentVO); 58 | } 59 | model.addAttribute("comments", commentVOs); 60 | } 61 | 62 | model.addAttribute("news", news); //资讯 63 | model.addAttribute("owner", userService.getUser(news.getUserId())); //得到资讯的作者 64 | return "detail"; 65 | } 66 | 67 | @RequestMapping(path = {"/addComment"}, method = {RequestMethod.POST}) 68 | public String addComment(@RequestParam("newsId") int newsId, 69 | @RequestParam("content") String content) { 70 | try { 71 | content = HtmlUtils.htmlEscape(content); 72 | // 过滤content 73 | Comment comment = new Comment(); 74 | comment.setUserId(hostHolder.getUser().getId()); 75 | comment.setContent(content); 76 | comment.setEntityId(newsId); 77 | comment.setEntityType(EntityType.ENTITY_NEWS); 78 | comment.setCreatedDate(new Date()); 79 | comment.setStatus(0); 80 | 81 | commentService.addComment(comment); 82 | // 更新news里的评论数量 83 | int count = commentService.getCommentCount(comment.getEntityId(), comment.getEntityType()); 84 | newsService.updateCommentCount(comment.getEntityId(), count); 85 | // 怎么异步化 86 | } catch (Exception e) { 87 | logger.error("增加评论失败" + e.getMessage()); 88 | } 89 | return "redirect:/news/" + String.valueOf(newsId); 90 | } 91 | 92 | //展示图片, 服务器给客户端 response, 所以直接body出来,不用渲染 93 | @RequestMapping(path = {"/image"}, method = {RequestMethod.GET}) 94 | @ResponseBody 95 | public void getImage(@RequestParam("name") String imageName, 96 | HttpServletResponse response) { 97 | try { 98 | response.setContentType("image/jpeg"); 99 | StreamUtils.copy(new FileInputStream(new 100 | File(ToutiaoUtil.IMAGE_DIR + imageName)), response.getOutputStream()); 101 | } catch (Exception e) { 102 | logger.error("读取图片错误" + imageName + e.getMessage()); 103 | } 104 | } 105 | 106 | @RequestMapping(path = {"/uploadImage/"}, method = {RequestMethod.POST}) 107 | @ResponseBody 108 | public String uploadImage(@RequestParam("file") MultipartFile file) { 109 | try { 110 | //直接从service层存图片 111 | //String fileUrl = newsService.saveImage(file); 112 | String fileUrl = qiniuService.saveImage(file); // 就是写了一个接口 113 | if (fileUrl == null) { 114 | return ToutiaoUtil.getJSONString(1, "上传图片失败"); 115 | } 116 | return ToutiaoUtil.getJSONString(0, fileUrl); 117 | } catch (Exception e) { 118 | logger.error("上传图片失败" + e.getMessage()); 119 | return ToutiaoUtil.getJSONString(1, "上传失败"); 120 | } 121 | } 122 | 123 | @RequestMapping(path = {"/user/addNews/"}, method = {RequestMethod.POST}) 124 | @ResponseBody 125 | public String addNews(@RequestParam("image") String image, 126 | @RequestParam("title") String title, 127 | @RequestParam("link") String link) { 128 | try { 129 | News news = new News(); 130 | news.setCreatedDate(new Date()); 131 | news.setTitle(title); 132 | news.setImage(image); 133 | news.setLink(link); 134 | if (hostHolder.getUser() != null) { 135 | news.setUserId(hostHolder.getUser().getId()); 136 | } else { 137 | // 设置一个匿名用户 138 | news.setUserId(3); 139 | } 140 | newsService.addNews(news); 141 | return ToutiaoUtil.getJSONString(0); 142 | } catch (Exception e) { 143 | logger.error("添加资讯失败" + e.getMessage()); 144 | return ToutiaoUtil.getJSONString(1, "发布失败"); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/resources/templates/detail.html: -------------------------------------------------------------------------------- 1 | #parse("header.html") 2 |
3 |
4 |
5 | 6 |
7 | #if($like>0) 8 | 9 | #else 10 | 11 | #end 12 | #if($like<0) 13 | 14 | #else 15 | 16 | #end 17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 |

25 | $!{news.title} 26 |

27 |
28 | $!{news.link} 29 | 30 | $!{news.commentCount} 31 | 32 |
33 |
34 |
35 | 50 | 51 | 52 |
53 | 69 | 70 |
71 | #if($user) 72 | 评论 ($!{news.commentCount}) 73 |
74 | 75 | 76 |
77 | 79 | 80 |
81 |
82 | 83 |
84 |
85 | #else 86 | 89 | #end 90 |
91 | 92 |
93 | #foreach($commentvo in $comments) 94 |
95 | 96 | 97 | 98 |
99 |

100 | $date.format('yyyy-MM-dd HH:mm:ss', $!{commentvo.comment.createdDate}) 101 | 102 |

103 |
$!{commentvo.comment.content}
104 |
105 |
106 | #end 107 |
108 | 109 |
110 | 152 | 153 |
154 | #parse("footer.html") -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/base/base.js: -------------------------------------------------------------------------------- 1 | (function (window, undefined) { 2 | var Base = window.Base = fCreateClass('main.base.Base'); 3 | $.extend(Base, { 4 | ready: fReady, 5 | tpl: fTpl, 6 | bind: fBind, 7 | createClass: fCreateClass, 8 | getClass: fGetClass, 9 | mix: fMix, 10 | inherit: fInherit 11 | }); 12 | 13 | // 类型判断 14 | var aType = ['Array', 'Object', 'Function', 'String', 'Number', 'RegExp']; 15 | for (var i = 0, l = aType.length; i < l; i++) { 16 | (function (sName) { 17 | Base['is' + sName] = function (obj) { 18 | return Object.prototype.toString.call(obj) === '[object ' + sName + ']'; 19 | }; 20 | })(aType[i]); 21 | } 22 | 23 | function fReady(sName, oParam) { 24 | var that = this; 25 | var oSpecialMap = {'document': document, 'body': document.body, 'window': window}; 26 | // 调整参数 27 | if (arguments.length === 1) { 28 | oParam = sName; 29 | sName = 'page' + (new Date().getTime()) + (Math.random()); 30 | } 31 | // 每个页面的脚本作为一个对象处理 32 | var oClass = that.createClass('JSAction.' + sName); 33 | $.extend(oClass, oParam); 34 | $(function () { 35 | oClass.initialize.call(oClass); 36 | // 绑定的事件 37 | $.each(oClass.binds, function (sEventName, sCbName) { 38 | var aMatch = sEventName.match(/^(\S+)\s*(.*)$/); 39 | var sEvent = aMatch[1]; 40 | var sSelector = aMatch[2]; 41 | // 兼容字符串和函数回调 42 | if (that.isString(sCbName)) { 43 | sCbName = oClass[sCbName]; 44 | } 45 | // 绑定事件 46 | $(oSpecialMap[sSelector] || sSelector).on(sEvent, function (oEvent) { 47 | sCbName.call(oClass, oEvent); 48 | }); 49 | }); 50 | // 代理的事件 51 | $.each(oClass.events, function (sEventName, sCbName) { 52 | var aMatch = sEventName.match(/^(\S+)\s*(.*)$/); 53 | var sEvent = aMatch[1]; 54 | var sSelector = aMatch[2]; 55 | // 兼容字符串和函数回调 56 | if (that.isString(sCbName)) { 57 | sCbName = oClass[sCbName]; 58 | } 59 | // 绑定事件 60 | $(document).on(sEvent, sSelector, function (oEvent) { 61 | sCbName.call(oClass, oEvent); 62 | }); 63 | }); 64 | }); 65 | return oClass; 66 | } 67 | 68 | function fTpl(sTpl, oData) { 69 | var that = this; 70 | sTpl = $.trim(sTpl); 71 | return sTpl.replace(/#{(.*?)}/g, function (sStr, sName) { 72 | return oData[sName] === undefined || oData[sName] === null ? '' : oData[sName]; 73 | }); 74 | } 75 | 76 | function fBind(f, oTarget) { 77 | var aArgs = [].slice.call(arguments, 2); 78 | return function () { 79 | var aCallArgs = aArgs.concat([].slice.call(arguments, 0)); 80 | var oResult = f.apply(oTarget, aCallArgs); 81 | aCallArgs.length = 0; 82 | return oResult; 83 | }; 84 | } 85 | 86 | function fCreateClass(sPackage, sClassName) { 87 | var Class = function () { 88 | var that = this.constructor === Class ? this : arguments.callee; 89 | if (that.initialize) { 90 | return that.initialize.apply(that, arguments); 91 | } 92 | }; 93 | if (arguments.length === 0) { 94 | return Class; 95 | } 96 | 97 | var oParent; 98 | if (arguments.length === 2 && typeof sPackage !== 'string') { 99 | oParent = _fFixParent(sPackage); 100 | oParent[sClassName] = Class; 101 | } else { 102 | var sNamespace = sClassName ? (sPackage + '.' + sClassName) : sPackage; 103 | oParent = window; 104 | var aName = sNamespace.split('.'); 105 | for (var i = 0, l = aName.length; i < l; i++) { 106 | var sName = aName[i]; 107 | if (i + 1 === l) { 108 | if (typeof oParent[sName] === 'function') { 109 | Class = oParent[sName]; 110 | } else { 111 | oParent[sName] = Class; 112 | } 113 | } else { 114 | oParent[sName] = _fFixParent(oParent[sName]); 115 | oParent = oParent[sName]; 116 | } 117 | } 118 | } 119 | return Class; 120 | 121 | function _fFixParent(oParent) { 122 | var sType = typeof oParent; 123 | if (sType === 'undefined') { 124 | oParent = {}; 125 | } else if (sType === 'number' || sType === 'string' || sType === 'boolean') { 126 | oParent = new oParent.constructor(oParent); 127 | } 128 | return oParent; 129 | } 130 | } 131 | 132 | function fGetClass(sPackage, sClassName) { 133 | var Class; 134 | try { 135 | var sNamespace = sClassName ? (sPackage + '.' + sClassName) : sPackage; 136 | var aName = sNamespace.split('.'); 137 | var oParent = window; 138 | 139 | for (var i = 0, l = aName.length; i < l; i++) { 140 | var sName = aName[i]; 141 | if (i + 1 === l) { 142 | Class = oParent[sName]; 143 | } else { 144 | oParent = oParent[sName]; 145 | } 146 | } 147 | if (!Class) { 148 | throw new Error('找不到类:' + sNamespace); 149 | } 150 | return Class; 151 | } catch (e) { 152 | throw e; 153 | } 154 | } 155 | 156 | function fMix(oChild, oParent, oExtend, oExtendPrototype) { 157 | var that = this; 158 | if (!oChild || !oParent) { 159 | return; 160 | } 161 | oChild.superClass = oChild.superClass || {}; 162 | $.each(oParent, function (sKey, oVal) { 163 | if (that.isFunction(oVal)) { 164 | if (!oChild.superClass[sKey]) { 165 | oChild.superClass[sKey] = oVal; 166 | } else { 167 | /* jshint ignore:start */ 168 | var _function = oChild.superClass[sKey]; 169 | oChild.superClass[sKey] = function (_property, fFunc) { 170 | return function () { 171 | fFunc.apply(this, arguments); 172 | oParent[_property].apply(this, arguments); 173 | }; 174 | }(sKey, _function); 175 | /* jshint ignore:end */ 176 | } 177 | } else { 178 | oChild.superClass[sKey] = oVal; 179 | } 180 | oChild[sKey] = oChild[sKey] || oVal; 181 | }); 182 | 183 | oExtend && $.extend(oChild, oExtend); 184 | if (oParent.toString != oParent.constructor.prototype.toString) { 185 | oChild.superClass.toString = function () { 186 | oParent.toString.apply(oChild, arguments); 187 | }; 188 | } 189 | oExtendPrototype && oChild.prototype && oParent.prototype && that.inherit(oChild, oParent, oExtendPrototype); 190 | return oChild; 191 | } 192 | 193 | function fInherit(oChild, oParent, oExtend) { 194 | var Inheritance = function() {}; 195 | Inheritance.prototype = oParent.prototype; 196 | oChild.prototype = new Inheritance(); 197 | oChild.prototype.constructor = oChild; 198 | oChild.superConstructor = oParent; 199 | oChild.superClass = oParent.prototype; 200 | oParent._onInherit && oParent._onInherit(oChild); 201 | oExtend && $.extend(oChild.prototype, oExtend); 202 | } 203 | 204 | })(window); -------------------------------------------------------------------------------- /src/main/resources/static/scripts/main/component/popupLogin.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | var PopupLogin = Base.createClass('main.component.PopupLogin'); 3 | var Popup = Base.getClass('main.component.Popup'); 4 | var Component = Base.getClass('main.component.Component'); 5 | var Util = Base.getClass('main.base.Util'); 6 | 7 | Base.mix(PopupLogin, Component, { 8 | _tpl: [ 9 | '
', 10 | '
', 11 | '
', 12 | '', 13 | '
', 14 | '
', 15 | '
', 16 | '', 17 | '
', 18 | '
', 19 | '
', 20 | '
', 21 | '', 22 | '
', 23 | '
', 24 | '
', 25 | '', 29 | '
', 30 | '
', 31 | '
'].join(''), 32 | listeners: [{ 33 | name: 'render', 34 | type: 'custom', 35 | handler: function () { 36 | var that = this; 37 | var oEl = that.getEl(); 38 | that.emailIpt = oEl.find('div.js-email'); 39 | that.pwdIpt = oEl.find('div.js-pwd'); 40 | that.initCpn(); 41 | } 42 | }, { 43 | name: 'click a.js-login', 44 | handler: function (oEvent) { 45 | oEvent.preventDefault(); 46 | var that = this; 47 | // 值检查 48 | if (!that.checkVal()) { 49 | return; 50 | } 51 | var oData = that.val(); 52 | $.ajax({ 53 | url: '/login/', 54 | type: 'post', 55 | dataType: 'json', 56 | data: { 57 | username: oData.email, 58 | password: oData.pwd, 59 | rember: oData.rember ? 1 : 0 60 | } 61 | }).done(function (oResult) { 62 | if (oResult.code === 0) { 63 | // window.location.reload(); 64 | that.emit('login'); 65 | } else { 66 | oResult.msgname && that.iptError(that.emailIpt, oResult.msgname); 67 | oResult.msgpwd && that.iptError(that.pwdIpt, oResult.msgpwd); 68 | } 69 | }).fail(function () { 70 | alert('出现错误,请重试'); 71 | }); 72 | } 73 | }, { 74 | name: 'click a.js-register', 75 | handler: function (oEvent) { 76 | oEvent.preventDefault(); 77 | var that = this; 78 | // 值检查 79 | if (!that.checkVal()) { 80 | return; 81 | } 82 | var oData = that.val(); 83 | $.ajax({ 84 | url: '/reg/', 85 | type: 'post', 86 | dataType: 'json', 87 | data: { 88 | username: oData.email, 89 | password: oData.pwd 90 | } 91 | }).done(function (oResult) { 92 | if (oResult.code === 0) { 93 | // window.location.reload(); 94 | that.emit('register'); 95 | } else { 96 | oResult.msgname && that.iptError(that.emailIpt, oResult.msgname); 97 | oResult.msgpwd && that.iptError(that.pwdIpt, oResult.msgpwd); 98 | } 99 | }).fail(function () { 100 | alert('出现错误,请重试'); 101 | }); 102 | } 103 | }], 104 | show: fStaticShow 105 | }, { 106 | initialize: fInitialize, 107 | initCpn: fInitCpn, 108 | val: fVal, 109 | checkVal: fCheckVal, 110 | iptSucc: fIptSucc, 111 | iptError: fIptError, 112 | iptNone: fIptNone 113 | }); 114 | 115 | function fStaticShow(oConf) { 116 | var that = this; 117 | var oLogin = new PopupLogin(oConf); 118 | var oPopup = new Popup({ 119 | width: 540, 120 | content: oLogin.html() 121 | }); 122 | oLogin._popup = oPopup; 123 | Component.setEvents(); 124 | } 125 | 126 | function fInitialize(oConf) { 127 | var that = this; 128 | delete oConf.renderTo; 129 | PopupLogin.superClass.initialize.apply(that, arguments); 130 | } 131 | 132 | function fInitCpn() { 133 | var that = this; 134 | that.emailIpt.find('input').on('focus', Base.bind(that.iptNone, that, that.emailIpt)); 135 | that.pwdIpt.find('input').on('focus', Base.bind(that.iptNone, that, that.pwdIpt)); 136 | } 137 | 138 | function fVal(oData) { 139 | var that = this; 140 | var oEl = that.getEl(); 141 | var oEmailIpt = that.emailIpt.find('input'); 142 | var oPwdIpt = that.pwdIpt.find('input'); 143 | var oRemberChk = oEl.find('.js-rember'); 144 | if (arguments.length === 0) { 145 | return { 146 | email: $.trim(oEmailIpt.val()), 147 | pwd: $.trim(oPwdIpt.val()), 148 | rember: oRemberChk.prop('checked') 149 | }; 150 | } else { 151 | oEmailIpt.val($.trim(oData.email)); 152 | oPwdIpt.val($.trim(oData.pwd)); 153 | oRemberChk.prop('checked', !!oData.rember); 154 | } 155 | } 156 | 157 | function fCheckVal() { 158 | var that = this; 159 | var oData = that.val(); 160 | var bRight = true; 161 | /* 162 | if (!Util.isEmail(oData.email)) { 163 | that.iptError(that.emailIpt, '请填写正确的邮箱'); 164 | bRight = false; 165 | }*/ 166 | if (!oData.pwd) { 167 | that.iptError(that.pwdIpt, '密码不能为空'); 168 | bRight = false; 169 | } else if (oData.pwd.length < 6) { 170 | that.iptError(that.pwdIpt, '密码不能小于6位'); 171 | bRight = false; 172 | } 173 | return bRight; 174 | } 175 | 176 | function fIptSucc(oIpt) { 177 | var that = this; 178 | oIpt = $(oIpt); 179 | that.iptNone(oIpt); 180 | oIpt.addClass('success'); 181 | if (!oIpt.find('.icon-ok-sign').get(0)) { 182 | oIpt.append(''); 183 | } 184 | } 185 | 186 | function fIptError(oIpt, sMsg) { 187 | var that = this; 188 | oIpt = $(oIpt); 189 | that.iptNone(oIpt); 190 | oIpt.addClass('error'); 191 | if (!oIpt.find('.icon-remove-sign').get(0)) { 192 | oIpt.append(''); 193 | } 194 | var oSpan = oIpt.find('.input-tip'); 195 | if (!oSpan.get(0)) { 196 | oSpan = $(''); 197 | oIpt.append(oSpan); 198 | } 199 | oSpan.html($.trim(sMsg)); 200 | } 201 | 202 | function fIptNone(oIpt) { 203 | var that = this; 204 | $(oIpt).removeClass('error success'); 205 | } 206 | })(window); -------------------------------------------------------------------------------- /src/main/resources/templates/home.html: -------------------------------------------------------------------------------- 1 | #parse("header.html") 2 | 3 |
4 | 60 | 61 |
62 |
63 |
64 | 65 | #set($cur_date = '') 66 | #foreach($vo in $vos) 67 | #if ($cur_date != $date.format('yyyy-MM-dd', $vo.news.createdDate)) 68 | #if ($foreach.index > 0) 69 |
## 上一个要收尾 70 | #end 71 | #set($cur_date = $date.format('yyyy-MM-dd', $vo.news.createdDate)) 72 |

73 | 74 | 头条资讯   $date.format('yyyy-MM-dd', $vo.news.createdDate) 75 |

76 | 77 |
78 | #end 79 |
80 |
81 | #if ($vo.like > 0) 82 | 83 | #else 84 | 85 | #end 86 | #if($vo.like < 0) 87 | 88 | #else 89 | 90 | #end 91 |
92 |
93 |
94 | 95 |
96 |
97 |

98 | $!{vo.news.title} 99 |

100 |
101 | $!{vo.news.link} 102 | 103 | $!{vo.news.commentCount} 104 | 105 |
106 |
107 |
108 | 124 | 125 | 126 |
127 | 128 | 133 | #if ($foreach.count == $vos.size()) ##最后有个元素要收尾 134 |
135 | #end 136 | 137 | #end 138 | 139 | 140 |
141 |
142 |
143 | 144 |
145 | 146 | 147 | #if ($pop) 148 | 151 | 152 | #end 153 | 154 | #parse("footer.html") -------------------------------------------------------------------------------- /src/main/resources/static/styles/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=3.2.1');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=3.2.1') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=3.2.1') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=3.2.1') format('truetype'),url('../fonts/fontawesome-webfont.svg#fontawesomeregular?v=3.2.1') format('svg');font-weight:normal;font-style:normal;}[class^="icon-"],[class*=" icon-"],.caret{font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;*margin-right:.3em;} 2 | [class^="icon-"]:before,[class*=" icon-"]:before{text-decoration:inherit;display:inline-block;speak:none;} 3 | .icon-large:before{vertical-align:-10%;font-size:1.3333333333333333em;} 4 | a [class^="icon-"],a [class*=" icon-"]{display:inline;} 5 | [class^="icon-"].icon-fixed-width,[class*=" icon-"].icon-fixed-width{display:inline-block;width:1.1428571428571428em;text-align:right;padding-right:0.2857142857142857em;}[class^="icon-"].icon-fixed-width.icon-large,[class*=" icon-"].icon-fixed-width.icon-large{width:1.4285714285714286em;} 6 | .icons-ul{margin-left:2.142857142857143em;list-style-type:none;}.icons-ul>li{position:relative;} 7 | .icons-ul .icon-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;text-align:center;line-height:inherit;} 8 | [class^="icon-"].hide,[class*=" icon-"].hide{display:none;} 9 | .icon-muted{color:#eeeeee;} 10 | .icon-light{color:#ffffff;} 11 | .icon-dark{color:#333333;} 12 | .icon-border{border:solid 1px #eeeeee;padding:.2em .25em .15em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} 13 | .icon-2x{font-size:2em;}.icon-2x.icon-border{border-width:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} 14 | .icon-3x{font-size:3em;}.icon-3x.icon-border{border-width:3px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} 15 | .icon-4x{font-size:4em;}.icon-4x.icon-border{border-width:4px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;} 16 | .icon-5x{font-size:5em;}.icon-5x.icon-border{border-width:5px;-webkit-border-radius:7px;-moz-border-radius:7px;border-radius:7px;} 17 | .pull-right{float:right;} 18 | .pull-left{float:left;} 19 | [class^="icon-"].pull-left,[class*=" icon-"].pull-left{margin-right:.3em;} 20 | [class^="icon-"].pull-right,[class*=" icon-"].pull-right{margin-left:.3em;} 21 | [class^="icon-"],[class*=" icon-"]{display:inline;width:auto;height:auto;line-height:normal;vertical-align:baseline;background-image:none;background-position:0% 0%;background-repeat:repeat;margin-top:0;} 22 | .icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"]{background-image:none;} 23 | .btn [class^="icon-"].icon-large,.nav [class^="icon-"].icon-large,.btn [class*=" icon-"].icon-large,.nav [class*=" icon-"].icon-large{line-height:.9em;} 24 | .btn [class^="icon-"].icon-spin,.nav [class^="icon-"].icon-spin,.btn [class*=" icon-"].icon-spin,.nav [class*=" icon-"].icon-spin{display:inline-block;} 25 | .nav-tabs [class^="icon-"],.nav-pills [class^="icon-"],.nav-tabs [class*=" icon-"],.nav-pills [class*=" icon-"],.nav-tabs [class^="icon-"].icon-large,.nav-pills [class^="icon-"].icon-large,.nav-tabs [class*=" icon-"].icon-large,.nav-pills [class*=" icon-"].icon-large{line-height:.9em;} 26 | .btn [class^="icon-"].pull-left.icon-2x,.btn [class*=" icon-"].pull-left.icon-2x,.btn [class^="icon-"].pull-right.icon-2x,.btn [class*=" icon-"].pull-right.icon-2x{margin-top:.18em;} 27 | .btn [class^="icon-"].icon-spin.icon-large,.btn [class*=" icon-"].icon-spin.icon-large{line-height:.8em;} 28 | .btn.btn-small [class^="icon-"].pull-left.icon-2x,.btn.btn-small [class*=" icon-"].pull-left.icon-2x,.btn.btn-small [class^="icon-"].pull-right.icon-2x,.btn.btn-small [class*=" icon-"].pull-right.icon-2x{margin-top:.25em;} 29 | .btn.btn-large [class^="icon-"],.btn.btn-large [class*=" icon-"]{margin-top:0;}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x,.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-top:.05em;} 30 | .btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x{margin-right:.2em;} 31 | .btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-left:.2em;} 32 | .nav-list [class^="icon-"],.nav-list [class*=" icon-"]{line-height:inherit;} 33 | .icon-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:-35%;}.icon-stack [class^="icon-"],.icon-stack [class*=" icon-"]{display:block;text-align:center;position:absolute;width:100%;height:100%;font-size:1em;line-height:inherit;*line-height:2em;} 34 | .icon-stack .icon-stack-base{font-size:2em;*line-height:1em;} 35 | .icon-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear;} 36 | a .icon-stack,a .icon-spin{display:inline-block;text-decoration:none;} 37 | @-moz-keyframes spin{0%{-moz-transform:rotate(0deg);} 100%{-moz-transform:rotate(359deg);}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg);} 100%{-webkit-transform:rotate(359deg);}}@-o-keyframes spin{0%{-o-transform:rotate(0deg);} 100%{-o-transform:rotate(359deg);}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg);} 100%{-ms-transform:rotate(359deg);}}@keyframes spin{0%{transform:rotate(0deg);} 100%{transform:rotate(359deg);}}.icon-rotate-90:before{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);} 38 | .icon-rotate-180:before{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);} 39 | .icon-rotate-270:before{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);} 40 | .icon-flip-horizontal:before{-webkit-transform:scale(-1, 1);-moz-transform:scale(-1, 1);-ms-transform:scale(-1, 1);-o-transform:scale(-1, 1);transform:scale(-1, 1);} 41 | .icon-flip-vertical:before{-webkit-transform:scale(1, -1);-moz-transform:scale(1, -1);-ms-transform:scale(1, -1);-o-transform:scale(1, -1);transform:scale(1, -1);} 42 | a .icon-rotate-90:before,a .icon-rotate-180:before,a .icon-rotate-270:before,a .icon-flip-horizontal:before,a .icon-flip-vertical:before{display:inline-block;} 43 | .icon-glass:before{content:"\f000";} 44 | .icon-music:before{content:"\f001";} 45 | .icon-search:before{content:"\f002";} 46 | .icon-envelope-alt:before{content:"\f003";} 47 | .icon-heart:before{content:"\f004";} 48 | .icon-star:before{content:"\f005";} 49 | .icon-star-empty:before{content:"\f006";} 50 | .icon-user:before{content:"\f007";} 51 | .icon-film:before{content:"\f008";} 52 | .icon-th-large:before{content:"\f009";} 53 | .icon-th:before{content:"\f00a";} 54 | .icon-th-list:before{content:"\f00b";} 55 | .icon-ok:before{content:"\f00c";} 56 | .icon-remove:before{content:"\f00d";} 57 | .icon-zoom-in:before{content:"\f00e";} 58 | .icon-zoom-out:before{content:"\f010";} 59 | .icon-power-off:before,.icon-off:before{content:"\f011";} 60 | .icon-signal:before{content:"\f012";} 61 | .icon-gear:before,.icon-cog:before{content:"\f013";} 62 | .icon-trash:before{content:"\f014";} 63 | .icon-home:before{content:"\f015";} 64 | .icon-file-alt:before{content:"\f016";} 65 | .icon-time:before{content:"\f017";} 66 | .icon-road:before{content:"\f018";} 67 | .icon-download-alt:before{content:"\f019";} 68 | .icon-download:before{content:"\f01a";} 69 | .icon-upload:before{content:"\f01b";} 70 | .icon-inbox:before{content:"\f01c";} 71 | .icon-play-circle:before{content:"\f01d";} 72 | .icon-rotate-right:before,.icon-repeat:before{content:"\f01e";} 73 | .icon-refresh:before{content:"\f021";} 74 | .icon-list-alt:before{content:"\f022";} 75 | .icon-lock:before{content:"\f023";} 76 | .icon-flag:before{content:"\f024";} 77 | .icon-headphones:before{content:"\f025";} 78 | .icon-volume-off:before{content:"\f026";} 79 | .icon-volume-down:before{content:"\f027";} 80 | .icon-volume-up:before{content:"\f028";} 81 | .icon-qrcode:before{content:"\f029";} 82 | .icon-barcode:before{content:"\f02a";} 83 | .icon-tag:before{content:"\f02b";} 84 | .icon-tags:before{content:"\f02c";} 85 | .icon-book:before{content:"\f02d";} 86 | .icon-bookmark:before{content:"\f02e";} 87 | .icon-print:before{content:"\f02f";} 88 | .icon-camera:before{content:"\f030";} 89 | .icon-font:before{content:"\f031";} 90 | .icon-bold:before{content:"\f032";} 91 | .icon-italic:before{content:"\f033";} 92 | .icon-text-height:before{content:"\f034";} 93 | .icon-text-width:before{content:"\f035";} 94 | .icon-align-left:before{content:"\f036";} 95 | .icon-align-center:before{content:"\f037";} 96 | .icon-align-right:before{content:"\f038";} 97 | .icon-align-justify:before{content:"\f039";} 98 | .icon-list:before{content:"\f03a";} 99 | .icon-indent-left:before{content:"\f03b";} 100 | .icon-indent-right:before{content:"\f03c";} 101 | .icon-facetime-video:before{content:"\f03d";} 102 | .icon-picture:before{content:"\f03e";} 103 | .icon-pencil:before{content:"\f040";} 104 | .icon-map-marker:before{content:"\f041";} 105 | .icon-adjust:before{content:"\f042";} 106 | .icon-tint:before{content:"\f043";} 107 | .icon-edit:before{content:"\f044";} 108 | .icon-share:before{content:"\f045";} 109 | .icon-check:before{content:"\f046";} 110 | .icon-move:before{content:"\f047";} 111 | .icon-step-backward:before{content:"\f048";} 112 | .icon-fast-backward:before{content:"\f049";} 113 | .icon-backward:before{content:"\f04a";} 114 | .icon-play:before{content:"\f04b";} 115 | .icon-pause:before{content:"\f04c";} 116 | .icon-stop:before{content:"\f04d";} 117 | .icon-forward:before{content:"\f04e";} 118 | .icon-fast-forward:before{content:"\f050";} 119 | .icon-step-forward:before{content:"\f051";} 120 | .icon-eject:before{content:"\f052";} 121 | .icon-chevron-left:before{content:"\f053";} 122 | .icon-chevron-right:before{content:"\f054";} 123 | .icon-plus-sign:before{content:"\f055";} 124 | .icon-minus-sign:before{content:"\f056";} 125 | .icon-remove-sign:before{content:"\f057";} 126 | .icon-ok-sign:before{content:"\f058";} 127 | .icon-question-sign:before{content:"\f059";} 128 | .icon-info-sign:before{content:"\f05a";} 129 | .icon-screenshot:before{content:"\f05b";} 130 | .icon-remove-circle:before{content:"\f05c";} 131 | .icon-ok-circle:before{content:"\f05d";} 132 | .icon-ban-circle:before{content:"\f05e";} 133 | .icon-arrow-left:before{content:"\f060";} 134 | .icon-arrow-right:before{content:"\f061";} 135 | .icon-arrow-up:before{content:"\f062";} 136 | .icon-arrow-down:before{content:"\f063";} 137 | .icon-mail-forward:before,.icon-share-alt:before{content:"\f064";} 138 | .icon-resize-full:before{content:"\f065";} 139 | .icon-resize-small:before{content:"\f066";} 140 | .icon-plus:before{content:"\f067";} 141 | .icon-minus:before{content:"\f068";} 142 | .icon-asterisk:before{content:"\f069";} 143 | .icon-exclamation-sign:before{content:"\f06a";} 144 | .icon-gift:before{content:"\f06b";} 145 | .icon-leaf:before{content:"\f06c";} 146 | .icon-fire:before{content:"\f06d";} 147 | .icon-eye-open:before{content:"\f06e";} 148 | .icon-eye-close:before{content:"\f070";} 149 | .icon-warning-sign:before{content:"\f071";} 150 | .icon-plane:before{content:"\f072";} 151 | .icon-calendar:before{content:"\f073";} 152 | .icon-random:before{content:"\f074";} 153 | .icon-comment:before{content:"\f075";} 154 | .icon-magnet:before{content:"\f076";} 155 | .icon-chevron-up:before{content:"\f077";} 156 | .icon-chevron-down:before{content:"\f078";} 157 | .icon-retweet:before{content:"\f079";} 158 | .icon-shopping-cart:before{content:"\f07a";} 159 | .icon-folder-close:before{content:"\f07b";} 160 | .icon-folder-open:before{content:"\f07c";} 161 | .icon-resize-vertical:before{content:"\f07d";} 162 | .icon-resize-horizontal:before{content:"\f07e";} 163 | .icon-bar-chart:before{content:"\f080";} 164 | .icon-twitter-sign:before{content:"\f081";} 165 | .icon-facebook-sign:before{content:"\f082";} 166 | .icon-camera-retro:before{content:"\f083";} 167 | .icon-key:before{content:"\f084";} 168 | .icon-gears:before,.icon-cogs:before{content:"\f085";} 169 | .icon-comments:before{content:"\f086";} 170 | .icon-thumbs-up-alt:before{content:"\f087";} 171 | .icon-thumbs-down-alt:before{content:"\f088";} 172 | .icon-star-half:before{content:"\f089";} 173 | .icon-heart-empty:before{content:"\f08a";} 174 | .icon-signout:before{content:"\f08b";} 175 | .icon-linkedin-sign:before{content:"\f08c";} 176 | .icon-pushpin:before{content:"\f08d";} 177 | .icon-external-link:before{content:"\f08e";} 178 | .icon-signin:before{content:"\f090";} 179 | .icon-trophy:before{content:"\f091";} 180 | .icon-github-sign:before{content:"\f092";} 181 | .icon-upload-alt:before{content:"\f093";} 182 | .icon-lemon:before{content:"\f094";} 183 | .icon-phone:before{content:"\f095";} 184 | .icon-unchecked:before,.icon-check-empty:before{content:"\f096";} 185 | .icon-bookmark-empty:before{content:"\f097";} 186 | .icon-phone-sign:before{content:"\f098";} 187 | .icon-twitter:before{content:"\f099";} 188 | .icon-facebook:before{content:"\f09a";} 189 | .icon-github:before{content:"\f09b";} 190 | .icon-unlock:before{content:"\f09c";} 191 | .icon-credit-card:before{content:"\f09d";} 192 | .icon-rss:before{content:"\f09e";} 193 | .icon-hdd:before{content:"\f0a0";} 194 | .icon-bullhorn:before{content:"\f0a1";} 195 | .icon-bell:before{content:"\f0a2";} 196 | .icon-certificate:before{content:"\f0a3";} 197 | .icon-hand-right:before{content:"\f0a4";} 198 | .icon-hand-left:before{content:"\f0a5";} 199 | .icon-hand-up:before{content:"\f0a6";} 200 | .icon-hand-down:before{content:"\f0a7";} 201 | .icon-circle-arrow-left:before{content:"\f0a8";} 202 | .icon-circle-arrow-right:before{content:"\f0a9";} 203 | .icon-circle-arrow-up:before{content:"\f0aa";} 204 | .icon-circle-arrow-down:before{content:"\f0ab";} 205 | .icon-globe:before{content:"\f0ac";} 206 | .icon-wrench:before{content:"\f0ad";} 207 | .icon-tasks:before{content:"\f0ae";} 208 | .icon-filter:before{content:"\f0b0";} 209 | .icon-briefcase:before{content:"\f0b1";} 210 | .icon-fullscreen:before{content:"\f0b2";} 211 | .icon-group:before{content:"\f0c0";} 212 | .icon-link:before{content:"\f0c1";} 213 | .icon-cloud:before{content:"\f0c2";} 214 | .icon-beaker:before{content:"\f0c3";} 215 | .icon-cut:before{content:"\f0c4";} 216 | .icon-copy:before{content:"\f0c5";} 217 | .icon-paperclip:before,.icon-paper-clip:before{content:"\f0c6";} 218 | .icon-save:before{content:"\f0c7";} 219 | .icon-sign-blank:before{content:"\f0c8";} 220 | .icon-reorder:before{content:"\f0c9";} 221 | .icon-list-ul:before{content:"\f0ca";} 222 | .icon-list-ol:before{content:"\f0cb";} 223 | .icon-strikethrough:before{content:"\f0cc";} 224 | .icon-underline:before{content:"\f0cd";} 225 | .icon-table:before{content:"\f0ce";} 226 | .icon-magic:before{content:"\f0d0";} 227 | .icon-truck:before{content:"\f0d1";} 228 | .icon-pinterest:before{content:"\f0d2";} 229 | .icon-pinterest-sign:before{content:"\f0d3";} 230 | .icon-google-plus-sign:before{content:"\f0d4";} 231 | .icon-google-plus:before{content:"\f0d5";} 232 | .icon-money:before{content:"\f0d6";} 233 | .icon-caret-down:before{content:"\f0d7";} 234 | .icon-caret-up:before{content:"\f0d8";} 235 | .icon-caret-left:before{content:"\f0d9";} 236 | .icon-caret-right:before{content:"\f0da";} 237 | .icon-columns:before{content:"\f0db";} 238 | .icon-sort:before{content:"\f0dc";} 239 | .icon-sort-down:before{content:"\f0dd";} 240 | .icon-sort-up:before{content:"\f0de";} 241 | .icon-envelope:before{content:"\f0e0";} 242 | .icon-linkedin:before{content:"\f0e1";} 243 | .icon-rotate-left:before,.icon-undo:before{content:"\f0e2";} 244 | .icon-legal:before{content:"\f0e3";} 245 | .icon-dashboard:before{content:"\f0e4";} 246 | .icon-comment-alt:before{content:"\f0e5";} 247 | .icon-comments-alt:before{content:"\f0e6";} 248 | .icon-bolt:before{content:"\f0e7";} 249 | .icon-sitemap:before{content:"\f0e8";} 250 | .icon-umbrella:before{content:"\f0e9";} 251 | .icon-paste:before{content:"\f0ea";} 252 | .icon-lightbulb:before{content:"\f0eb";} 253 | .icon-exchange:before{content:"\f0ec";} 254 | .icon-cloud-download:before{content:"\f0ed";} 255 | .icon-cloud-upload:before{content:"\f0ee";} 256 | .icon-user-md:before{content:"\f0f0";} 257 | .icon-stethoscope:before{content:"\f0f1";} 258 | .icon-suitcase:before{content:"\f0f2";} 259 | .icon-bell-alt:before{content:"\f0f3";} 260 | .icon-coffee:before{content:"\f0f4";} 261 | .icon-food:before{content:"\f0f5";} 262 | .icon-file-text-alt:before{content:"\f0f6";} 263 | .icon-building:before{content:"\f0f7";} 264 | .icon-hospital:before{content:"\f0f8";} 265 | .icon-ambulance:before{content:"\f0f9";} 266 | .icon-medkit:before{content:"\f0fa";} 267 | .icon-fighter-jet:before{content:"\f0fb";} 268 | .icon-beer:before{content:"\f0fc";} 269 | .icon-h-sign:before{content:"\f0fd";} 270 | .icon-plus-sign-alt:before{content:"\f0fe";} 271 | .icon-double-angle-left:before{content:"\f100";} 272 | .icon-double-angle-right:before{content:"\f101";} 273 | .icon-double-angle-up:before{content:"\f102";} 274 | .icon-double-angle-down:before{content:"\f103";} 275 | .icon-angle-left:before{content:"\f104";} 276 | .icon-angle-right:before{content:"\f105";} 277 | .icon-angle-up:before{content:"\f106";} 278 | .icon-angle-down:before{content:"\f107";} 279 | .icon-desktop:before{content:"\f108";} 280 | .icon-laptop:before{content:"\f109";} 281 | .icon-tablet:before{content:"\f10a";} 282 | .icon-mobile-phone:before{content:"\f10b";} 283 | .icon-circle-blank:before{content:"\f10c";} 284 | .icon-quote-left:before{content:"\f10d";} 285 | .icon-quote-right:before{content:"\f10e";} 286 | .icon-spinner:before{content:"\f110";} 287 | .icon-circle:before{content:"\f111";} 288 | .icon-mail-reply:before,.icon-reply:before{content:"\f112";} 289 | .icon-github-alt:before{content:"\f113";} 290 | .icon-folder-close-alt:before{content:"\f114";} 291 | .icon-folder-open-alt:before{content:"\f115";} 292 | .icon-expand-alt:before{content:"\f116";} 293 | .icon-collapse-alt:before{content:"\f117";} 294 | .icon-smile:before{content:"\f118";} 295 | .icon-frown:before{content:"\f119";} 296 | .icon-meh:before{content:"\f11a";} 297 | .icon-gamepad:before{content:"\f11b";} 298 | .icon-keyboard:before{content:"\f11c";} 299 | .icon-flag-alt:before{content:"\f11d";} 300 | .icon-flag-checkered:before{content:"\f11e";} 301 | .icon-terminal:before{content:"\f120";} 302 | .icon-code:before{content:"\f121";} 303 | .icon-reply-all:before{content:"\f122";} 304 | .icon-mail-reply-all:before{content:"\f122";} 305 | .icon-star-half-full:before,.icon-star-half-empty:before{content:"\f123";} 306 | .icon-location-arrow:before{content:"\f124";} 307 | .icon-crop:before{content:"\f125";} 308 | .icon-code-fork:before{content:"\f126";} 309 | .icon-unlink:before{content:"\f127";} 310 | .icon-question:before{content:"\f128";} 311 | .icon-info:before{content:"\f129";} 312 | .icon-exclamation:before{content:"\f12a";} 313 | .icon-superscript:before{content:"\f12b";} 314 | .icon-subscript:before{content:"\f12c";} 315 | .icon-eraser:before{content:"\f12d";} 316 | .icon-puzzle-piece:before{content:"\f12e";} 317 | .icon-microphone:before{content:"\f130";} 318 | .icon-microphone-off:before{content:"\f131";} 319 | .icon-shield:before{content:"\f132";} 320 | .icon-calendar-empty:before{content:"\f133";} 321 | .icon-fire-extinguisher:before{content:"\f134";} 322 | .icon-rocket:before{content:"\f135";} 323 | .icon-maxcdn:before{content:"\f136";} 324 | .icon-chevron-sign-left:before{content:"\f137";} 325 | .icon-chevron-sign-right:before{content:"\f138";} 326 | .icon-chevron-sign-up:before{content:"\f139";} 327 | .icon-chevron-sign-down:before{content:"\f13a";} 328 | .icon-html5:before{content:"\f13b";} 329 | .icon-css3:before{content:"\f13c";} 330 | .icon-anchor:before{content:"\f13d";} 331 | .icon-unlock-alt:before{content:"\f13e";} 332 | .icon-bullseye:before{content:"\f140";} 333 | .icon-ellipsis-horizontal:before{content:"\f141";} 334 | .icon-ellipsis-vertical:before{content:"\f142";} 335 | .icon-rss-sign:before{content:"\f143";} 336 | .icon-play-sign:before{content:"\f144";} 337 | .icon-ticket:before{content:"\f145";} 338 | .icon-minus-sign-alt:before{content:"\f146";} 339 | .icon-check-minus:before{content:"\f147";} 340 | .icon-level-up:before{content:"\f148";} 341 | .icon-level-down:before{content:"\f149";} 342 | .icon-check-sign:before{content:"\f14a";} 343 | .icon-edit-sign:before{content:"\f14b";} 344 | .icon-external-link-sign:before{content:"\f14c";} 345 | .icon-share-sign:before{content:"\f14d";} 346 | .icon-compass:before{content:"\f14e";} 347 | .icon-collapse:before{content:"\f150";} 348 | .icon-collapse-top:before{content:"\f151";} 349 | .icon-expand:before{content:"\f152";} 350 | .icon-euro:before,.icon-eur:before{content:"\f153";} 351 | .icon-gbp:before{content:"\f154";} 352 | .icon-dollar:before,.icon-usd:before{content:"\f155";} 353 | .icon-rupee:before,.icon-inr:before{content:"\f156";} 354 | .icon-yen:before,.icon-jpy:before{content:"\f157";} 355 | .icon-renminbi:before,.icon-cny:before{content:"\f158";} 356 | .icon-won:before,.icon-krw:before{content:"\f159";} 357 | .icon-bitcoin:before,.icon-btc:before{content:"\f15a";} 358 | .icon-file:before{content:"\f15b";} 359 | .icon-file-text:before{content:"\f15c";} 360 | .icon-sort-by-alphabet:before{content:"\f15d";} 361 | .icon-sort-by-alphabet-alt:before{content:"\f15e";} 362 | .icon-sort-by-attributes:before{content:"\f160";} 363 | .icon-sort-by-attributes-alt:before{content:"\f161";} 364 | .icon-sort-by-order:before{content:"\f162";} 365 | .icon-sort-by-order-alt:before{content:"\f163";} 366 | .icon-thumbs-up:before{content:"\f164";} 367 | .icon-thumbs-down:before{content:"\f165";} 368 | .icon-youtube-sign:before{content:"\f166";} 369 | .icon-youtube:before{content:"\f167";} 370 | .icon-xing:before{content:"\f168";} 371 | .icon-xing-sign:before{content:"\f169";} 372 | .icon-youtube-play:before{content:"\f16a";} 373 | .icon-dropbox:before{content:"\f16b";} 374 | .icon-stackexchange:before{content:"\f16c";} 375 | .icon-instagram:before{content:"\f16d";} 376 | .icon-flickr:before{content:"\f16e";} 377 | .icon-adn:before{content:"\f170";} 378 | .icon-bitbucket:before{content:"\f171";} 379 | .icon-bitbucket-sign:before{content:"\f172";} 380 | .icon-tumblr:before{content:"\f173";} 381 | .icon-tumblr-sign:before{content:"\f174";} 382 | .icon-long-arrow-down:before{content:"\f175";} 383 | .icon-long-arrow-up:before{content:"\f176";} 384 | .icon-long-arrow-left:before{content:"\f177";} 385 | .icon-long-arrow-right:before{content:"\f178";} 386 | .icon-apple:before{content:"\f179";} 387 | .icon-windows:before{content:"\f17a";} 388 | .icon-android:before{content:"\f17b";} 389 | .icon-linux:before{content:"\f17c";} 390 | .icon-dribbble:before{content:"\f17d";} 391 | .icon-skype:before{content:"\f17e";} 392 | .icon-foursquare:before{content:"\f180";} 393 | .icon-trello:before{content:"\f181";} 394 | .icon-female:before{content:"\f182";} 395 | .icon-male:before{content:"\f183";} 396 | .icon-gittip:before{content:"\f184";} 397 | .icon-sun:before{content:"\f185";} 398 | .icon-moon:before{content:"\f186";} 399 | .icon-archive:before{content:"\f187";} 400 | .icon-bug:before{content:"\f188";} 401 | .icon-vk:before{content:"\f189";} 402 | .icon-weibo:before{content:"\f18a";} 403 | .icon-renren:before{content:"\f18b";} 404 | --------------------------------------------------------------------------------