基于Vue+Java实现的在线聊天APP系统设计与实现
全套資料下載地址:https://download.csdn.net/download/sheziqiong/85595798
一、需求分析
1.核心用戶分析
在線聊天系統(tǒng)主要針對一些年輕用戶群體以及因?yàn)楣ぷ餍枨蠖鴮τ趯?shí)時交流以及非實(shí)時交流有較大需求的群里。就青年群體而言,這一用戶群體特征比較鮮明,其主要需求為基礎(chǔ)聊天需求以及一些能夠凸顯個性的功能需求。在線聊天對于青年人來說也逐漸成為一種主流的設(shè)計(jì)方式。年輕人們通過在線交流和好友印象的可以了解到對方的性格,而且可以通過相互添加好友保持關(guān)系。而對于有工作需求的人來說能夠?qū)崟r交流以及處理未讀消息就顯得十分重要。
2. 系統(tǒng)的主要功能的概述
首先未注冊的用戶可以注冊賬號,已經(jīng)注冊的用戶可以使用賬號密碼進(jìn)行登錄。
用戶可以搜索好友,搜索之后可以進(jìn)行添加好友
主界面分為兩個部分,一個部分為消息盒子,一個部分為好友盒子
消息盒子主要存放未讀消息,如果有一個好友向你發(fā)送消息你沒有點(diǎn)到聊天框里查看的話就會在消息盒子界面顯示
好友盒子顯示如下幾個部分,好友列表,添加好友的入口,個人信息的入口,朋友驗(yàn)證的入口
所有的好友會在好友列表中展示,一開始所有的好友的在默認(rèn)分組。點(diǎn)擊好友之后可以進(jìn)入好友的資料卡頁面
可以在好友資料卡中可以查看好友的基本消息,以及會顯示好友的印象,當(dāng)點(diǎn)擊某個印象標(biāo)簽的時候會提示你可以進(jìn)行刪除。還可以在好友資料卡頁面點(diǎn)擊發(fā)送消息進(jìn)入聊天窗口。除此之外右上角點(diǎn)擊之后可以有刪除好友,移動好友,添加標(biāo)簽的選項(xiàng)
刪除好友:點(diǎn)擊之后好友將被刪除,你可以通過再次發(fā)送驗(yàn)證消息進(jìn)行添加
移動好友:可以將好友移動到指定的分組,如果分組不存在則創(chuàng)建分組,若移動后分組內(nèi)沒有成員則刪除分組。
添加標(biāo)簽:可以為好友添加一個標(biāo)簽。
當(dāng)進(jìn)入聊天框之后發(fā)送消息對方就可以發(fā)收到,點(diǎn)擊下載聊天記錄的按鈕就可以下載所有的聊天記錄,點(diǎn)擊刪除聊天記錄可以刪除和當(dāng)前用戶所有的云端記錄。
個人信息,在這里可以修改個人信息包括修改頭像,以及刪除別人給自己的標(biāo)簽,并且可以在此處退出登錄
3. 項(xiàng)目操作流程圖
4. 功能詳解
使用賬號密碼進(jìn)行登錄,登錄成功之后跳轉(zhuǎn)到主頁面中的消息盒子的頁面
賬號采用郵箱格式,密碼要求大于八位
消息盒子顯示你的所有的未讀消息,一旦消息已讀就會從消息盒子中去除
好友盒子有如下這些部分組成:新的朋友,我的賬號,朋友驗(yàn)證,好友列表
按照分組展示所有的好友,點(diǎn)擊好友可以進(jìn)入好友資料卡頁面
當(dāng)你發(fā)送的請求別人已經(jīng)處理完了或者別人向你發(fā)送了請求的話此處會有一個紅點(diǎn)表示消息數(shù)量。點(diǎn)擊進(jìn)入之后進(jìn)入驗(yàn)證消息模塊
點(diǎn)擊之后進(jìn)入個人資料卡,在這里可以修改姓名,頭像,性別,頭像要求小于 30kb,年齡要求不能為負(fù)數(shù),性別要求只能是男或者女,還可以在此處刪除自己的標(biāo)簽,也可以退出登錄。
可以進(jìn)行全局搜索,即不進(jìn)行任何輸入直接回車可以顯示所有的好友,并且可以進(jìn)行模糊搜索,只輸入名字的部分也可搜索到。并且可以添加年齡和性別的限制條件。點(diǎn)擊搜索結(jié)果可以進(jìn)入好友資料卡。在這里可以填寫驗(yàn)證消息,并且發(fā)送好友驗(yàn)證,自己不能添加自己,不能添加以及添加的好友,如果已經(jīng)發(fā)送過依次請求對方為響應(yīng)也不能發(fā)送。當(dāng)這里發(fā)送之后對方的朋友驗(yàn)證會出現(xiàn)紅點(diǎn)。
當(dāng)我們點(diǎn)擊朋友驗(yàn)證之后,進(jìn)入驗(yàn)證消息頁面,如果我們發(fā)送的消息被處理了,則會有一個紅點(diǎn)標(biāo)記,別人發(fā)送的請求我們可以選擇拒絕和接受。如果我們進(jìn)入了此頁面的話,如果存在我們發(fā)送的消息被處理了且我們自己之前未讀的,則會被設(shè)置為已讀。對于別人發(fā)給自己的請求,則必須在處理完之后才會被設(shè)置為已讀。
顯示好友的基本信息,好友的標(biāo)簽,點(diǎn)擊標(biāo)簽可以進(jìn)行刪除,并且可以在此頁面點(diǎn)擊發(fā)送消息進(jìn)入聊天框進(jìn)行聊天,此頁面中點(diǎn)擊右上角還可以進(jìn)行刪除好友,移動好友,添加標(biāo)簽。
輸入要移動的分組如果不存在則創(chuàng)建分組,若某個分組內(nèi)沒有了用戶則刪除分組,所有用戶默認(rèn)在默認(rèn)分組中
可以對一個用戶添加一個標(biāo)簽,添加重復(fù)標(biāo)簽沒有用
聊天界面可以雙方可以實(shí)時發(fā)送消息,顯示的時候自己的消息在右側(cè),對方的消息在左側(cè),且按時間排序,點(diǎn)擊下載按鈕可以進(jìn)行聊天記錄下載,點(diǎn)擊刪除按鈕可以刪除云端數(shù)據(jù)
5. 系統(tǒng)的頂級用例圖
6. 系統(tǒng)的原型圖設(shè)計(jì)
原型圖主要是用圖片的形式站輸出之前的功能模塊,并且也是后面前端 UI 的主要依據(jù)
登陸界面,注冊界面類似消息盒子界面
聊天界面 好友列表界面
搜索界面 好友申請界面
個人資料頁面 驗(yàn)證消息界面
項(xiàng)目的頁面和原型圖過多這里就不一一展示,詳情可見壓縮文件
二、數(shù)據(jù)庫設(shè)計(jì)
因?yàn)榱奶煜到y(tǒng)的所有功能基本上都是圍繞著用戶進(jìn)行的。聊天是用戶和用戶聊天,添加好友也是用戶添加用戶,印象管理也是用戶給用戶添加印象。所以主要的聯(lián)表操作都和 user 表有關(guān)。這里就先給出整個項(xiàng)目的 ER 圖
根據(jù) ER 圖可以構(gòu)建數(shù)據(jù)庫的物理設(shè)計(jì)如下
Redis 中的存儲結(jié)構(gòu)的說明,因?yàn)?Redis 的 Nosql 的性質(zhì)很適合用于存儲未讀的聊天記錄,我是用 Redis 中的哈希結(jié)構(gòu)進(jìn)行存儲未讀消息。每一條記錄規(guī)則如下鍵為 unread+userId,值為一個 Java 中的 Map<String, Map<String, String>> 的類型。Map<String, Map<String, String>> 的類型數(shù)據(jù)格式如下:
senderId: {nickname:xxx,content:xxx,pic:xxx }除此之外在 Redis 中單獨(dú)存儲一個哈希結(jié)構(gòu),鍵為 unreadNum+userId,值為未讀消息數(shù)量。
三、架構(gòu)設(shè)計(jì)
1.技術(shù)棧
(1)前端
①Vue 作為前端框架
②vue-router 進(jìn)行前端路由管理
③webpack 開發(fā) SPA(單頁面應(yīng)用)
④mint-UI 作為 UI 框架
⑤STOMP 實(shí)現(xiàn) Socket 通信的框架
⑥axios 發(fā)送請求
⑦sass(css 預(yù)處理器,進(jìn)行 CSS 代碼的編寫)
前端架構(gòu)說明:
Webpack 搭建項(xiàng)目前端框架,打包生成 SPA(單頁面)的移動端應(yīng)用。
1、 Webpack 配置文件
webpack.base.conf.js 為基礎(chǔ)配置 一些最基本的 loader 與 plugin 都在這里面 webpack.dev.conf.js 為開發(fā)環(huán)境配置,配置了適合開發(fā)環(huán)境的 sourceMap,能快速的定位開發(fā)環(huán)境代碼報錯位置 webpack.prod.conf.js 為生產(chǎn)環(huán)境下配置,關(guān)閉了 sourceMap,極大的減小了線上環(huán)境代碼包的大小,啟用了 UglifyJsPlugin 進(jìn)行代碼壓縮,使首屏加載速度低于 500ms。
2、 Src 目錄下為主要代碼, assets 文件夾下存儲著圖片,iconfont 等靜態(tài)資源。Router 文件夾下為 vue 的路由,控制著頁面的跳轉(zhuǎn)。 Views 文件夾下為視圖組件,我們開發(fā)的代碼主要在這里。每個人負(fù)責(zé)不同的模塊,采用 gitflow 工作流,幾乎沒有產(chǎn)生沖突。
(2)后端
①Spring Boot 作為后端框架
②Shiro 作為安全驗(yàn)證框架
③STOMP 協(xié)議作為通信協(xié)議
④Redis 存儲未讀消息
⑤MySQL 作為用戶信息等的存儲
(3)部署
①Docker 部署
(4)測試
①Selemiun
②textNG
(5)項(xiàng)目管理平臺
①github+git
后端架構(gòu)說明:
(1) 后端 package 說明如下:
① config 表示配置文件,里面存放諸如 Shiro,STOMP 的配置
② controller 放置 API 接口
③ dao 放置數(shù)據(jù)庫操作類和接口,其下的 impl 這個 package 表示 JAP 擴(kuò)展實(shí)現(xiàn)類
④ dto 存放數(shù)據(jù)庫表的交互數(shù)據(jù)結(jié)構(gòu)
⑤ entity 存放數(shù)據(jù)庫表中的映射
⑥ pojo:存放普通 Java 類,一般是存放工具類
⑦ processor:存放過濾器,攔截器和監(jiān)聽器
⑧ service:供其他模塊調(diào)用的服務(wù)類,采用接口和實(shí)現(xiàn)的方式。impl 為其實(shí)現(xiàn)類
⑨ vo 和前端交互的數(shù)據(jù)類(主要用于將數(shù)據(jù)格式轉(zhuǎn)化為和前端約定的格式)
(2) 使用如此結(jié)構(gòu)的原因
① 這些 package 中需要實(shí)現(xiàn) config 中的文件和 entity 中的文件,只要這兩個 package 的文件配置了,整個項(xiàng)目就可以運(yùn)行。之后的編寫只需要在其他包中添加自己的代碼即可
② 在完成了 config 和 entity 的文件之后,其他功能的編寫只需要注重代碼邏輯的實(shí)現(xiàn)即可
③ 當(dāng)多個人同時寫后端的時候,在寫自己的功能的時候只需要在對應(yīng)的 package 中編寫對應(yīng)的代碼即可互不干擾。
四、功能實(shí)現(xiàn)
后端實(shí)現(xiàn):
1) 基礎(chǔ)配置:
1. RedisConfig
主要是注入 RedisTemplate<Object, Object> 這個 bean
2. Shiro
首先建立自己的匹配器 TokenCredentialsMatcher 用于驗(yàn)證比較,此比較器實(shí)現(xiàn)了 CredentialsMatcher 接口,實(shí)現(xiàn)類 doCredentialsMatch 方法。我們通過 token 比較進(jìn)行驗(yàn)證用戶登錄狀態(tài)是否合法。
String token = (String) authenticationToken.getCredentials(); return TokenManager.verify(token); ```http://www.biyezuopin.vip然后再建立自己的 Reanlm:TokenRealm,自己配置的 TokenRealm 繼承自 AuthorizingRealm,并且再構(gòu)造函數(shù)的時候就將我們自己的 TokenCredentialsMatcher 作為此類的匹配器加入。這樣的話進(jìn)行比較的時候就不會調(diào)用默認(rèn)的比較匹配器而是調(diào)用我們自定義的比較器。我們重寫 supports 方法,只有當(dāng) support 方法返回 true 的時候才會之后的操作。return token instanceof MyToken
然后再重寫 doGetAuthenticationInfo 方法和 doGetAuthorizationInfo 方法,但是 doGetAuthorizationInfo 為授權(quán)方法,在我們本系統(tǒng)中并沒有使用,所以只給出前者核心代碼String token = (String)authenticationToken.getCredentials();
Integer userId = TokenManager.getId(token);
if(userId==null)
userId=0;
return new SimpleAuthenticationInfo(userId, token, getName());
@Override
public Object getPrincipal() {
Integer userId = -1;
if(TokenManager.verify(this.token)){
userId = TokenManager.getId(token);
}
return userId;
}
@Override
public Object getCredentials() {
return token;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put(“verification”, new AuthFilter());
shiroFilterFactoryBean.setFilters(filterMap);
Map<String, String> filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put(“/verification/”, “verification”);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilterFactoryBean;
}
@Bean
public SessionsSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(tokenRealm);
//關(guān)閉Shiro自帶的Session。
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
return ((HttpServletRequest) request).getHeader(“Authorization”) != null;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response){
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader(“Authorization”);
MyToken token = new MyToken(authorization.substring(7));
getSubject(request, response).login(token);
return true;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
if (isLoginAttempt(request, response)) {
executeLogin(request, response);
} else {
throw new Exception();
}
} catch (Exception e){
return false;
}
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response){
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
AuthMessage authMessage = AuthMessage.getAuthMessage(
“請先登錄”, “”, “l(fā)ogin /auth/login”,
“/auth/login”, “l(fā)ogin”, “application/json”);
httpServletResponse.setCharacterEncoding(“UTF-8”);
httpServletResponse.setHeader(“Content-Type”, “application/json;charset=UTF-8”);
String ret = JSON.toJSONString(authMessage);
httpServletResponse.setStatus(401);
try {
response.getWriter().write(ret);
response.getWriter().close();
} catch (IOException ex) {
ex.printStackTrace();
}
return false;
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(“/chat”).setAllowedOrigins(“*”).withSockJS(); //設(shè)置連接url并且設(shè)置跨域
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker(“/subscription”); //設(shè)置訂閱的url
config.setApplicationDestinationPrefixes(“/socket”); //設(shè)置訪問url前綴
}
userService.setUser(user);
userService.encode();
if(!userService.isRegistered()){
throw new HttpException(HttpStatus.UNAUTHORIZED, AuthMessage.getAuthMessage(
“賬號未注冊”,“”, “registration /auth/registration”,
“/auth/registration”, “registration”, “application/json”));
} else if(!userService.checkPassword()){
throw new HttpException(HttpStatus.UNAUTHORIZED, AuthMessage.getAuthMessage(
“賬號密碼不匹配”,“”, “l(fā)ogin /auth/login”,
“/auth/login”, “l(fā)ogin”, “application/json”));
}else{
response.setStatus(200);
return AuthMessage.getAuthMessage(
“登錄成功”,TokenManager.sign(userService.getUserInDataBase().getId()), “messages /management/messages”,
“/management/messages”, “messages”, “application/json”);
}
userService.setUser(user);
if(userService.isRegistered()){
throw new HttpException(HttpStatus.FORBIDDEN, AuthMessage.getAuthMessage(
“賬號已注冊”,“”, “registration /auth/registration”,
“/auth/registration”, “registration”, “application/json”));
}else if(user.getPassword().length()<8){
throw new HttpException(HttpStatus.FORBIDDEN, AuthMessage.getAuthMessage(
“密碼長度小于八位”,“”, “registration /auth/registration”,
“/auth/registration”, “registration”, “application/json”));
}else{
userService.encode();
userService.initPic();
userService.setAge(1);
userService.setGender(“男”);
userService.save();
response.setStatus(201);
return AuthMessage.getAuthMessage(
“注冊成功”, TokenManager.sign(userService.getUser().getId()), “messages /management/messages”,
“/management/messages”, “messages”, “application/json”);
}
{
senderId:xxx,
recipientId:xxx,
content:{“me”:”xxx”}
}
@MessageMapping(“/chat”)
public void sayHello(ChatMessage message){
User sender = userService.findById(message.getSenderId());
User recipient = userService.findById(message.getRecipientId());
if(sendernull||recipientnull)
return;
Map<String, String> content = message.getContent();;
String destination = “/subscription/” + recipient.getId();
String origin = “/subscription/” + sender.getId();
if(relationshipService.isFriend(recipient, sender)){
Record record = new Record(content.get(“my”), new Date(), sender, recipient);
userMessage.save(record);
userMessage.addMessage(recipient.getId(), sender.getId(), sender.getNickname(), content.get(“my”), sender.getPic());
messagingTemplate.convertAndSend(destination, new HashMap<String, Object>(){{
put(“senderId”, String.valueOf(sender.getId()));
put(“senderPic”, sender.getPic());
put(“senderName”, sender.getNickname());
put(“content”, new HashMap<String, String>(){{
put(“he”, content.get(“my”));
}}
);
}});
messagingTemplate.convertAndSend(origin, new HashMap<String, String>(){{
put(“content”,“發(fā)送成功”);
}});http://www.biyezuopin.vip
}else{
messagingTemplate.convertAndSend(origin, new HashMap<String, String>(){{
put(“content”,“對方不是你好友”);
}});
}
}
@RequestMapping(value = “/index”, method = {RequestMethod.GET})
public InfoMessage getIndex(@RequestParam Integer customer){
User user = userService.setUser();
if(!(customernull||customer-1)){
userMessage.readMessage(user.getId(),customer);
}
Map<Object, Object> unreadMessages = userMessage.getAllUnread(user.getId());
Map<Object, Object> unreadNum = userMessage.getAllUnreadNum(user.getId());
LinkMessage linkMessage = new LinkMessage(
“”,
“”,
“”,
“”);
return new InfoMessage(“獲取成功”, unreadMessages, unreadNum, new UserInformation(
user.getId(), user.getNickname(), user.getPic(), user.getGender(), user.getAge()
), linkMessage);
}
@RequestMapping(value = “/history/download”, method = {RequestMethod.GET})
public ResponseEntity download(@RequestParam Integer customer){
User user = userService.setUser();
List records = recordRepository.getChatRecord(user.getId(), customer, -1);
HttpHeaders headers = new HttpHeaders();
headers.add(“Cache-Control”, “no-cache, no-store, must-revalidate”);
headers.add(“Content-Disposition”, “attachment; filename=” + System.currentTimeMillis() + “.xls”);
headers.add(“Pragma”, “no-cache”);
headers.add(“Expires”, “0”);
headers.add(“Last-Modified”, new Date().toString());
headers.add(“ETag”, String.valueOf(System.currentTimeMillis()));
try {
File file = new File(“history/”+user.getId()+“with”+customer+“.txt”); // 相對路徑,如果沒有則要建立一個新的output.txt文件
if(!file.exists()) {
file.createNewFile(); // 創(chuàng)建新文件,有同名的文件的話直接覆蓋
}
FileWriter writer = new FileWriter(file);
BufferedWriter out = new BufferedWriter(writer);
for(RecordDto record: records){
out.write(“at “+ record.getTime()+”|”+record.getSenderId()+“–>”+record.getRecipientId()+“=”+record.getContent()+“rn”);
}
out.flush(); // 把緩存區(qū)內(nèi)容壓入文件
out.close();
return ResponseEntity.ok().headers(headers) .contentLength(file.length()).contentType(MediaType.parseMediaType(“application/octet-stream”)) .body(new FileSystemResource(file));
} catch (IOException e) {
e.printStackTrace();
}
File file = new File(“history/”+user.getId()+“with”+customer+“.txt”);
return ResponseEntity.ok().headers(headers) .contentLength(file.length()).contentType(MediaType.parseMediaType(“application/octet-stream”)) .body(new FileSystemResource(file));
}
好友列表的渲染
根據(jù)后端傳來的雙層數(shù)組進(jìn)行列表渲染,使用 Vue 中 value 和 key 的列表渲染方式,將不同分組的人員分開渲染,同時使用 URL 的傳值方式,使點(diǎn)擊不同的用戶,好友展示中展示不同的好友信息
<div v-for="(value,key) in friends" :key="key"><mt-cell :title="key"></mt-cell><mt-cellv-for="(value2,key2) in value":key="key2":to="'/home/showfriend?group='+key+'&id='+key2"><span>{{value2.nickname}}</span><img slot="icon" v-bind:src="value2.pic" width="28" height="28" /></mt-cell></div>添加好友模塊
使用 mint-ui 的搜索框組件進(jìn)行昵稱的輸入,同時使用兩個下拉框?qū)崿F(xiàn)年齡和性別的篩選,使用 Vue 的原生回車事件進(jìn)行提交
<mt-searchv-model="searchContent"cancel-text="取消"placeholder="輸入昵稱搜索"@keyup.enter.native="search"></mt-search>發(fā)送好友請求模塊
模塊初始化時,使用 LocalStorage 取出存儲的用戶信息,使用 axios 的錯誤捕獲來判斷是否成功發(fā)送好友請求
.catch(err => {MessageBox.alert("不能重復(fù)添加,或者添加自己").then(action => {});});個人信息修改模塊
個人修改的信息通過表單獲取,同時使用正則表達(dá)式判斷是否符合要求。圖片文件則進(jìn)行后綴以及大小的合法性判斷,不符合要求則不進(jìn)行修改
var regPos = /^\d+$/;if (this.info.nickname == "" ||this.info.account == "" ||this.info.pic == "" ||this.info.gender == "") {MessageBox.alert("所填項(xiàng)不能為空").then(action => {});return false;}else if(this.info.gender!="男" && this.info.gender !="女"){MessageBox.alert("性別只能為男或女").then(action => {});return false;}else if(!regPos.test(this.info.age)){MessageBox.alert("年齡只能為非負(fù)整數(shù)").then(action => {});return false;}var img = e.target.files[0];if (img.type !== "image/jpeg" &&img.type !== "image/png" &&img.type !== "image/gif") {MessageBox.alert("請選擇圖片文件").then(action => {});return false;} else if (img.size > 1024 * 30) {console.log(img.size);MessageBox.alert("選擇小于30kb的圖片").then(action => {});return false;http://www.biyezuopin.vip}好友展示模塊
通過 mint-ui 的上拉框進(jìn)行功能的選擇,可以選擇刪除好友,添加標(biāo)簽,移動好友的功能,添加標(biāo)簽時與以往的標(biāo)簽進(jìn)行比較,若有相同的則進(jìn)行去重操作
<mt-button icon="more" slot="right" @click.native="actionSheet"></mt-button>var add = {};add.targetId = this.info.id;add.contents = this.info.impressions;const impressions = new Set(add.contents);if(!impressions.has(value)){this.info.impressions.push(value);}console.log(impressions);add.contents = [...impressions.values()];好友驗(yàn)證模塊
使用兩個列表渲染,展示自己發(fā)送的請求,以及別人發(fā)送給你的請求,同時組件被創(chuàng)建時,自動請求后端,將所有的未讀消息置為已讀消息
this.axios //標(biāo)記好友請求已讀.post("/verification/user/validation-reading").then(response => {}).catch(err => {console.log(err); //異常});聊天模塊
Connection方法,建立stomp連接,并且為了防止websocket連接中斷,我采用了心跳?;罴夹g(shù),每60s發(fā)送一條無用消息,確保連接正常。 并且推出頁面會斷開socket連接,以減小服務(wù)器壓力。
發(fā)送消息,進(jìn)行了內(nèi)容判斷 發(fā)送內(nèi)容不能為空或者內(nèi)容長度不能超過 200
登錄注冊模塊
登錄后 將 token 與個人信息相關(guān)的數(shù)據(jù)存入 localstorage 內(nèi),退出登錄就直接清除 localstorage 內(nèi)全部數(shù)據(jù)。
注冊后,直接跳轉(zhuǎn)到主頁面,有良好的錯誤處理機(jī)制。
測試模塊
TESTNG 文件配置
使用 XML 配置要測試的模塊,包括注冊功能,登錄功能,添加好友功能,聊天功能等
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="Link聊天自動化測試"><test name="測試"><classes><class name="register.Register"/><class name="login.Login" /><class name="addfriend.Addfriend"/><class name="impression.Impression"/><class name="chat.Chat"/><class name="deletefriend.Deletefriend"/></classes></test> </suite>瀏覽器驅(qū)動公共代碼
公共代碼中實(shí)現(xiàn)公共操作,包括啟動瀏覽器驅(qū)動,瀏覽器驅(qū)動關(guān)閉,共享變量 DRIVER,供所有模塊使用
@BeforeClass public static void setUp() throws Exception {System.out.println("啟動瀏覽器");System.setProperty("webdriver.chrome.driver","./src/chromedriver.exe");driver = new ChromeDriver();driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); }注冊模塊自動化測試代碼
使用 selenium 進(jìn)行瀏覽器的操作,輸入待注冊的賬號,注冊完成后跳轉(zhuǎn)到主頁面進(jìn)行登錄,判斷是否注冊成功
Main.setUp(); Main.driver.get(RegisterURL); Main.driver.findElement(By.name("user_register")).sendKeys(this.email); Main.driver.findElement(By.name("user_name")).sendKeys(this.name); Main.driver.findElement(By.name("user_pass")).sendKeys(this.psw); Main.driver.findElement(By.cssSelector(".btn_register")).click(); Thread.sleep(3000); String current_url = Main.driver.getCurrentUrl(); try {Assert.assertEquals(current_url,this.SuccessURL,"注冊驗(yàn)證失敗!!"); }catch (Exception e){Main.tearDown(); } Main.tearDown();登錄模塊自動化代碼
在輸入框中輸入賬號與密碼,點(diǎn)擊登錄,頁面加載完成后判斷是否轉(zhuǎn)跳到主頁面
Main.setUp(); Main.driver.get(Main.BaseURL); WebElement email = Main.driver.findElement(By.name("user_login")); WebElement psw = Main.driver.findElement(By.name("user_pass")); WebElement button = Main.driver.findElement(By.cssSelector(".btn_login")); email.sendKeys(Main.email); psw.sendKeys(Main.psw); button.click();印象模塊自動化測試代碼
根據(jù)提供的賬號登錄到主頁面,點(diǎn)擊好友框,添加印象,然后匹配數(shù)據(jù)庫中的印象表,查看是否添加成功
Main.driver.findElement(By.xpath("//*[text()='康王']")).click(); Main.driver.findElement(By.className("mintui-more")).click(); Thread.sleep(2000); Main.driver.findElement(By.xpath("//*[text()='添加標(biāo)簽']")).click(); Main.driver.findElement(By.cssSelector("input")).sendKeys(this.impre); Main.driver.findElement(By.xpath("//*[text()='確定']")).click();好友刪除自動化測試代碼
點(diǎn)擊頁面刪除好友按鈕,退回到好友列表界面,查看是否刪除該好友
Main.driver.findElement(By.className("mintui-more")).click(); Main.driver.findElement(By.xpath("//*[text()='刪除好友']")).click(); Main.driver.findElement(By.xpath("//*[text()='確定']")).click(); Main.driver.findElement(By.xpath("//*[text()='確定']")).click();try{WebElement a = Main.driver.findElement(By.xpath("//*[text()='康王']"));Thread.sleep(4000); }catch (Exception e){Main.tearDown(); } Main.tearDown();聊天界面自動化測試代碼
點(diǎn)擊預(yù)訂的好友,點(diǎn)擊發(fā)送消息,跳轉(zhuǎn)到消息界面,輸入框中輸入消息,點(diǎn)擊發(fā)送,查看頁面中是否渲染出該條消息,并登陸另一賬號查看是否接受到該消息
Main.driver.findElement(By.className("mint-button--large")).click(); Main.driver.findElement(By.className("footer_inpuit")).sendKeys("呆呆呆呆"); Main.driver.findElement(By.className("footer_button")).click(); try{WebElement a = Main.driver.findElement(By.xpath("//*[text()='確定']")); }catch(Exception e){Assert.fail("發(fā)送消息失敗!!");Main.tearDown(); }五、成果展示
登錄頁面 注冊頁面
消息盒子頁面(無消息) 消息盒子(有消息)
好友盒子(無朋友驗(yàn)證) 好友盒子(有朋友驗(yàn)證)
在好友列表中點(diǎn)擊好友之后顯示的頁面 點(diǎn)擊右上角的點(diǎn)之后的頁面
個人信息 搜索結(jié)果界面
搜索點(diǎn)擊好友后的界面 可以發(fā)送驗(yàn)證消息(帶信息)
聊天界面
驗(yàn)證消息
全套資料下載地址:https://download.csdn.net/download/sheziqiong/85595798
總結(jié)
以上是生活随笔為你收集整理的基于Vue+Java实现的在线聊天APP系统设计与实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python发明家_Python版多图表
- 下一篇: 撸代码更有劲了(这应该算是福利吧)