简介:面向南航师生及校园信息化开发者,这是一套官方综合服务微信小程序的完整源码,将校园导航、课表与成绩查询、空教室查询、校车时刻、图书馆座位预约、校园卡在线充值、食堂菜单、社团活动、二手交易、失物招领、校友交流与就业信息等十几项高频功能集成于一体,省去在多平台间切换的麻烦。源码覆盖日常办事与学习场景,既适合学生快速处理校园事务,也适合开发者参考真实校园服务的模块组织。压缩包共87个文件,整体约138KB,以22个js逻辑脚本、17个json配置、16个wxss样式、15个wxml页面结构为主体,另有13个png图标资源及README.md、txt、docx说明文档,目录按pageMain、message、tools、me等模块划分,便于定位页面与对应逻辑。目前已有73人学习下载,可参考其进行课程项目设计、校园信息化产品原型演示,或直接作为二次开发的功能基础。
1. 校园服务小程序不是功能堆砌,而是一套可以复用的校园数字化底座
南航的校园服务综合平台微信小程序,把校园导航、课表查询、成绩查询、空教室查询、校车时刻、图书馆预约、校园卡充值这些高频场景收敛到了一个微信入口里。我接过不止一个校园信息化项目,这类需求真正的痛点不是某个功能写不出来,而是入口分散——教务系统一个账号、图书馆一个系统、一卡通一个 App,学生查成绩和预约座位要反复切换,最终结果是每个系统都在吃灰。这个项目把高频服务聚合到微信小程序里,对开发者来说,它是一套标准的校园服务脚手架:地图导航用微信原生地图组件,课表成绩走登录态接口,社区模块不过是列表加表单的组合。这篇笔记适合三类人——接校园外包的工程师、学校信息中心的技术负责人、想用真实业务练手的小程序开发者。下面按选型、核心模块、服务模块、避坑、上线验证的顺序拆开讲。
2. 技术栈与工程结构:原生小程序加分包的选型理由
2.1 选原生还是 uni-app:我为什么用原生开发
拿到这个标题时,第一个问题是技术栈。市面上校园类小程序大量用 uni-app 或 Taro 做跨端,但这里我明确倾向原生微信小程序。原因很简单:这个项目高度依赖微信生态能力——定位、支付、订阅消息、地图组件,这些能力在原生环境里调用最直接,调试时报错信息也最完整。uni-app 的价值在于多端复用,但这里要的是校园场景,微信是最好的载体,没有明显的跨端诉求,引入框架反而多一层编译链路。
另外这类项目的开发者通常是学校信息中心或外包团队,原生小程序的维护门槛最低。任何会 Vue 的人都能快速上手,不需要额外学习 uniapp 的打包配置和平台差异处理。实战中我还见过 uniapp 打包后 source size 超过 2MB 上限的情况,真机预览直接被开发者工具拦截,排查链路比原生长很多。原生开发配合分包,体积控制是可控的。
2.2 目录结构与分包规划
功能模块多,意味着页面多,所以工程结构一开始就要按分包规划,不能等包体积超了再拆。我一般把 tabBar 页面、公共组件、登录态工具放在主包,其余按业务域拆成分包。常见划分如下:
- 主包:pages/index、pages/course(课表)、pages/grade(成绩)、pages/mine(我的),公共组件和 utils
- 分包 classroom:空教室查询、图书馆预约
- 分包 life:校车时刻、食堂菜单、校园卡充值、失物招领
- 分包 community:社团活动、二手交易、校友交流、就业信息
- 分包 nav:校园导航(地图是大组件,独立分包加载)
这样做的直接好处是主包控制在一两 MB 以内,首屏只加载 tab 页。校园导航和社区这种低频但体积大的页面延迟到用户点击时才下载,体感上冷启动快很多。目录结构上,我自己习惯每个分包内页面平铺,共享的 request 封装和 token 管理放在主包的 utils 下,分包通过相对路径引用,避免循环依赖。
2.3 全局配置:tabBar 与导航栏高度适配
全局配置集中在 app.json 里,tabBar 是第一个要处理的点。微信小程序 tabBar 最多 5 项,这个项目里有 13 个功能模块,所以只能把最高频的入口放 tab——一般选首页、课表、成绩、服务大厅、我的。其余模块在首页或服务大厅里用九宫格入口跳转,这样既满足高频访问,又绕开数量限制。
导航栏高度是新手必踩的坑。标题里提到顶部导航栏高度,实际场景是页面用自定义导航栏时,不同手机的胶囊按钮位置和状态栏高度不一致。我在 utils 里放一个系统信息封装:
const systemInfo = wx.getSystemInfoSync(); const statusBarHeight = systemInfo.statusBarHeight || 20; const menuRect = wx.getMenuButtonBoundingClientRect(); const navBarHeight = (menuRect.top - statusBarHeight) * 2 + menuRect.height; module.exports = { statusBarHeight, navBarHeight };这段代码的意思是:状态栏高度从系统信息里拿,导航栏高度用胶囊按钮的 top 减去状态栏高度,再乘以 2 加上胶囊本身高度。因为胶囊按钮垂直居中的导航栏上,这个公式算出来的是标准导航栏高度,适配 iPhone 和安卓的差异。注意这段要在 Page 构造器外执行一次并缓存,不要每个页面重复 getSystemInfoSync,会拖慢启动。
app.json 里的 pages 数组要按分包注册,主包的页面放前面,预下载规则可以配合 loading 页做分包预取。这里提醒一句:导航栏高度适配一定要真机验,开发者工具里模拟器的高度值和真机有偏差,后面避坑章节会展开讲。
3. 核心查询模块:校园导航与课表成绩的实现要点
3.1 校园导航:地图组件与定位授权的调用姿势
校园导航这个功能,本质上不是“导航”而是“找楼”。校园里更常见的诉求是新生找食堂、找体育馆、找某个学院的楼,真正点到点的路线导航需要高德或腾讯地图的步行路线能力。我见过有人在这个模块集成天地图,但校园场景用微信原生地图组件就够了,省去天地图的服务申请和密钥配置。实现分两级:楼宇列表页和地图定位页。
列表页维护一个楼宇信息 JSON 数组,字段包括楼宇名、别名、经度、纬度、简介、照片。经纬度我建议用 GCJ-02 坐标,微信地图组件默认支持这个坐标系,直接填 WGS-84 会出现几十米偏移。点击某栋楼,调用 wx.openLocation 打开地图:
wx.openLocation({ latitude: 31.9986, longitude: 118.8212, name: '将军路校区图书馆', address: '南京市江宁区将军大道29号', scale: 18 });参数说明:scale 是地图缩放级别,16 是街道级,18 可以看到建筑轮廓,适合校园楼宇定位。name 和 address 会显示在底部卡片上,这里要填业务名,比如“东区食堂(二食堂)”,不要只填“食堂”,否则用户定位到了还要猜是哪栋。
授权问题是这个模块最常见的故障点。小程序里获取定位需要用户授权 scope.userLocation,拒绝一次之后再触发授权弹窗会被系统拦截。我一般把引导逻辑做成“先尝试拿定位,失败则提示去设置页打开授权”,而不是一进页面就弹窗。校园导航默认展示校区总览图和楼宇列表,定位只是增强项,不授权也不影响核心功能,这样授权失败率会低很多。真机上还要确认 app.json 里声明了 requiredPrivateInfos 字段,不声明直接调用 getLocation 会在正式版报错。
3.2 空教室查询:筛选逻辑与数据源设计
空教室查询在技术上不难,难的是数据源。教务系统的课表数据一般不会直接对外开放,常见做法是学校信息中心导出一份学期课表数据,按楼宇、教室、周次、节次建索引。前端拿到数据后做筛选,逻辑比较直接:
function filterFreeRooms(rooms, timetable, day, period) { return rooms.filter(room => { const hasClass = timetable.some(item => item.roomId === room.id && item.day === day && item.period.indexOf(period) !== -1 ); return !hasClass; }); }这里 roomId 是教室主键,day 是周一到周日的数字 1 到 7,period 是节次数组。注意课表里的节次不是单个数字,比如“1-2 节”是一个时间段,所以 period 用数组存,前端传 1 或 2 时做包含判断,而不是相等判断。这是空教室查询最容易写错的地方,很多新手直接 item.period === period,查出来的结果全错。
筛选之外要支持楼宇和教室类型过滤,我一般在前端做二级筛选:先选校区,再选楼宇,最后选节次。校区切换会触发教室列表重新加载,教室类型(普通教室、多媒体教室、实验室)作为房间字段里的一个枚举。数据量几千条放本地没问题,但更新的时效性很关键——碰到调课、节假日停课,数据源不更新就会导出错误结果。我见过一个学校的系统在考试周完全不更新课表,空教室查询结果直接变成“全校满课”,用户以为系统崩了,其实是数据源停更。解决的办法是在页面顶部显示“数据更新日期”,并加一个缓存过期提示,让用户知道当前结果基于哪一天的课表。
3.3 课表查询:周次计算和单双周处理的数学
课表查询是校园小程序的刚需,但它的难点不在 UI,而在周次计算。国内高校普遍有“教学周”概念,开学第一周是第 1 教学周,部分课程标注“单周上课”或“双周上课”,还有的课程第 3 周到第 16 周。前端要算出“今天是第几教学周”,再根据课程周次标记判断今天的课程。
function getTermWeek(termStartDate) { const start = new Date(termStartDate); const now = new Date(); const startTime = start.getTime(); const nowTime = now.getTime(); const diffDays = Math.floor((nowTime - startTime) / (1000 * 3600 * 24)); const week = Math.floor(diffDays / 7) + 1; return week > 0 ? week : 1; } function isThisWeek(course, currentWeek) { if (course.weekRange && (currentWeek < course.weekRange[0] || currentWeek > course.weekRange[1])) { return false; } if (course.weekType === 'odd' && currentWeek % 2 === 0) return false; if (course.weekType === 'even' && currentWeek % 2 === 1) return false; return true; }这个函数的核心是拿当前时间减去开学日期,得到天数差再除以 7。这里有几个坑:第一,Date 对象要统一用本地时区,不要用 Date.parse 或 toISOString,否则遇到 UTC 时区转换会差 8 小时;第二,开学日期要从接口拿而不是写死,因为每学期会变;第三,termStartDate 要精确到当天的零点,否则晚上 11 点查课表会多算一天。我一般让后端返回“开学周的周一日期”,前端只做减法取整。
课表渲染用表格形式,横向是节次,纵向是周一到周日。注意手机上横向空间有限,我习惯用横向滚动条配合固定第一列。课程冲突是另一个隐藏需求——有些同学会跨年级选课,同一个时间段出现两门课,这时候展示层要支持重叠课程并排显示,后台不能直接覆盖。
3.4 成绩查询:登录态与接口鉴权的最小流程
成绩查询比课表多一个关键环节:登录态。成绩属于敏感数据,不能放在本地缓存,必须每次从接口拉取。这里的前端流程是:进入页面时先检查本地 token,有 token 直接请求成绩接口;接口返回 401 时,触发静默登录——调用 wx.login 拿 code 换 openid,后端再用 openid 换 token,然后重放原始请求。
function fetchWithAuth(url, data) { return new Promise((resolve, reject) => { wx.request({ url, data, header: { token: wx.getStorageSync('token') }, success(res) { if (res.data.code === 401) { silentLogin().then(() => { handleWithAuth(url, data, resolve, reject); }).catch(() => { wx.navigateTo({ url: '/pages/login/login' }); }); } else if (res.data.code === 0) { resolve(res.data.data); } } }); }); }这段封装做的事情是:所有带鉴权的请求都走这一个函数,token 失效时自动用 wx.login 的 code 换取新 token,然后重试一次请求;重试还失败就跳登录页。注意 wx.login 拿到的 code 只能用一次,有效期五分钟,所以 silentLogin 里拿到新 token 后要立刻写入 storage。成绩接口返回的数据一般包含课程名、学分、成绩、绩点、补考标记,前端不要自己算绩点——不同学校的绩点算法不一样,让后端按教务处规则算好返回,前端只做展示。成绩页还有一个隐藏需求是“本学期 vs 全部学期”切换,这个在前端用一个 segmented 控件切接口参数就行。登录态是整个小程序的地基,写不好后面所有模块都会跟着串号,这点在第 5 章避坑里细讲。
4. 服务与社区模块:从图书馆预约到二手交易的通用模板
4.1 校车时刻与食堂菜单:静态数据也要有接口
校车时刻和食堂菜单这两个模块看起来最简单,但它们是最容易被低估的。校车时刻表有工作日和节假日的区别,还有寒暑假的班次调整;食堂菜单每天变,但展示上通常只需要午晚餐的菜品列表和价格。我建议这两个模块都走后端接口,不要把数据写死在代码里——写死代码意味着每次更新要发版本,而校园信息变更频率远高于发版频率。
数据设计上,校车时刻表用一条“线路 + 方向 + 发车时间 + 乘车地点”的记录结构,接口返回当天的班次列表,前端用 timeline 组件按时间排序。食堂菜单按“食堂 ID + 档口 + 菜品 + 价格”的结构返回,前端分组渲染。这两个模块最实用的功能是“当前状态”——比如当前时间是否还有校车、食堂是否在营业时间。我一般会加一个计算属性:拿当前时间和班次表做比较,计算出“下一班 X 分钟后发车”或“食堂还差 30 分钟打烊”。这个功能比单纯的时刻表有用得多,用户打开小程序就是为了看“现在有没有车、去哪个食堂能吃上饭”。
4.2 图书馆预约与校园卡充值:有状态的流程怎么设计
图书馆预约和校园卡充值是有状态的操作,和上面纯展示的模块完全不同。图书馆预约的流程是:选座位 → 选时间段 → 确认预约 → 到馆签到 → 释放座位。前端每一步都是一个独立接口,设计上要注意“状态机”的概念——用户在页面上做的每个动作都必须有对应的后端状态流转,不能靠前端页面跳转来模拟状态。
我一般把预约页拆成两个:选座页和预约记录页。选座页展示座位图,座位状态用颜色区分(绿色可约、黄色已占用、灰色不可用),点击座位后弹出时间段选择。提交预约时前端要传一个幂等键,一般用时间戳加随机数,防止用户重复点击提交产生多条预约。校园卡充值的流程要更谨慎,涉及微信支付,核心是调起支付前必须从后端拿到支付参数,支付成功后要轮询订单状态,而不是直接显示成功——微信支付回调有延迟,直接显示成功可能造成金额到账但业务未入账的对账差异。我的习惯是支付成功后显示“支付处理中,请在订单页查看结果”,然后轮询订单接口。
function pollOrder(orderId, times = 0) { if (times > 10) { wx.showToast({ title: '订单处理较慢,请稍后查看', icon: 'none' }); return; } wx.request({ url: `${baseUrl}/order/${orderId}`, success(res) { if (res.data.data.status === 'SUCCESS') { wx.showToast({ title: '充值成功', icon: 'success' }); } else { setTimeout(() => pollOrder(orderId, times + 1), 3000); } } }); }这段轮询逻辑的要点是次数限制,防止接口异常时无限递归。每次轮询间隔 3 秒,最多 10 次,超过就提示用户稍后查看,这样可以避免长时间占用请求资源。注意支付结果不能信任本地缓存,一切以订单接口返回为准。
4.3 社团活动与二手交易:UGC 模块的通用表单
社团活动、二手交易、失物招领、校友交流、就业信息这五个模块在技术上高度相似:都是“发布者创建内容,浏览者查看列表,详情页展示完整信息”。唯一有差异的是二手交易,它多了图片上传和交易状态的流转(在售、已预订、已成交)。我把这几个模块抽成一套通用的 UGC 模板:列表页 + 详情页 + 发布表单。
发布表单的核心是图片上传,代码上有几个注意点:
wx.chooseMedia({ count: 9, mediaType: ['image'], success(res) { const uploadTasks = res.tempFiles.map(file => { return new Promise((resolve, reject) => { wx.uploadFile({ url: `${baseUrl}/upload`, filePath: file.tempFilePath, name: 'file', success(uploadRes) { const data = JSON.parse(uploadRes.data); resolve(data.url); }, fail: reject }); }); }); Promise.all(uploadTasks).then(urls => { that.setData({ imageUrls: urls }); }); } });这段代码的处理逻辑是:选择图片后并行上传,全部完成后统一把 URL 写入表单数据。关键点是 Promise.all,如果逐张上传并等待再传下一张,用户上传 9 张图的等待时间会累加到不可接受。还有一点:上传前要检查图片大小,微信小程序里图片超过 10MB 上传大概率超时,客户端先压缩再上传是常见做法,用 canvas 把图片压到 2MB 以内再走上传链路。
二手交易分类用 radio 单选,这个模块的核心字段是“分类(教材/电子产品/生活用品)+ 成色 + 价格 + 交易地点 + 联系方式”。失物招领要加“拾取地点和时间”,校友交流可以复用同一个模板但字段是“标题 + 正文 + 图片”。社团活动的差异化在于时间字段,要支持“活动开始时间”和“报名截止时间”,前端需要用日期选择器而不是纯文本输入。就业信息一般不需要图片,用富文本或纯文本展示招聘要求就够了,招聘信息的格式模板用职位、公司、薪资范围、投递方式即可。
5. 校园服务小程序避坑实录:登录态、地图黑屏与体积超限
5.1 登录态串号与 Token 静默续期
校园小程序的登录态问题比一般电商小程序更典型,因为用户在课表和成绩之间频繁切换,如果登录态管理做得粗糙,会出现“A 同学查到了 B 同学的成绩”这种严重事故。现象是同一台设备上切换账号后,老 token 没清干净,所有请求都带着旧身份。原因多半是 token 只存在一个全局变量里,登录新账号时只覆盖了部分模块的缓存。
解决的办法是统一封装 request,在登录成功时把旧 token 清除并写入新 token,所有页面不得直接操作 storage 里的 token。我还会在校验接口里带一个 user_id 字段,后端在校验 token 时同时返回 user_id,前端把返回的 user_id 和当前登录用户的 user_id 做比对,不一致则强制退出登录。这是双保险——即使前端缓存错乱,后端也不会把别的用户数据返回给页面。实践中碰到过旧 token 已经过期但静默续期失败的情况,这时的策略是不要无限重试,重试一次失败就跳登录页,至少让用户知道自己需要重新登录,而不是卡在页面里看不了任何数据。
5.2 手机号获取失败,动态验证码兜底
手机号绑定是校园服务里常见的一环,比如二手交易联系人和校友交流都需要手机号。很多开发者第一反应是用微信的“获取手机号”按钮组件,但这里有个现实问题:微信的 getPhoneNumber 接口需要小程序认证,而且评论区反馈部分 Android 机型上返回的 encryptedData 解析不稳定,偶尔直接报错。
我一般做双重方案:优先使用微信手机号快速验证组件,失败时降级到短信验证码。短信验证码看起来老套,但胜在稳定,而且能覆盖微信组件解析失败的场景。实现上,动态验证码需要在后端存有效期 5 分钟的 code,前端输入后走校验接口。这里有个体验细节:验证码发送按钮要有 60 秒倒计时,倒计时期间按钮置灰,防止用户重复点击导致短信轰炸。微信手机号组件的回调里有 code,后端拿 code 换手机号,注意这个 code 和 wx.login 的 code 是两回事,不要混淆。
5.3 地图组件真机黑屏:key 与域名白名单
校园导航地图在开发者工具里一切正常,一上真机就黑屏或者只显示网格线,这是地图模块最常见的故障。原因通常是两类:一是微信的腾讯地图服务需要小程序后台配置域名白名单,二是不知道什么时候开始,地图组件在正式版里还需要在 app.json 配置 requiredPrivateInfos 中的 getLocation 声明,漏了就直接拒绝调用。
排查顺序我一般是:先开 vConsole 看报错信息,如果报“getLocation:fail the api need to be declared in the requiredPrivateInfos field”,就是声明缺了,补上再试试。如果报“invalid key”,去腾讯地图开放平台查看 key 的绑定域名是否包含小程序的 appid。这个 key 在开发时可以选“未校验域名”,但发布前一定要换成正式配置,否则审核和线上都会出问题。还有 Android 和 iOS 对定位权限的弹窗文案不同,iOS 弹窗只显示一次,用户拒绝后再次唤起必须引导去系统设置里打开,这个在代码里给出提示文案即可。地图黑屏问题如果用了自定义 map 组件 layers,还要看 layer 图层是否和 map 组件冲突,复杂情况下建议把地图功能独立分包,减少和主包组件的叠加干扰。
5.4 主包体积超限:分包之外再砍两刀
小程序主包体积限制是 2MB,超过这个数开发者工具直接报错,用户扫码后也会提示加载失败。这个标题下的功能模块多,按功能分包之后,主包里残留的公共文件往往还是不知不觉超过 2MB。我自己的经验是两个大头:图片和第三方库。
图片是最容易超限的。很多开发者图省事直接把图片放在 assets 里,一张高清校园图就几百 KB,放十几张包就没了。正确做法是图片全部传 CDN,本地只保留 tab 图标和必须的占位图,并且 tab 图标用 81x81 像素的 PNG 或直接用 SVG 图标字体。另一个大头是第三方库。小程序里引入 WeUI、Vant 组件库之类的时候,要用按需引入,不要 import 整个库。Vant 里只用 button 和 dialog,就只 import 这两个组件文件,不要整包引入。最后还有一个技巧:开发工具里看主包体积时,要关注的是“打包后的大小”而不是源码大小,压缩开关和 ES6 转 ES5 的选项会导致体积不同,本地验证时用“真机预览”模式更准确。
5.5 订阅消息弹窗被拒后怎么补救
订阅消息是校园服务里通知的载体——预约成功通知、课表变动通知、失物招领有人认领通知。微信的订阅消息现在做了一次性订阅,弹窗授权被用户拒绝是常态,很多同学看到弹窗第一反应是点“取消”。
我在这里踩过的坑是:每次打开小程序就弹订阅消息授权,用户烦了以后连功能都不用了。后来改了策略:先让用户完成一次操作,比如提交预约成功后再弹订阅授权,这时候用户是“有预期”的,授权率明显提高。对于一次性订阅只能接收一次通知的限制,我的做法是引导用户勾选“接下来一周都接收通知”,后端去调多次订阅的接口,一次授权获取多次投递资格。还有一个补救手段:在小程序内部做一个“消息中心”页面,即使订阅消息被拒,用户仍可以手动打开消息中心查看历史通知和预约状态,保证消息不丢失。这个页面和订阅消息形成互补,一边是被动推送,一边是主动查询,对于用户来说都比“弹窗弹到烦”体验好得多。
6. 进阶:缓存预加载与发布前要做的三件小事
6.1 接口缓存与骨架屏
校区服务的接口响应速度一般不太快,特别是成绩和课表这类要查教务系统的接口,动辄两三秒。我用了一个简单的缓存封装来降低慢接口的体感:
function getCache(key, expire) { const data = wx.getStorageSync(key); if (!data) return null; if (Date.now() - data.timestamp > expire) return null; return data.value; } function setCache(key, value) { wx.setStorageSync(key, { value, timestamp: Date.now() }); }这个缓存的逻辑是:接口返回成功的同时写入缓存,下次进入页面先用缓存渲染,再在后台请求新数据做对比。expire 按模块设置——校车时刻表缓存 5 分钟,课表缓存 1 小时,成绩缓存 12 小时。缓存失效时显示骨架屏而不是空白页,骨架屏用 CSS 动画模拟列表区块的加载状态,成本低但体验提升非常明显。这是我在实际项目里被用户夸过的点,校园用户用手机比较杂,低端机上慢接口配骨架屏,观感比 loading 转圈舒服得多。
6.2 发布前要做的三件小事:真机预览、抓包验登录、降级测试
最后分享发布前的三个验证习惯。第一个是强制真机预览,不要只在开发者工具里跑。开发者工具模拟器里定位、地图、下拉刷新这些表现和真机差异很大,特别是顶部导航栏高度,模拟器里算的数值真机上可能偏三四像素,导致自定义导航栏按钮错位。第二个是抓包验证登录链路,我习惯用 Charles 抓小程序的请求,看登录态每次请求是否带上正确的 token、401 时静默续期是否真的生效。这类问题不抓包是发现不了的,因为你肉眼看到的页面可能因为缓存掩盖了 token 失效的事实。
第三个是降级测试——把 wifi 关掉用 4G 网络,或者打开开发者工具的弱网模拟,把请求限速到 100KB/s,看慢网络下图片加载、地图渲染、上传进度条是否正常。校园场景里宿舍区网络质量不稳定,弱网下最容易暴露的问题是没有加载超时控制——请求挂起十几秒既不成功也不失败,用户只能杀掉小程序重开。我的做法是 wx.request 里设 timeout 默认 10 秒,超时后弹 toast 提示“网络不给力,请稍后重试”,并提供重试按钮。这个习惯帮我挡下了不少用户投诉,至少用户知道是网络问题而不是小程序坏了。以上这些经验都是在一个个项目里滑铁卢换来的,希望帮到你。
本文还有配套的精品资源,点击获取