☰
Java聊天系统课程设计实战:数据库、WebSocket与避坑指南
2026/10/1 4:40:50 网站建设 项目流程

简介:这是一份基于 Java Swing、多线程与 MySQL 实现的仿 QQ 聊天室项目,适合作为 Java 期末大作业或课程设计参考。系统覆盖用户注册、登录、找回密码、查看在线人员、群聊/私聊、修改密码、注销账户等完整功能,源码结构清晰,包含 34 个 Java 源文件与对应 class 编译文件,可直接对照学习或二次开发。压缩包共 291 个文件、21.46MB,另有 178 张 png 界面效果图、16 个 xml 配置、1 个 sql 数据库脚本和 1 份项目展示 PPT,从运行环境搭建到答辩展示均有配套材料。项目已调试通过、无 bug,已有 587 人学习下载,对于需要快速搭建聊天室项目、理解 Swing 界面编程与多线程通信的同学具有较高参考价值。

1. 从课程设计到能跑的聊天室:这套 Java Chat 到底值不值得你动手

如果你在找「java做的chat聊天系统(附带数据库文件、项目展示PPT)」,大概率是正在做 Java 课程设计、毕业设计,或者想用一个小项目把 JSP/Servlet、数据库、前端三件套串起来。先说结论:这类项目的定位是「教学闭环」,不是「生产级 IM」。它的价值在于——数据库文件让你不用从零建表,PPT 让你不用从零憋答辩稿,而你真正要做的,是把代码跑起来、讲明白、能改能扩展。反直觉的是,这种「带数据库带 PPT」的项目,最大的坑反而不是代码跑不起来,而是你拿到手后不知道该改哪里、更不知道该在答辩时突出什么。本文会从选型、建表、实时消息三条主线拆开讲,最后落到你一定会遇到的乱码、连接池和版本兼容问题上。适合正在做 Java 课程设计、或者准备用 SSM/Spring Boot 重写旧项目的人照着重现一遍。

2. 技术选型先想清楚:Servlet 还是 Spring Boot,这不是面子问题

2.1 这类带数据库和 PPT 的项目,常见技术栈是哪一套

我见过几十个「Java chat 聊天系统」课程设计,技术栈高度集中。绝大多数是 JSP + Servlet + MySQL + Tomcat,偶尔有加了 Bootstrap 和 jQuery 的,再新一点的是 Spring Boot + MyBatis + WebSocket。你拿到手的项目是哪一套,直接决定你要装什么环境、怎么部署——这不是偏好问题,是你能不能跑起来的现实问题。

JSP + Servlet 这套老组合的好处是:不需要 Maven 拉依赖,不需要配置数据源框架,一个 Tomcat 丢进去就能跑,适合机器上只有 JDK 和 Tomcat 的课程设计环境。Spring Boot 版本的好处是:内嵌 Tomcat,启动就是一个 main 方法,前后端分离也好做,但你需要 Maven、需要联网拉依赖、需要处理端口冲突。我的建议是——先看你的 JDK 版本。如果你装的是 JDK 8,两个都能跑;如果你装的是 JDK 17 及以上,老项目里的 javax.servlet 包可能直接编译报错,那就要么换 JDK 8,要么选 Spring Boot 3 + jakarta.servlet 的新版本。这个兼容性问题,是这类项目翻车的第一大原因,后面避坑章节会细说。

2.2 用 Servlet 手写登录和注册:最小的可运行骨架

不管最终选哪套,登录注册都是聊天系统的入口。如果是 SSR 架构,流程是:浏览器提交表单 → Servlet 接收参数 → 查数据库验证 → session 记录登录态 → 重定向到聊天主页。以下是核心代码骨架,这段逻辑在任何 Java Web 课程设计里都能直接复用:

@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); // 用 PreparedStatement 防 SQL 注入,避免拼音拼接字符串 String sql = "SELECT id, nickname FROM user WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { HttpSession session = req.getSession(); session.setAttribute("userId", rs.getInt("id")); session.setAttribute("nickname", rs.getString("nickname")); resp.sendRedirect("chat.jsp"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); } } } catch (SQLException e) { e.printStackTrace(); resp.sendError(500, "数据库访问异常"); } } }

这段代码有三个参数细节值得你注意。第一,@WebServlet("/login")是 Servlet 3.0 的注解式注册,前提是 Tomcat 7 以上,如果你的 Tomcat 是 6 以下的古董,必须去 web.xml 里配<servlet-mapping>,否则 404。第二,conn.prepareStatement(sql)的写法就是为了防注入——课程设计答辩时老师基本必问这一句和你手拼字符串的区别,回答「PreparedStatement 会预编译、参数化传值,SQL 语句和数据分离」,这一分就拿到了。第三,登录成功用sendRedirect而不是forward,是为了避免刷新页面时重复提交表单,这是 Web 开发的基本功,答辩时讲「PRG 模式」会显得你水平在线。

2.3 连接池为什么要配,怎么配:不配你的 Chat 撑不过 10 个用户

课程设计里最常见的跑路姿势是:数据库工具有 Navicat,但代码里每次DriverManager.getConnection()现连现断。这样写小 demo 没问题,但聊天系统是高频请求场景——每个用户每次拉取好友列表、发消息、刷新在线状态都要建连,而 MySQL 默认的max_connections也就 151,连接一多直接就Too many connections了。而且每次建连的握手开销在局域网里可能感觉不到,一旦答辩现场用的是无线网络,延迟立刻放大。

正确的做法是配数据库连接池。如果你拿到的是 SSM 或 Spring Boot 项目,Druid 和 HikariCP 二选一;如果是纯 Servlet 项目,至少用 Apache DBCP 或者 C3P0。以下是最常见的 Druid 配置模板,可以直接抄进application.properties或独立 properties 文件里:

spring.datasource.druid.url=jdbc:mysql://localhost:3306/chat_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai spring.datasource.druid.username=root spring.datasource.druid.password=123456 spring.datasource.druid.initial-size=5 spring.datasource.druid.min-idle=5 spring.datasource.druid.max-active=20 spring.datasource.druid.max-wait=60000 spring.datasource.druid.validation-query=SELECT 1

这里serverTimezone=Asia/Shanghai是很多人忽略的坑——MySQL 8 默认时区是 UTC,你不加这个参数,存时间字段会差 8 小时。还有validation-query=SELECT 1,它保证从池里拿出来的连接是可用的,避免数据库重启后拿到一堆失效连接。initial-size和min-idle都设成 5,意思是应用启动就预建 5 条连接,空闲时也维持 5 条,不要等用户来了才现建。至于max-active=20,对课程设计的体量绰绰有余,记着这个数不要乱改——你又不是在跑双十一。

3. 数据库文件拆开看:三张表撑起一个聊天系统的增删改查

3.1 用户表、好友表、消息表:字段设计背后的理由

标题说附带数据库文件,你导入后第一件事不是看数据,而是看表结构。一个标准的聊天系统库,核心就三张表:user、friend、chat_message。很多课程设计还会加一张friend_request(好友申请表)和group_chat(群聊表),但那是加分项,不是必需品。以下是你会频繁操作的核心表结构:

CREATE TABLE `user` ( `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', `password` VARCHAR(64) NOT NULL COMMENT '登录密码', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '显示昵称', `avatar` VARCHAR(255) DEFAULT 'default.png' COMMENT '头像路径', `status` TINYINT DEFAULT 0 COMMENT '在线状态:0离线 1在线', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `friend` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NOT NULL COMMENT '用户ID', `friend_id` INT NOT NULL COMMENT '好友的用户ID', `remark` VARCHAR(50) DEFAULT NULL COMMENT '好友备注名', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_user_friend` (`user_id`, `friend_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `chat_message` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `from_user_id` INT NOT NULL COMMENT '发送方用户ID', `to_user_id` INT NOT NULL COMMENT '接收方用户ID', `content` TEXT NOT NULL COMMENT '消息内容', `msg_type` TINYINT DEFAULT 1 COMMENT '1文本 2图片 3文件', `is_read` TINYINT DEFAULT 0 COMMENT '0未读 1已读', `send_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为什么user表要把username设为 UNIQUE?因为账号是登录凭证,唯一约束在数据库层兜底,比你在 Java 代码里先查后插要可靠得多——并发注册时两个请求同时查到「不存在」就会重复插入,有唯一索引直接报错,你 catch 一下告诉用户「账号已被注册」就行。friend表加联合唯一键uk_user_friend也是同理,防止 A 重复添加 B 为好友。chat_message表必须要is_read字段,否则你根本没法实现「未读消息数」这种看着不起眼、其实是聊天系统核心体验的功能。

3.2 从 SQL 文件到跑起来:导入数据库的三个动作

拿到数据库文件后,新手最常见的翻车点是直接在 Navicat 里双击 .sql 文件,然后发现表建好了但中文乱码。正确的导入路径是:先建库、再选字符集、再导数据。命令行做法如下:

mysql -u root -p # 进入 MySQL 后执行以下三条语句 CREATE DATABASE IF NOT EXISTS chat_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE chat_db; SOURCE /path/to/chat_db.sql;

第一条语句的utf8mb4是关键——你要存 Emoji 表情(聊天内容里极其常见),utf8是存不下的,会直接报错或者变成问号。COLLATE utf8mb4_unicode_ci是排序规则,选它是因为对中文和特殊字符的排序更符合直觉。如果你用的是 Navicat 这种图形工具,对应操作是右键连接 → 新建数据库 → 字符集选utf8mb4→ 排序规则选utf8mb4_unicode_ci,然后右键该库 → 运行 SQL 文件 → 选择你拿到的 .sql。导入后不要急着看数据,先执行一条SHOW TABLES;确认表数量跟你预期一致,再打开user表看看有没有初始账号——很多课程设计会预置一个admin/123456,这能帮你省掉注册流程直接进聊天页。

3.3 数据库文件里的数据一致性:Java 面试里怎么问都不虚

标题既然带了数据库文件,说明数据是预先灌好的。但你要知道,聊天系统对数据一致性天然敏感。举一个最经典的场景:A 给 B 发了一条消息,写进chat_message表,但 B 刷新后看不到——为什么?最常见原因是写入成功了但你查询时用了to_user_id而前端传参把字段名写成了toUserId,MyBatis 里没做驼峰映射,结果查出来是 null。另一个更隐蔽的坑是事务:如果你在「发送消息」接口里同时做两件事——插入消息记录 + 更新好友的未读计数——这两步必须在一个事务里,否则消息入库了计数没更新,用户就永远看不到红点。

@Transactional(rollbackFor = Exception.class) public void sendMessage(Integer fromUserId, Integer toUserId, String content) { chatMessageMapper.insert(new ChatMessage(fromUserId, toUserId, content)); friendMapper.incrementUnread(toUserId, fromUserId); }

@Transactional(rollbackFor = Exception.class)这个注解不是加上就完事了。关键在于:Spring 默认只在 RuntimeException 时回滚,如果你自己的业务异常继承的是Exception,不加rollbackFor就不会回滚——消息插进去了、计数更新失败了,数据就脏了。另外注意,事务要生效,这个方法必须通过 Spring 代理调用,不能同类内部this.sendMessage()直接调,那是事务失效的重灾区,面试和答辩时被问到「事务为什么没生效」十有八九是这个原因。

4. 聊天的核心:从轮询到 WebSocket,实时消息到底怎么选

4.1 轮询方案:代码简单但会被人问「这是实时聊天吗」

如果你拿到的项目用的是 Ajax 定时刷新消息列表,也就是每 3 秒setInterval发一次请求拉取新消息,你千万别急着换成 WebSocket——先把它跑通,再考虑升级。轮询方案有它存在的合理性:实现简单、没有长连接管理、不会因为代理服务器或防火墙断连。在课程设计这个体量下,10 个用户轮询,每 3 秒一次请求,MySQL 和 Tomcat 毫无压力。

核心逻辑是前端定时器 + 后端查新消息接口。后端接口一般长这样:接收当前用户 ID 和最后一次拉取的消息 ID,然后查WHERE to_user_id = ? AND id > ?,返回比上次更新的消息。这里有个细节要提醒你——「增量查询」用id > 上次最大id比用send_time > 上次时间更准确,因为在同一秒内发了两条消息,时间戳相同,按时间去查会漏消息。按 ID 查不会漏,这是轮询方案最重要的一条性能与准确性平衡点。

4.2 WebSocket 方案:用 Java 原生注解实现服务端主动推送

如果你想让系统看起来更像「实时聊天」,WebSocket 是更优解。它的优势是:一次握手后建立长连接,服务端可以主动推送消息给指定用户,不用客户端反复问「有没有新消息」。最常见的做法是用 Java 的javax.websocket原生注解(Java EE / Tomcat 自带),也可以用 Spring 的WebSocketHandler。以下是课程设计中最容易理解的一套:

@ServerEndpoint("/chat/{userId}") public class ChatEndpoint { // 用 ConcurrentHashMap 存所有在线会话,key 是用户ID private static final Map<Integer, Session> ONLINE_SESSIONS = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("userId") Integer userId) { ONLINE_SESSIONS.put(userId, session); // 可以在这里把 user 表的 status 字段更新为 1(在线) } @OnMessage public void onMessage(String message, Session session) { // 消息格式约定为 JSON:{"toUserId": 2, "content": "你好"} // 解析后从 ONLINE_SESSIONS 找到目标用户,推送 JsonObject payload = JsonParser.parseString(message).getAsJsonObject(); Integer toUserId = payload.get("toUserId").getAsInt(); Session targetSession = ONLINE_SESSIONS.get(toUserId); if (targetSession != null) { targetSession.getBasicRemote().sendText(payload.get("content").getAsString()); } // 如果目标不在线,就只存数据库,等对方上线后再拉取 } @OnClose public void onClose(@PathParam("userId") Integer userId) { ONLINE_SESSIONS.remove(userId); // 相应把 status 更新为 0 } }

这段代码的ONLINE_SESSIONS必须是ConcurrentHashMap,因为 WebSocket 是多线程环境下并发访问的,用普通 HashMap 会在高并发时出现 CPU 100% 甚至死循环。另外注意@OnClose里的@PathParam("userId")——如果你的路径参数是从@OnOpen里解析的,@OnClose里也要重新声明一次,因为容器不会帮你保存上个方法的参数。最大的坑是:如果你用 Nginx 做了反向代理,记得配置proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade";,否则 WebSocket 握手一定失败,浏览器控制台会一直报WebSocket connection to 'ws://...' failed——这个问题能卡你一下午,而且和 Java 代码本身半毛钱关系都没有。

4.3 离线消息怎么补:WebSocket 连接不是无线的

实时聊天的另一半是「离线消息」。你要清楚一点:WebSocket 只能覆盖「双方都在线」的场景。只要 A 给不在线的 B 发了消息,B 下次登录时必须有办法把错过的消息捞回来。这个逻辑几乎必然落在数据库上——发送时先写chat_message表,目标在线则顺便推 WebSocket,不在线就等对方登录后查库。

具体做法是在登录进聊天页时,调一个「拉取离线消息」接口:SELECT * FROM chat_message WHERE to_user_id = ? AND is_read = 0,然后把is_read批量置 1。这里有个体验细节:如果你把所有未读消息一次性查出来全部置为已读,用户根本看不出哪条是新的;更好的做法是按from_user_id分组,先展示「谁给你发了消息」,点开好友再看到具体内容。消息体可以顺手返回发送方昵称和头像路径——一条 SQLJOIN user就能搞定,别在前端拿一个fromUserId再挨个查用户信息,那是 N+1 查询的经典反面教材。

5. 避坑指南:从数据库连接到页面乱码,这 6 个坑我帮你先踩

5.1 中文乱码:从 Tomcat 到 MySQL 每一层都得是 UTF-8

现象:注册时填中文昵称,登录后显示「??」,或者发送中文消息收到方看到一串乱码。原因:字符编码在传输链路上被某一步转成了 ISO-8859-1 或 GBK。解决:按顺序检查四层。第一,JSP 页面顶部加<%@ page contentType="text/html;charset=UTF-8" %>;第二,Servlet 里req.setCharacterEncoding("UTF-8")必须在getParameter()之前调用;第三,MySQL 连接串加characterEncoding=utf8,建库时指定utf8mb4;第四,Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8"。如果以上都做了还乱码,十有八九是你在 IDE 里复制粘贴了别人的源码,而 IDE 默认编码是 GBK——全选文件改成 UTF-8 保存再重新编译,这个坑最阴间,但它真的存在。

5.2 数据库文件导入后外键报错

现象:用 Navicat 导入 .sql 文件,提示Cannot add foreign key constraint,表建了一半就停了。原因:导入顺序不对。你拿到的 SQL 文件里,如果先建了chat_message表(它外键引用user表),但user表还没建出来,外键约束就报错了。解决:用命令行SOURCE方式导入,让 MySQL 按文件顺序逐条执行;如果图形工具一次性执行失败,把 SQL 文件打开,手动把建表语句按依赖顺序拆开执行——先user,再friend,最后chat_message。另外一个常见原因:两张表的引擎不一致,一张是 InnoDB、一张是 MyISAM,InnoDB 支持外键,MyISAM 根本不认外键,也会导致同样的报错。

5.3 JDK 17 跑老项目:javax 还是 jakarta,一句话的事

现象:编译报错package javax.servlet does not exist。原因:JDK 9 开始模块化,JDK 11 移除了 Java EE 模块,Java EE 也改名为 Jakarta EE——Servlet 的包名从javax.servlet变成了jakarta.servlet。Tomcat 9 及以下是javax,Tomcat 10 及以上是jakarta。解决:要么把 JDK 降到 8 并用 Tomcat 9,要么把源码里所有javax.servlet批量替换成jakarta.servlet。降 JDK 是最省事的路——装一个 JDK 8,IDE 里 Project Structure 把 SDK 切过去,Tomcat 也换成 9,全程不到五分钟。硬着头皮换 jakarta 包名也不难,但后续 Spring、JSP 等库的版本可能也要跟着升级,不建议课程设计阶段折腾。

5.4 数据库连接池配置正确但启动报「Access denied」

现象:Tomcat 启动后第一次访问登录接口,控制台弹出java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)。原因:密码错了,或者 MySQL 8 的认证插件是caching_sha2_password,而你的连接池驱动是旧版 MySQL Connector/J 5.x,不支持这个新认证方式。解决:先检查配置里的 username/password 和实际一致;不一致就改配置。如果一致还报错,就是驱动版本问题——换成mysql-connector-java8.0.x,并且把依赖里旧的 5.1.x 排掉。还有一个隐藏坑:如果 MySQL 8 里你给 root 账号设置的是空密码,注意配置里password=后面空着和没有这个属性是两回事,别在这上面耗时间。

5.5 「明明在登录页跳转到了 chat.jsp,但马上又弹回登录页」

现象:登录成功后跳转到聊天页,刷新一下就被拦截器踢回登录页。原因:session 没有持久化或者拦截器放行条件写错。常见拦截器逻辑是判断session.getAttribute("userId") == null就重定向到登录页——但如果你在登录后把用户数据存进了请求域(request)而不是会话域(session),跳转后自然就丢了。解决:登录成功时务必存 session,而不是req.setAttribute。另一个脏坑是:你用了 Spring Security 或 Shiro,csrf 校验把表单请求拒了但不报错直接重定向——看浏览器 Network 面板,如果chat.jsp的状态码是 302 而不是 200,问题大概率不在你的代码,而在过滤器链的配置上。

5.6 PPT 上被问到「单点登录怎么处理」,直接答不上来

现象:答辩演示时老师看着 PPT 里「登录模块」这一页,随口问了一句「用户已经在聊天页面了,再打开一个标签页访问登录页,会怎么样」。你愣住。原因:你没有处理「已登录用户重复登录」的情况——现在大多数课程设计不会处理,但这正是拉开分数差的地方。解决:一个最小实现是——登录成功时把 userId 写进 session,同时在数据库user表维护一个session_id字段;每次访问受保护资源时,拦截器比对当前 session 的 id 和数据库里存的 session_id,不一致就强制下线。这样实现成本不高,但答辩效果立刻不一样——这证明你想过并发登录的问题,而大多数人没想过。

6. 让项目从「能跑」到「能加分」:Session 管理和答辩演示的三个细节

先搞清楚一个概念:HTTP 是无状态的,WebSocket 管的是实时消息通道,但你的登录状态依然靠 Session 维持。一个合格的聊天系统至少要能在两个地方用到 Session:登录后把userId和nickname放进去,以及过滤器里拦截未登录请求。以下是一个极简过滤器写法:

@WebFilter("/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录页、静态资源和登录接口本身 if (uri.endsWith("login.jsp") || uri.endsWith("login") || uri.endsWith(".css") || uri.endsWith(".js")) { chain.doFilter(req, resp); return; } if (request.getSession().getAttribute("userId") == null) { response.sendRedirect("login.jsp"); return; } chain.doFilter(req, resp); } }

这段代码要特别留意:放行条件不要写成uri.contains("login"),因为chat_login、user_login_image这类路径也会被放行,造成逻辑漏洞。endsWith只是其中一种写法,更严谨的是用 URL 白名单列表去匹配。会话超时时间默认是 30 分钟,课程设计的演示场景里可以调短一点——在web.xml里配<session-config><session-timeout>15</session-timeout></session-config>。

答辩演示层面的三个建议。第一,演示前先把chat_db里预置的几个账号网页打开,别现场注册——注册时如果昵称撞了唯一索引,报错弹窗会让你手忙脚乱。第二,演示实时聊天时,开两个浏览器窗口(Chrome 和 Edge 各一个),一个发消息、一个收消息,比在一个页面里自己和自己说话可信得多,也顺便验证了不同 Session 之间互不干扰。第三,PPT 里讲到数据库那一页时,别只贴建表语句——把 ER 图放上去,用箭头标出friend.user_id → user.id和chat_message.from_user_id → user.id的关系,这是评委一眼就能看懂的数据库设计证据,比任何文字都有力。

最后说一下我自己的习惯:拿到任何一份带「数据库文件 + PPT」的课程设计代码,第一件事永远是打开DBUtil.java或application.properties看数据库连接配置,改成本地账号密码后跑一遍登录——这一步能过滤掉八成“代码没问题、环境没配好”的隐藏问题。然后用五分钟通读表结构,确认三张核心表之间没有外键循环依赖,再去看 WebSocket 或轮询的代码位置。这套流程走下来,你答辩时被问「系统整体怎么跑的」的时候,脑子里会有一条清晰的线:用户从登录页进来,Session 记住了我是谁,我发消息时先落库再推送,对方不在线就等上线后拉取离线消息——就这么简单,但能把这条线讲清楚的人,分数都比只会点开演示页说「这是登录、这是聊天」的人高一个档。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询