八个步骤实现一个Web项目(在线聊天室)

您所在的位置:网站首页 一个完整的web项目开发流程图 八个步骤实现一个Web项目(在线聊天室)

八个步骤实现一个Web项目(在线聊天室)

2024-07-10 06:26| 来源: 网络整理| 查看: 265

实现一个在线网页的聊天室

Hello,今天给大家带来的是我的一个Web项目的开发过程的相关步骤,这个项目实现的功能是一个Web在线聊天室,简单的来说就是实现在网页版的聊天框,能够实现对于用户信息进行注册,登录,在网页上收发消息的功能。 这个项目也实现了我和别的小伙伴一起实现在线聊天的功能,这是我实现的Web聊天室网页链接地址:[http://47.100.138.17:8080/chatroom/index.html] 感兴趣的小伙伴可以注册登录呦在网上尝试一下聊天。 话不多说,我们直接开始对于开发过程进行实现吧:

第一步:首先是第一步对于需求分析创建需要的数据库表单

对于用户使用Web聊天室实现来说,需要用户用自己的账号,密码登录,同时有自己设置的昵称信息,头像信息;在登录之后有聊天室需要提供频道来使用户在其中进行交流;在交流的时候需要用户去发送消息,不同的用户会在不同的时间发不同的消息内容。 因此呢,根据这些需求就设计了 User(用户表)、channel(频道表)、message(消息表) 三个表单信息:

create table user( id int primary key auto_increment, username varchar(15) not null unique comment '账号', password varchar(15) not null comment '密码', nickname varchar(20) not null comment '昵称', head varchar(50) comment '头像url(相对路径)', logout_time datetime comment '退出登录的时间' ) comment '用户表'; create table channel( id int primary key auto_increment, name varchar(20) not null unique comment '频道名称' )comment '频道'; create table message( id int primary key auto_increment, user_id int comment '消息发送方:用户id', user_nickname varchar(20) comment '消息发送方:用户昵称(历史消息展示需要)', channel_id int comment '消息接收方:频道id', content varchar(255) comment '消息内容', send_time datetime comment '消息发送时间', foreign key (user_id) references user(id), foreign key (channel_id) references channel(id) ) comment '发送的消息记录';

三个表单的关系在navicat的EP图中表现是如下的: 在这里插入图片描述

第二步:创建一个Mavaen项目,将三个表单的实体类放在Model中

在这里插入图片描述 根据MySQL中数据库设计的信息在实体类中实现其属性,利用@Getter、@Setter、@ToString注解快速实现对于类相关方法的生产(需要导入lombok的依赖包)。 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

第三步:设计关键性的工具类:数据库操作的JDBC工具类;json和java对象转换,session操作的Web工具类 (1)对于JDBC的工具类

在JDBC工具类设计中提供连接连接数据库和释放数据库资源的关键方法。同时为保证线程安全部分功能使用懒汉式的双重校验锁的形式来实现。实现代码如下:

//和数据库连接的工具类 public class DBUtil { //定义一个单例的数据源来连接对象 private static MysqlDataSource DS=null; //懒汉式的双重校验锁的形式 private static MysqlDataSource getDS(){ if (DS==null) { synchronized (DBUtil.class) { if (DS == null) { //确保只有当前的操作能够访问数据库 DS = new MysqlDataSource(); //设置数据库连接的属性值 DS.setURL("jdbc:mysql://127.0.0.1:3306/onlinechatroom"); DS.setUser("root"); DS.setPassword("123456"); DS.setUseSSL(false); DS.setUseUnicode(true); DS.setCharacterEncoding("utf-8"); } } } return DS; } //数据库的连接方法实现,数据库的关闭方法实现 public static Connection getConnection(){ try { return getDS().getConnection(); } catch (SQLException e) { throw new RuntimeException("数据库连接异常",e); } } public static void close(Connection c , Statement s){ close(c,s,null); } public static void close(Connection c, Statement s, ResultSet r){ try { if (r!=null) r.close(); if (s!=null) s.close(); if (c!=null) c.close(); } catch (SQLException e) { throw new RuntimeException("数据库释放资源出错",e); } } } (2)对于Web工具类

在Web工具类设计中提供Java对象转为json字符串,json字符串转为Java对象,获取当前登录用户的session信息的功能。为保证线程安全,使用懒汉式的双重校验锁的写法。具体实现代码的如下:

public class WebUtil { public static final String LOCAL_HEAD_PATH="E://TMP"; //从json中读取到java对象,则jackson库中通过ObjectMapper实现了将数据集或对象转换的实现。 private static ObjectMapper M=null; //使用懒汉式的双重校验锁的单例模式 private static ObjectMapper getMapper(){ if (M==null){ synchronized (WebUtil.class){ if (M==null){ M=new ObjectMapper(); //SimpleDateFormat是日期工具类能够实现将文本和日期的双重转化 SimpleDateFormat df=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //设置自定义的时间结构 M.setDateFormat(df); } } } return M; } //实现将 JAVA对象————>json字符串 的方法 public static String Write(Object o){ try { //通过 return getMapper().writeValueAsString(o); } catch (JsonProcessingException e) { throw new RuntimeException("将JAVA对象转为json字符串时出错",e); } } //反序列化设计:将JSON字符串————>java对象 //两个重载的方法:InputStream 和 String 来进行转换 //inputStream 字节流输入 读取的数据 public static T read(InputStream inputStream,Class tClass){ try { return getMapper().readValue(inputStream,tClass); } catch (IOException e) { throw new RuntimeException("将json字符串转化为JAVA对象时出错",e); } } public static T read(String string ,Class tClass){ try { return getMapper().readValue(string,tClass); } catch (IOException e) { throw new RuntimeException("将json字符串转化为JAVA对象时出错", e); } } //对于session的操作 获取session中的用户信息 public static User getLoginUser(HttpSession session) { if (session != null) { //获取登录Session中的user信息 由于getAttribute 返回值是任意类型的 所以需要进行类型的强制转型 //获取的键和登录时设置的键一样 return (User) session.getAttribute("user"); } return null; } } 第四步:实现用户的注册功能 (1)对于用户注册的前端处理实现

在前端中需要创建相应的标签来让用户将自己的用户名,密码,昵称等相关信息输入当中,在标签中设置required则表示必须填写的内容。 用户将需要填写的内容在浏览器上输入完毕之后由前端页面将用户信息获取保存, 将前端中标记好的相关用户信息,放在一个formdata表单中进行存储。创建好格式,调用一个ajax请求,将当前页面的信息进行上传后端,用callback函数做接收信息的处理,如果成功就返回到登录页面,如果是失败就跳提示注册失败的原因。 在这里插入图片描述 举例:对于头像文件信息,设置为一个event事件,将event事件传入下方中showHead中。通过获取其中的文件将其保存在vue框架中的head中,在将文件信息写入到body中发送。 实现代码如下:

DOCTYPE html> 在线聊天室 聊天室注册 用户名 密码 昵称 头像 {{ errorMessage }} 返回登录 let app = new Vue({ el: "#app", data: { errorMessage: "", username: "", password: "", nickname: "", head: { file: "", //在这里保存选择的文件 src: "", //选择好图片还没上传,客户端本地有一个的图片 }, }, methods: { //注册选择头像,显示预览图片 //e是传入的事件对象 showHead: function (e){ //获取选择的文件: 通过e.target.file 可获取 在上面标签中的input中的 @chang let headFile = e.target.files[0]; //保存信息 用上面的vue框架信息里面的file地址保存图片 app.head.file= headFile; //将文件的信息转化为url 调用Url中的信息 app.head.src=URL.createObjectURL(headFile); }, register: function (){ //注册功能的实现 //使用FormData对象作为form-data格式上传的数据 //创建FormData格式的对象来调用该形式 let formData=new FormData(); //添加数据,利用append将相关参数进行设置 使用 k v 模型 k参数和APP中设置的参数一样 formData.append("username",app.username); formData.append("password",app.password); formData.append("nickname",app.nickname); //如果上传了头像的信息 if (app.head.file){ //将头像的信息传入当中 formData.append("headFile",app.head.file); } ajax({ method: "post", //当前html位置是在/views/register.html url: "../register",//当前html是在/views/register.html //上传文件,使用form-data格式,但是不能设置这个Content-Type //body中放置的信息 上传文件使用的form-data格式 body: formData, //返回响应 callback: function(status, responseText){ //表示服务器返回的相应状态码出错 // console.log(responseText);//查看一下响应正文的数据是否符合业务的,可以抓包(建议) if(status != 200){ alert("出错了,响应状态码:"+status); return; } //表示正常返回200 就进行接下来的操作 //响应正文的地方 let body = JSON.parse(responseText);//响应正文 if(body.ok){ alert("注册成功"); //跳转到登陆的页面 window.location.href = "../index.html"; }else{ // //注册失败 显示错误,根据后端的reason反馈信息 app.errorMessage = body.reason; } } }); } }, }); (2)对于用户注册的后端处理实现

(tips:在写后端的响应之前做一个测试,利用抓包工具验证是否能够正常的发送请求和响应。) 接下来进行对于RegisterServlet的开发,首先设置Servlet注解获取前端传过来的formdata表单数据,在其次对于传过来的数据进行构造成一个实体类,存到数据库中。在针对于头像文件获取的时候,需要先获取存储照片的后缀名,将其保存下来创建一个时间戳相关的随机字符串构成重命名,最后形成新的文件保存在本地的文件中。 最后调用用户表的相关操作(封装在UserDao类中)将数据库的信息存储完毕之后就可以返回后续的响应,但是需要构造一个响应对象,需要设置JsonResult返回的对象类,设置好返回格式,返回信息。最后通过resp的相关API来实现对于响应数据的返回。 实现的代码如下:

//做前端注册页面的响应 @WebServlet("/register") @MultipartConfig //FormData上传数据需要 public class RegisterServlet extends HttpServlet { //对于前端页面发送的POST请求数据做后端解析 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //设置请求正文的编码 req.setCharacterEncoding("UTF-8"); //获取前端传递过来的FormData表单格式数据 //在这里需要添加一个 @MultipartConfig 的注解获取form data格式的数据 //用String创建对象来接受传递过来的参数 通过请求的getParameter来获取 String username=req.getParameter("username"); String password=req.getParameter("password"); String nickname=req.getParameter("nickname"); //对于头像文件的获取,前端传递可能为空 //如果存在就从数据中获取信息 Part headFile=req.getPart("headFile"); //将接受到的数据形成一个User对象 进一步的将User对象传递到数据库中 形成注册 User user= new User(); user.setUsername(username); user.setPassword(password); user.setNickname(nickname); //对于传递过来的头像文件需要进行判断是否为空才能进行存储 if (headFile!=null){ //传递头像文件的方法:将文件保存在服务端的一个路径中 //先获取上传文件的后缀名称 getSubmittedFileName() 获取其原始名称 String filename=headFile.getSubmittedFileName(); //找到最后一个点的索引位置,并返回 能够获取其中的照片格式 如JPG 或者JPEG String suffix=filename.substring(filename.lastIndexOf('.')); //添加一个随即字串和时间戳有关 在拼接上后续的照片格式,实现了对于文件的重命名 filename= UUID.randomUUID()+suffix; //保存文件的路径 headFile.write(WebUtil.LOCAL_HEAD_PATH+"/"+filename); //数据库保存头像的路径 user.setHead("/"+filename); } //将数据信息保存到数据库中:判断其是否存在用户名和账号重复 User exist = UserDao.checkIfExist(username,nickname); //后续的逻辑信息,如果数据存在,就返回错误的信息, 如果不存在就 进行注册功能 并返回响应 //此时需要一个返回格式 构建model——JsonResult //构造响应的对象 JsonResult result=new JsonResult(); if (exist!=null) { //表示查询不为空,用户信息存在 //result.setOk(false); 初始的布尔值为false所以可以不用给设置 result.setReason("账号或者昵称已存在"); }else { //表示查询为空,执行数据库的插入信息功能 int n= UserDao.insert(user); result.setOk(true); } //接下来应该返回HTTP响应给前端数据 resp.setContentType("application/json; charset=utf-8"); //需要将java对象转为json的形式 String body=WebUtil.Write(result); resp.getWriter().write(body); } } 第五步:对于数据库三个类的JDBC操作实现 (1)对于用户表的工具类实现

在实现数据存储到数据库中时需要去开发UserDao这个实体类用来存放用户信息到数据库,因此对于user用户表的查询,插入,修改操作是经常性的需要去进行完成。所以在这个user用户表的工具类中实现了对于插入、查询、修改的操作方法。 实现的代码如下:

//用户表数据库相关的操作 public class UserDao { //注册:检查账号、昵称是否存在 实现JDBC操作 public static User checkIfExist(String username,String nickname){ Connection c=null; PreparedStatement preparedStatement=null; ResultSet rs=null; try { c= DBUtil.getConnection(); String sql="select * from user where username=?"; if (nickname!=null){ sql+="or nickname=?"; } //将上面的预编译的的占位符进行替换数据 preparedStatement=c.prepareStatement(sql); preparedStatement.setString(1,username); if (nickname!=null){ preparedStatement.setString(2,nickname); } //执行查询操作,返回结果集进行接收 rs=preparedStatement.executeQuery(); //准备查询的User对象 User queryUser=null; while (rs.next()){ queryUser=new User(); //将结果集的字段设置到属性中 Integer id=rs.getInt("id"); String loginNickname=rs.getString("nickname"); String password=rs.getString("password"); String head=rs.getString("head"); java.sql.Timestamp logoutTime=rs.getTimestamp("logout_time"); queryUser.setId(id); queryUser.setUsername(username); queryUser.setPassword(password); queryUser.setNickname(loginNickname); queryUser.setHead(head); if (logoutTime!=null){ //考虑数据是不是为空的情况 long l=logoutTime.getTime(); queryUser.setLogoutTime(new java.util.Date(l)); } } return queryUser; } catch (SQLException e) { throw new RuntimeException("注册检查账号昵称是否存在JDBC出现错误",e); }finally { DBUtil.close(c,preparedStatement,rs); } } public static int insert(User user) { Connection c=null; PreparedStatement ps=null; try { c=DBUtil.getConnection(); String sql="insert into user (username,password,nickname,head)"+" values(?,?,?,?)"; //进行预编译 ps=c.prepareStatement(sql); //替换占位符 ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getNickname()); ps.setString(3, user.getHead()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("插入数据时出现错误",e); }finally { DBUtil.close(c,ps); } } //数据库修改用户的退出时间jdbc代码 public static int updateLogoutTime(User loginUser) { Connection c=null; PreparedStatement ps=null; try { c=DBUtil.getConnection(); String sql="update user set logout_time=? where id=?"; ps=c.prepareStatement(sql); //替换时间站位符 获取用户中存储的退出时间 long currentTime=loginUser.getLogoutTime().getTime(); ps.setTimestamp(1,new Timestamp(currentTime)); ps.setInt(2,loginUser.getId()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("更新用户上次注销时间出错", e); } finally { DBUtil.close(c,ps); } } } (2)对于消息表的工具类实现

对于用户的发送的消息需要存储到数据库的消息表字段中,同时上线的用户也需要获取历史的消息,因此对于消息表需要实现插入和查询的方法。实现代码如下:

//对于数据库Message的JDBC操作 public class MessageDao { //对于Message操作有查询和插入操作 //给用户放回从退出时间开始算起的保存的历史消息 public static List query(Date logoutTime){ Connection c=null; PreparedStatement ps=null; ResultSet rs = null; try { c= DBUtil.getConnection(); String sql="select * from message"; //对于刚注册是用户,没有上次注销的时间,要进行判断一下 if (logoutTime!=null) { //表示有用户存在 sql += " where send_time > ?"; } ps=c.prepareStatement(sql); if (logoutTime!=null){ //表示用户存在,将预编译的信息进行参数设置 //从退出的时间开始算起 保存的数据 ps.setTimestamp(1,new Timestamp(logoutTime.getTime())); } //得到执行的结果 存放在rs中 rs=ps.executeQuery(); List messages=new ArrayList(); while (rs.next()){ //构建Message对象来接收rs中的消息 Message getOldMessage=new Message(); getOldMessage.setId(rs.getInt("id")); getOldMessage.setUserId(rs.getInt("user_id")); getOldMessage.setUserNickname(rs.getString("user_nickname")); getOldMessage.setChannelId(rs.getInt("channel_id")); getOldMessage.setContent(rs.getString("content")); getOldMessage.setSendTime(rs.getTimestamp("send_time")); //将获取的历史消息对象传到消息队列中 messages.add(getOldMessage); } //将查询到的历史消息返回到队列中进行返回 return messages; } catch (SQLException e) { throw new RuntimeException("查询历史消息出错",e); }finally { DBUtil.close(c,ps,rs); } } //插入数据操作 public static int insert(Message m){ Connection c=null; PreparedStatement ps=null; try { c=DBUtil.getConnection(); //将接收到的用户发送的消息保存起来 //接收到的用户信息的包含的字段 有 内容 用户的昵称 用户的id 频道号 发送的时间 String sql="insert into message(content, user_id, user_nickname, channel_id, send_time) " + " values(?,?,?,?,now())"; ps=c.prepareStatement(sql); ps.setString(1,m.getContent()); ps.setInt(2,m.getId()); ps.setString(3,m.getUserNickname()); ps.setInt(4,m.getChannelId()); //设置接收到的信息发给前端 m.setSendTime(new Date()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("保存发送的消息jdbc出错",e); }finally { DBUtil.close(c,ps); } } } (3)对于频道表的工具类实现

用户在进入界面的时候能够获取到所有频道的信息,因此对于频道中所有频道需要实现查询的方法。实现代码如下:

public class ChannelDao { //实现查询返回 channels表单数据即可 public static List selectAll() { Connection c=null; Statement ps=null; ResultSet rs=null; try { //对于上述进行赋值 c= DBUtil.getConnection(); //展示的频道框为第一个 String sql ="select * from channel order by id"; ps=c.createStatement(); rs=ps.executeQuery(sql); //用来接受所有的channel List channels=new ArrayList(); while (rs.next()){ //获取的数据很多个,将每一个数据转为channel对象 Channel channel=new Channel(); //Channel的关键字段为 id name int id=rs.getInt("id"); String name=rs.getString("name"); channel.setId(id); channel.setName(name); //将数据添加的到List结构中的Channels里面 channels.add(channel); } return channels; } catch (SQLException e) { throw new RuntimeException("查询频道列表时出错",e); }finally { //释放资源 DBUtil.close(c,ps,rs); } } } 第六步:对于登录功能的实现 (1)对于登录页面的前端实现

用户在登录页面登录自己的用户名和密码信息后,前端进行获取,前端获取后通过ajax发送到后端进行解析,通过回调函数来确定是否是注册账号,如果是就进行登录,如果不是就提示输入有误。 实现的代码如下:

DOCTYPE html> 在线聊天室 聊天室登录 用户名 密码 {{ errorMessage }} 注册 let app = new Vue({ el: "#app", data: { errorMessage: "", username: "", password: "", }, methods: { //实现前端的登录功能 login: function (){ ajax({ method: "post", url: "login", contentType:"application/json", //转化为json对象 将数据转为字符串 body:JSON.stringify({ username: app.username, password: app.password, }), //回调函数 callback:function (status,responseText){ if (status!=200){ alert("登录出错了,服务器可能开小差了。" + "返回响应状态码为:"+status); return; } let body=JSON.parse(responseText); if (body.ok){ alert("账号密码验证成功,欢迎进入在线聊天室") //跳转到聊天框页面进行访问 window.location.href="views/message.html" }else { //登录失败,显示错误信息 app.errorMessage=body.reason; } } }); }, }, }); (2)对于登录页面的后端实现

首先在后端是对于前端的登录页面发来的请求的进行获取信息,通过获取前端的json字符串的user信息查询数据库来实现对于用户信息的校验,如果存在就创建session保存用户信息,不存在就提示用户有错误。最后将查询的相关用户的信息返回到当前页面的响应中。实现的代码如下:

//对于后端登录页面的信息的功能实现 @WebServlet("/login") public class loginServlet extends HttpServlet { //对于前端发起的Post方法做一个返回响应 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //设置一个请求正文的编码 req.setCharacterEncoding("utf-8"); //解析传递过来的json数据 调用WebUtil中的读取方法即可 将输入流中的数据转为Class对象 User input = WebUtil.read(req.getInputStream(),User.class); //对于账号密码的检验 //先检验账号是否存在,如果不存在提示,如果存在,在校验密码 使用UserDao 中对于用户名和昵称的检验方法 User exist= UserDao.checkIfExist(input.getUsername(),null); //准备返回的Web响应 调用JsonResult 实现即可 JsonResult result=new JsonResult(); //对于密码进行验证 if (exist==null){ result.setReason("您输入的账号不存在"); }else{ //对于密码进行判断 //exits是查询的username对应在数据库中的信息 input.getPassword是用户在页面上登录的信息 if (!exist.getPassword().equals(input.getPassword())){ //表示校验失败 登录密码错误 设置返回原因 result.setReason("您输入的密码有误,请重新输入"); }else { //校验成功 需要给用户创建session保存信息 HttpSession session=req.getSession(); //保存数据库查询到的用户信息 session.setAttribute("user",exist); result.setOk(true); } } //返回响应数据 resp.setContentType("application/json; charset=utf-8"); String body=WebUtil.Write(result); //把body数据写进当前页面的响应中 resp.getWriter().write(body); } } 第七步:对于聊天页面的功能实现 (1)对于聊天页面的前端实现

前端处理:先构建频道信息,对于每个对话框进行设计,能够实现基础的点击对话框就能跳转到非当前对话框。同时在页面加载的时候,需要对于对话框的列表进行获取和返回。根据与后端传递过来的channel参数信息来设置前端的响应,对于Channel中的属性继续的进行实现。对于消息推送功能的实现,使用WebSocket的方式实现,之所以不用Http是因为Http对于客户端和服务端之间需要一发一收后才能进行下一步操作,不能实现客户端和服务端全双工的特性,而WebSocket则实现了该特性,对于WebSocket,则是基于TCP协议,首先发送Http请求建立连接(目的是双方确定后续使用的协议和秘钥),后续使用Websocket协议(在应用层使用相同的数据格式来发送、接收数据)来实现收发数据。在前端使用socket的相关api来完成对于消息的处理。 前端处理代码实现:

DOCTYPE html> 在线聊天室 欢迎进入在线聊天室!{{ currentUser.nickname }} 注销 {{ c.name }} {{ c.unreadCount }} {{ m.sendTime }} {{ m.userNickname }} {{ m.content }} 发送(S) let app = new Vue({ el: "#app", data: { websocket:null, currentUser: { //当前登录用户 nickname: "", head: "", }, //设置频道信息 写静态数据验证前端代码,后续从servlet的响应中获取数据 channels: [ { id: 1, name: "带刀侍卫群", //存放历史消息的地点 historyMessage: [], //输入框的内容 inputMessageContent: "", unreadCount: 0, }, { id: 2, name: "门前麻将群", //存放历史消息的地点 historyMessage: [], //输入框的内容 每个频道的输入框内容不一样 inputMessageContent: "", unreadCount: 0, }, ], //当前频道 currentChannel: { id: 1, name: "带刀侍卫群", //存放历史消息的地点 historyMessage: [], //输入框的内容 inputMessageContent: "", unreadCount: 0, }, }, methods: { //点击切换频道的功能实现 changeChannel: function (channel) { //先判断点击的频道是否是当前频道 if (channel.id != app.currentChannel.id) { app.currentChannel = channel; } //切换到一个频道后,滚到最后,并且未读消息=0 app.scrollHistory(); }, //从后端获取频道列表 再设置到vue的变量中,页面就可以跟着去改变 getChannels: function () { //发送AJAX请求获取数据 ajax({ method: "get", //获取频道列表 url: "../channelList", callback: function(status, responseText){ // console.log(responseText);//查看一下响应正文的数据是否符合业务的,可以抓包(建议) if(status != 200){ alert("出错了,响应状态码:"+status); return; } //设置响应正文 let body = JSON.parse(responseText);//响应正文 //后端ChannelListServlet中返回的是{user:{},channels:[]} //返回的Channel是不带historyMessage(历史消息) inputMessageContent(输入框消息) //需要给返回的数据添加上当前的消息 //当前用户 app.currentUser = body.user; for(let i=0; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3