微信小程序心理咨询聊天室毕设实战:云开发实时消息与避坑指南

发布时间:2026/10/25 10:38:34
微信小程序心理咨询聊天室毕设实战:云开发实时消息与避坑指南

简介:面向毕业设计场景的学校心理咨询聊天室微信小程序项目,含小程序端和后台管理端,适合快速搭建心理咨询、在线聊天、文章发布类系统的计算机专业学生参考。项目基于微信开发者工具、Java SSM 框架与 MySQL,实现用户注册登录、心理医生展示与咨询回复、心理文章查看、在线聊天、公告推送、后台用户及内容管理等完整功能闭环。压缩包为RAR格式,共1342个文件,大小28.57MB。前端以Vue、JS、WXML、WXSS页面与逻辑为主,后端以Java、Class、XML接口和数据层为主,另有SQL初始化脚本、PNG/SVG/JPG素材、安装运行脚本、说明文档和演示录像,整体目录清晰,导入微信开发者工具即可调试扩展。已有370人学习下载。演示录像可直观查看注册、咨询、聊天、后台管理等功能运行效果;说明文档配合源码,能快速梳理前后端请求流程与数据表关系,支撑毕业设计说明书撰写、答辩演示和二次开发。

1. 学校心理咨询聊天室做的是什么:一张聊天列表背后的五个模块

把“学校心理咨询聊天室”当毕业设计做,最容易踩的坑是把它当成一个普通聊天 App 来写:做个登录页、拉个好友列表、把消息往数据库一塞就完事。实际上校园心理咨询的场景比社交聊天更克制——学生希望匿名倾诉,咨询师需要提前约时间,聊天记录要有隔离,敏感内容和排班信息又不能出错。也就是说,这个题目表面是一张聊天列表,背后至少拆成用户身份、咨询师排班、预约、聊天会话、消息记录五个模块。

这套交付物里,源码解决的是“怎么把五个模块串起来”,说明文档解决的是“导师怎么快速看懂你的架构”,演示录像则解决“答辩时怎么在两分钟里讲清楚你已经跑通了”。对准备拿它当毕设的人来说,真正值钱的部分不是那张 UI,而是消息链路的选型和数据模型的边界处理。下面这几章,就是顺着这条链路把实现、参数和坑位讲透。

2. 选型先做对:云开发、自建 WebSocket 还是 uniapp 打包

2.1 为什么毕设场景首选微信云开发

心理咨询聊天室这类小程序,后端需要处理的不只是 CRUD,还有实时消息推送。如果从零搭一套 Java Spring Boot 或者 Python Flask 后端,你要同时搞定服务器、域名备案、HTTPS 证书、WebSocket 服务和消息表设计,工作量直接翻倍。对毕业设计而言,微信云开发是性价比最高的路线:它把数据库、云函数、存储和实时数据推送都收进了微信生态里,小程序端直接调用,不需要自己维护服务器。

云开发的实时消息能力来自数据库的watch方法。它不是传统意义的 WebSocket 长连接,而是建立在微信底层信道上的数据变更订阅。小程序端collection.where(...).watch()之后,集合里新增、更新、删除文档都会通过快照推回客户端,正好覆盖聊天室“发一条消息,其他人马上看到”的需求。常见的做法是建三个集合:users存用户身份和昵称,rooms存咨询房间或排班记录,messages存聊天内容。房间维度用roomId关联,不用做复杂的会话表。

// app.js 入口初始化云环境 App({ onLaunch: function () { if (!wx.cloud) { console.error('请使用 2.2.3 或以上基础库以使用云能力'); } else { wx.cloud.init({ env: 'your-env-id', // 改成你自己的云环境 ID traceUser: true // 把用户访问记录到云开发控制台 }); } } })

这个初始化代码是云开发的起点。env参数必须在云开发控制台里新建环境后把环境 ID 填进来,填错了后面所有云函数、数据库调用都会报环境不存在的错误。traceUser建议打开,调试时能在控制台看到每次请求的用户来源,排查问题省很多时间。

云开发的数据库权限默认是“仅创建者可读写”,聊天室场景要把messages集合的权限调整成“所有用户可读,仅创建者可写”,否则咨询师看不到学生发来的消息。这个权限配置在云开发控制台的数据权限里改,不在代码里改。

2.2 自建后端:什么时候必须换成 WebSocket 方案

云开发也不是万能的。如果你的毕设要求里明确写了“基于 WebSocket 的实时通信”,或者导师要求你必须自建服务器,那就要切换到自建后端。这个改动影响的是消息链路,不是页面结构。常见做法是后端用 Node.js 的ws库或者 Java 的 Netty 维护一个长连接服务,小程序端用wx.connectSocket连上去。

自建 WebSocket 的核心不只是收发消息,而是连接管理和心跳保活。小程序切后台超过一定时间,系统会主动断开 socket,如果后端不做心跳检测,就会出现“对方明明在线却收不到消息”的假死状态。一般要设计的协议里至少包含三种帧:message表示业务消息,heartbeat表示客户端发来的心跳包,system表示上线、下线等系统事件。

// 小程序端 WebSocket 连接示例 const socket = wx.connectSocket({ url: 'wss://chat.example.com/ws', // 必须是 wss,且域名要配到后台 header: { token: wx.getStorageSync('token') }, success: () => console.log('连接发起成功') }); socket.onOpen(() => { // 连接建立后开始发心跳,每 30 秒一次 setInterval(() => { socket.send({ data: JSON.stringify({ type: 'heartbeat', ts: Date.now() }) }); }, 30000); }); socket.onMessage(res => { const packet = JSON.parse(res.data); if (packet.type === 'message') { // 追加到消息列表 } });

这里的关键参数是wss前缀。微信真机环境不允许ws://明文连接,也不允许连 IP,必须用备案域名加 SSL 证书,并在小程序后台的“开发设置-服务器域名”里把 socket 合法域名配进去。开发者在开发者工具里可以把“不校验合法域名”勾上,但真机预览就没用了,这是自建方案最容易翻车的地方。

2.3 用 uniapp 换皮到底好不好

很多想省事的同学会问:能不能用 uniapp 写,顺便把小程序打包出来?答案是能用,但这个题目我不推荐。心理咨询聊天室的核心难点在实时消息和权限边界,uniapp 帮你解决的是多端编译,不是聊天逻辑。如果你用 uniapp 打包微信小程序,打包出来的 source size 很容易超过 2 MB 限制,最终还是要做分包加载。更尴尬的是,uniapp 对微信云开发的支持不完整,云函数的调用和collection.watch的实时监听在 uniapp 里经常要绕一层uniCloud才会好用,等于给自己多套了一个黑匣子。

如果你的团队诉求是“后端代码想用现成的 PHP 源码或 Python 源码改”,那更要先想清楚一件事:网上那些免费源码解决的是登录、留言板这类通用功能,聊天室的连接管理、掉线重连、消息幂等这些细节,基本都要自己重写。把时间花在业务边界上,比换框架更有价值。

3. 把聊天室核心流程写出来:登录、房间、radio-group 与消息监听

3.1 登录与用户表:openid 是怎么来的

聊天室的第一个动作是登录,但这里说的登录不是让你做账号密码,而是拿到用户的openid。openid是微信用户在当前小程序下的唯一标识,咨询师和学生的身份都用它来区分。云开发里获取openid的标准做法是写一个云函数,在云函数里通过cloud.getWXContext()读取,而不是在小程序端直接拿。

// cloudfunctions/login/index.js const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); exports.main = async () => { const wxContext = cloud.getWXContext(); return { openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID }; };

注意cloud.DYNAMIC_CURRENT_ENV这个参数,它表示云函数运行在哪个环境就访问哪个环境的资源,不用硬编码环境 ID。登录云函数返回的openid要存到users集合里,同时建议把role字段写进去:学生是student,咨询师是counselor。这个角色字段就是之后判断“谁能进哪个房间”的依据。

unionid只有在同一主体下的公众号、小程序、移动应用绑定到开放平台时才会有值,单做一个毕设用不到,返回它只是为了调试方便。小程序端拿到openid后不要直接存 Storage,而是用它去查users集合,用户不存在就插入一条新记录。这里有个常见误区:有人把openid直接写在setData里传到页面用于展示,这没必要,前端只需要知道 uid 和角色即可。

3.2 咨询师列表与预约入口:radio-group 的联动写法

心理咨询聊天室一般不止一个咨询师,所以需要一个选择入口。常见的页面结构是:先展示咨询师列表,每个咨询师有简介、可预约时间段,学生选择一个时间段后进入聊天室。这里的选择控件用radio-group最直接,它天然适合“只能选一个”的场景,比checkbox少了至少一层判断逻辑。

<!-- pages/appointment/appointment.wxml --> <radio-group bindchange="onCounselorChange"> <label wx:for="{{counselors}}" wx:key="id"> <radio value="{{item.id}}" checked="{{item.id === selectedId}}" /> <text>{{item.name}} · {{item.intro}}</text> </label> </radio-group> <radio-group bindchange="onTimeChange"> <label wx:for="{{timeSlots}}" wx:key="id"> <radio value="{{item.id}}" disabled="{{item.booked}}" /> <text>{{item.time}}</text> </label> </radio-group> <button bindtap="submitAppointment" disabled="{{!selectedId || !selectedTimeId}}">提交预约</button>
// pages/appointment/appointment.js Page({ data: { counselors: [], timeSlots: [], selectedId: '', selectedTimeId: '' }, onCounselorChange(e) { const selectedId = e.detail.value; this.setData({ selectedId, timeSlots: this.getTimeSlots(selectedId) }); }, onTimeChange(e) { this.setData({ selectedTimeId: e.detail.value }); }, getTimeSlots(counselorId) { // 从云数据库按咨询师 ID 拉取时间段 return []; } });

这里有一个容易被忽视的参数:disabled="{{item.booked}}"。心理咨询室的时间段必须防重,一个时间段被别人约走之后要立刻置灰。这个booked字段不能只在前端判断,后端云函数在写入预约记录时要同时更新该时间段的booked状态,否则两个学生同时提交就会撞单。真实项目里这个动作要做成事务,云开发数据库支持runTransaction,毕设层面至少也要做到先查询再写入。

radio的value建议用咨询师 ID 而不是下标,这样后续跳转聊天室时能直接拿 ID 查房间,不用再从数组里按 index 取。表单提交前要校验selectedId和selectedTimeId两个值都非空,按钮上的disabled只是第一层防护,submitAppointment里还要再做一次空值判断。

3.3 消息发送与实时监听:watch 和 setData 的配合

聊天室页面的核心是消息列表的实时刷新。云开发数据库的watch方法监听集合变化,但当监听回调触发时,回调里拿到的是整个快照,如果不加处理直接setData整个数组,页面会随着消息增多越来越卡。推荐的做法是只对新增消息做增量追加,而不是全量覆盖。

// pages/chat/chat.js const db = wx.cloud.database(); let watcher = null; Page({ data: { roomId: '', messages: [], inputValue: '' }, onLoad(options) { this.setData({ roomId: options.roomId }); this.watchMessages(); }, watchMessages() { watcher = db.collection('messages') .where({ roomId: this.data.roomId }) .orderBy('createdAt', 'desc') .limit(20) .watch({ onChange: (snapshot) => { const docs = snapshot.docs; // 只把新消息追加到末尾,避免整页刷新 this.setData({ messages: this.data.messages.concat(docs) }); }, onError: (err) => { console.error('消息监听失败', err); } }); }, sendMessage(e) { const content = e.detail.value; if (!content.trim()) return; db.collection('messages').add({ data: { roomId: this.data.roomId, content: content.trim(), createdAt: db.serverDate(), from: wx.getStorageSync('uid') } }).then(() => { this.setData({ inputValue: '' }); }).catch((err) => { console.error('发送失败', err); }); }, onUnload() { if (watcher) { watcher.close(); // 离开页面必须关闭监听 } } });

这段代码有两个必须调好的参数:limit(20)和orderBy('createdAt', 'desc')。desc排序是为了让数据库返回最近 20 条消息,onChange里concat追加的顺序要注意——云开发 watch 返回的snapshot.docs本身是按你查的顺序返回的,如果查询用desc,新增消息会出现在数组头部而不是尾部,我一般会把监听查询改成asc排序后只取时间戳大于本地最后一条消息的记录,这样追加顺序才不会乱。

onUnload里watcher.close()是非常重要的清理动作。聊天室页面经常会有“退出房间再进另一个房间”的操作,不关闭旧监听会同时收到两个房间的消息。这在真机上表现为消息串台,原因不是云开发推送错了,而是你把多个 watcher 同时留在了页面上。

另一个容易被忽略的点是db.serverDate()。它返回的不是本地时间,而是云数据库服务器时间,用来排序不会因为用户手机时间不准而错位。发送消息后清空inputValue时,不要用setData({ inputValue: '' })以外的方案直接操作输入框的value,要用数据绑定让输入框和inputValue保持单向同步,否则会出现“发送后输入框内容没清掉”的玄学问题。

4. 微信小程序顶部导航栏高度与手机号登录:两个最容易背锅的细节

4.1 自定义导航栏:用胶囊按钮坐标反推导航栏高度

心理咨询聊天室不需要默认的导航栏样式,你要放一个“返回咨询师列表”的箭头和“当前咨询师”标题。很多毕设图省事直接用默认导航栏,结果页面顶部留白一大片,截图出来特别不专业。要做自定义导航栏,就得算清楚手机状态栏高度和胶囊按钮位置的关系。

微信提供了一个官方接口wx.getMenuButtonBoundingClientRect(),能拿到右上角胶囊按钮的坐标。胶囊按钮的top减去状态栏高度,再乘 2 加上按钮高度,就是导航栏的实际高度。这个公式在不同机型上都很稳,比去wx.getSystemInfoSync()里东凑一个值西凑一个值靠谱得多。

// utils/nav.js function getNavBarHeight() { const systemInfo = wx.getSystemInfoSync(); const menuButton = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = systemInfo.statusBarHeight; const navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height; return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight + navBarHeight // 页面自定义导航占用的总高度 }; } module.exports = { getNavBarHeight };
// pages/chat/chat.js const { getNavBarHeight } = require('../../utils/nav.js'); Page({ data: { navHeight: 0 }, onLoad() { this.setData({ navHeight: getNavBarHeight().totalHeight }); } });

拿到navHeight后,在 wxml 里给自定义导航容器style="padding-top: {{navHeight}}px;"就行。注意要在app.json里把对应页面配置成"navigationStyle": "custom",否则系统导航栏和自定义导航栏会叠在一起,顶部出现两层标题。

这个计算有一个容易被忽略的前提:必须在页面onLoad之后调用,因为wx.getMenuButtonBoundingClientRect()在页面没有完成渲染时拿到的坐标可能全为 0。如果发现部分安卓机顶部高度不对,先检查是不是在onLoad早期就调用了。胶囊按钮在屏幕最右侧,自定义导航的左侧按钮不要放得太靠右,预留 100 到 120 像素给胶囊按钮区域,否则会被微信的胶囊按钮盖住。

4.2 微信小程序登录获取手机号:getPhoneNumber 的权限边界

很多同学在聊天室里加了一个“微信小程序登录获取手机号”按钮,想让学生用手机号做身份标识。这里要说清楚:如果你用云开发登录,其实不需要手机号,openid已经够用;但如果答辩演示时必须展示手机号授权登录,那就要知道getPhoneNumber并不是按下按钮就能拿到手机号。

<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhone">使用手机号登录</button>
// 页面方法 onGetPhone(e) { if (e.detail.errMsg !== 'getPhoneNumber:ok') { wx.showToast({ title: '授权失败', icon: 'none' }); return; } // 将 e.detail.code 传给云函数,由云函数端调用接口换取手机号 wx.cloud.callFunction({ name: 'getPhone', data: { code: e.detail.code } }).then(res => { const phone = res.result.phoneNumber; // 用 phone 去 users 集合里匹配或建立用户记录 }); }

e.detail.code是动态令牌,不是手机号本身,必须在云函数端调用微信提供的手机号快速验证接口才能换到真实号码。个人主体的小程序默认拿不到这个接口的完整权限,如果你发现真机预览时errMsg一直返回getPhoneNumber:fail,先不要怀疑代码,先确认小程序的认证主体类型。是企业或组织主体,把getPhoneNumber接口权限开好;是个人主体,这个功能基本就是摆设,建议演示时绕过手机号,直接用openid登录。

另外一个细节:bindgetphonenumber在开发者工具里可以模拟成功,但真机上必须是用户点击按钮触发,不能通过wx.login或 JS 直接调用。这个接口和open-type按钮强绑定,别试图在生命周期函数里自动触发,微信不允许。演示录像里如果要展示手机号登录,务必用真机而不是开发者工具录,否则答辩现场容易翻车。

4.3 安全区与输入框:聊天室页面的底部适配

聊天室页面底部要放消息输入框,这个输入框在 iPhone X 以后带 Home 键的机型上会被底部小黑条挡住。原因是这些机型有原生安全区,页面布局要避开底部safe-area-inset-bottom。微信小程序里最简单的适配方式是给输入框容器加一个padding-bottom: env(safe-area-inset-bottom)。

.chat-input-bar { padding-bottom: env(safe-area-inset-bottom); padding-bottom: constant(safe-area-inset-bottom); }

env和constant要成对出现,前者是 iOS 11.2 以后的标准写法,后者兼容 iOS 11.0 到 11.2。安卓没有安全区概念,这个样式不会生效,不需要额外判断机型。聊天记录列表也要相应加scroll-view的高度计算,让最后一条消息不被输入框盖住。

键盘弹起时,聊天室页面的高度变化会导致scroll-view高度失效,常见做法是监听onKeyboardHeightChange回调,把键盘高度加到页面底部偏移里。这个 API 只有真机支持,开发者工具里调不到,调试时要做好心理准备。能用adjust-position="auto"解决的就不用自己算,把输入框放在textarea或input组件上,微信会自动抬升页面,但如果你用了自定义导航栏,抬升逻辑会和你的navHeight冲突,需要把页面整体高度改成100vh并动态调整内容区高度。

5. 微信小程序聊天室避坑手册:审核、真机、socket 与数据量

5.1 类目审核:心理咨询属于特殊行业类目,毕设怎么办

现象:小程序体验版能正常打开,但一提交审核就被拒,理由通常是“涉及心理咨询需提交相关资质”。

原因:微信对心理咨询、医疗健康类目卡得比较严,不是所有主体都能申请。个人主体连入口都找不到,企业主体也需要提供对应的行业资质文件,这些材料学生基本拿不到。

解决:毕业设计不需要上架运营,使用体验版就够了。体验版不上审核流程,把“聊天室”定位成学生互助交流,文案里不要出现“诊断”“治疗”“处方”“干预”等字眼,统一用“倾诉”“预约”“交流”来描述功能。说明文档里写清楚“本项目为校园场景的预约与匿名倾诉演示,不提供专业医疗服务”,既自保又能在答辩时体现你的边界意识。行业资质的事,等真正上线时再交给学校或合作机构处理。另外不要在小程序名称里直接带“心理诊所”“心理医疗”这类词,换“校园心语角落”“心理预约助手”这种偏校园的名称更稳妥。

5.2 真机 socket 连不上:合法域名和心跳保活

现象:开发者工具里 WebSocket 连接一切正常,换到真机就报Error: connect fail,或者显示连接成功但发消息没响应。

原因:两类情况。一类是没有配置 socket 合法域名,微信真机环境只允许连接wss://且域名必须在后台白名单里,IP 地址和ws://一律拒绝。另一类是后端没有心跳保活,运营商或微信底层会回收长时间空闲的连接,客户端还自以为连着,实际已经断了。

解决:自建后端方案里,先把https://和wss://两个合法域名都配好。域名必须备案,SSL 证书不能是自签的,微信后台只信任正规 CA 签发的证书。客户端每 25 到 30 秒发一次heartbeat包,后端 60 秒内没收到心跳就主动断开这条连接;客户端收到system帧里的offline事件后,要做指数退避重连,比如 1 秒、2 秒、4 秒递增,最多重试 5 次。云开发方案没有这个问题,但你要把 watch 的onError回调写全,断网恢复后重新调用watchMessages(),否则跨网络切换后聊天室会静默掉线。

5.3 聊天记录越拉越多,setData 卡出翔

现象:聊天室聊了二十条消息后开始卡顿,输入法弹起时明显掉帧,消息超过一百条后基本没法用。

原因:直接把整个messages数组塞进setData,微信小程序的渲染层每次都会重新 diff 全部节点,消息列表越长开销越大。有些组件库还会在每条消息里嵌套wx:for,性能雪上加霜。

解决:聊天室只渲染最近 20 到 50 条消息,更早的消息通过“上拉加载更多”分页拉取。发送新消息时用this.setData({ 'messages[0]': newMsg })这种路径更新指定下标,不要concat整个数组。另外给wx:for加wx:key="messageId",避免微信靠下标渲染导致节点复用错乱。如果性能还是不够,把每条消息拆成独立组件,并开启组件的lifetimes里detached清理监听,减少页面的重渲染范围。真机调试时打开 Performance 面板,重点看 setData 的传输字节数,超过 100 KB 基本就是渲染负担过重了。

5.4 云开发 watch 的作用域坑:roomId 过滤别漏

现象:云开发 watch 一接上,消息列表把其它房间的聊天记录都显示出来了。

原因:watch监听的是集合级变化,不是消息级变化。如果你的where条件里忘了带roomId,哪怕你在前端页面里传了roomId,数据库也只会按你实际查询的条件推送。这个错误在小房间数据量少时不容易发现,因为刚好只监听一个房间;一旦有两个学生同时进入不同房间,消息立刻乱掉。

解决:watch前先检查where条件,必须包含房间维度的过滤字段。建议把roomId放到消息文档的顶层字段而不是嵌套对象里,嵌套对象的查询条件容易因为字段路径写错而失效。还有一个坑:watch的onChange回调里拿到的snapshot.docs是完整快照不是增量,重复进入房间会造成重复 append。解决办法是维护一个lastMessageId变量,只追加docs里messageId不在本地列表中的消息。这个去重逻辑不写,表面上功能正常,但用户切后台再回来时就会看到自己刚才发的消息又刷了一遍。

5.5 调试时的清理工作

现象:代码逻辑没问题,但云函数调一次就报超时或数据错乱。

原因:云函数不是改成一次就会自动生效。微信开发者工具里云函数修改后需要右键“上传并部署:云端安装依赖”,本地改完直接跑,跑的还是旧代码。另外多个环境共享同一个云函数时,代码部署到了 A 环境,页面却初始化到了 B 环境,控制台报错永远对不上号。

解决:给云函数部署流程固定下来:改代码 → 右键上传部署 → 等状态变成“部署成功” → 再在页面里调用。创建环境时用一个环境,不要随手建多个环境,环境 ID 写在app.js里不要用变量拼接。真机调试时在云开发控制台打开“云函数日志”,看冷启动时间是不是超过 2 秒,超时的话在云函数入口加cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })能减少跨环境的初始化开销。所有坑里,环境不匹配和部署遗漏这两个最隐蔽,而且越到答辩越容易手忙脚乱。

6. 交付演示录像:五分钟讲清楚一个聊天室

演示录像不是把屏幕从头录到尾,它是你答辩时没人在场时的替身。常见的做法是先写一段 5 到 8 分钟的脚本,分三个场景:登录与身份识别、咨询师选择与预约、双端聊天实时收发。每个场景开始前在纸上列一句台词,不要照着念,但要保证关键功能都有特写。录像时长不建议超过 10 分钟,评审老师没有耐心看第三遍。

录屏时用系统自带的录屏功能即可,有条件的用 OBS 加手机投屏录真机画面。先用开发者工具录一遍功能链路,再用真机录一遍关键页面,最后剪辑时把两次素材穿插起来。真机素材里要注意关闭隐私通知和微信自带的悬浮消息,否则录到一半弹出来一条外卖广告,观赏性直接归零。聊天内容提前准备几条无害的示例文本,比如“老师,我这周考试压力有点大”,不要真的往录入画面里塞敏感内容。

录像的结束画面停在“消息已发送并在另一个账号上同步出现”这种带结果感的界面,比停在设置页强得多。这也是我一个人做这类项目时吃过最直接的亏:功能全都验证过,但录像里没录到双账号同步,答辩老师追问那一步时我当场现开模拟器,节奏全乱。后来我把“第二个账号的消息出现”做成每个演示录像的固定收尾动作,问题就再没出现过。希望帮到你。

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