简介:一套面向Java毕业设计场景的社区团购系统完整项目,采用SSM(Spring+SpringMVC+MyBatis)与微信小程序搭建前后端,适合计算机专业学生用于毕业设计、课程设计或项目实战参考。压缩包共1149个文件,大小52.07MB,其中java与xml、sql用于后端业务与数据库,vue、js、wxml、wxss负责管理端和微信小程序界面,png、jpg等图片资源覆盖页面与演示素材,另有pptx答辩PPT、doc开题报告/使用文档及mp4演示视频,部署说明齐全。已有142人学习,项目经过Window10/11环境调试,下载后可按教程快速运行。内容包含完整源码、数据库脚本、答辩PPT、开题报告、使用文档和演示视频,目录结构清晰,适合从需求分析、前后端实现到部署答辩全流程借鉴,可作为高分毕设选题参考。
1. 为什么SSM+微信小程序仍是社区团购毕业设计的稳妥选择
先说个反直觉的结论:2024 年打开招聘网站全是 Spring Boot,但拿 SSM 做毕设一点都不丢人,尤其在社区团购这个题目上,SSM+微信小程序反而是最好讲、最好答、最好改的组合。社区团购的业务主线是「团长开团 → 用户小程序下单 → 后台处理订单 → 自提点核销」,流程完整但复杂度适中,恰好能把 Spring、SpringMVC、MyBatis 三层各管的活讲清楚,数据库也能画出五六张表的关系图,论文和答辩素材天然就够。
这套高分项目正是这么设计的:后端 SSM,管理端 Vue,用户端微信小程序,还附带 MySQL 数据库脚本、开题报告、PPT、使用文档和演示视频,实测环境是 Win10/Win11。对急着交毕设又不想从零搭框架的人来说,它的价值在于每个文件都能对上业务,部署完直接看效果。对想拿高分的人来说,项目里可扩展的点也很明显,加一个模块就能变成自己的东西。
适合三类人:一是正在做 SSM 相关毕设、想找一套完整参考系统的同学;二是对微信小程序不熟、不想在前后端联调上耗时间的;三是打算二次开发再拿去答辩、需要一份底子干净的项目源码的。
2. 技术栈拆解:SSM 三层在团购业务里各管哪一段
2.1 看看解压目录里的 .bak 和 .bat,先搞清项目长什么样
打开压缩包,第一眼看到的往往不是 .java 文件,而是一堆 .vue 的备份文件和三个批处理脚本:main.css.bak、IndexAsideStatic.vue.bak、BreadCrumbs.vue.bak、IndexHeader.vue.bak、update-password.vue.bak,以及 1-install.bat、2-run.bat、3-build.bat。这个结构说明一件事:项目并不只有 SSM 后端,而是拆成了三个端。
- 后端:SSM 的 Java 服务,跑在 Tomcat 上,对外提供 REST 接口,默认端口常见的是 8080。
- 管理端:Vue 2 + Element UI 写成的后台管理页面,开发模式用 Node 启动,访问端口通常是 9527 或 8081。
- 小程序端:微信原生小程序,直接调后端的 HTTP 接口,不依赖管理端。
三个 .bat 文件分别对应管理端的依赖安装、开发启动和生产构建。1-install.bat 里面一般是npm install,2-run.bat 是npm run serve,3-build.bat 是npm run build。那些 .bak 文件是作者改管理端样式时留下的备份,改崩了可以随时还原,这个习惯在调 Element UI 主题时其实很实用。
这里需要提醒一下:SSM 是后端,Vue 管理端和小程序是前端。你在答辩 PPT 上可以大胆画三条线:SSM 服务端、Vue 管理后台、微信小程序用户端。技术栈的丰富度对评分是有帮助的,因为评审老师看到的是一套完整的现代前后端架构,不是老旧的 JSP 页面。
2.2 Spring、SpringMVC、MyBatis 在团购业务里的职责划分
标准 SSM 分层在社区团购这个业务里可以这样对应:
- Spring 管 Service 层的 Bean 和事务控制。最典型的场景是下订单:扣库存和生成订单必须在一个事务里,任何一个失败都要整体回滚。
- SpringMVC 管 Controller 层,负责接收小程序发来的 HTTP 请求,把 JSON 参数绑定成对象,再调 Service 处理。
- MyBatis 管 Mapper 层的 SQL 映射,写团购商品、订单、用户表的增删改查。
用下单这个动作串一遍三层,比背概念有用得多。Controller 收到小程序的 POST 请求,转给 Service 层做业务校验,Service 再调 MyBatis 的 Mapper 接口执行 SQL,这就是一次完整的请求生命周期。
看一个简化版 Controller 代码:
// OrderController.java @RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/create") public Result createOrder(@RequestBody OrderCreateDTO dto) { // 参数校验略 Order order = orderService.createOrder(dto.getUserId(), dto.getGroupId(), dto.getItems()); return Result.success(order); } }这段代码里,@RestController是 SpringMVC 提供的注解,表示这个类的所有方法都返回 JSON。@RequestMapping("/api/order")定义了接口的基础路径,小程序端请求时拼接成http://localhost:8080/api/order/create。@PostMapping("/create")限定只接收 POST 请求,@RequestBody把小程序的 JSON 请求体直接反序列化成 DTO 对象,省去了手动解析 JSON 的麻烦。
接下来是 Service 层的核心逻辑:
// OrderServiceImpl.java @Service @Transactional public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private GroupBuyMapper groupBuyMapper; @Override public Order createOrder(Long userId, Long groupId, List<OrderItem> items) { // 1. 校验团购活动状态 GroupBuy group = groupBuyMapper.selectById(groupId); if (group.getStatus() != 1 || group.getEndTime().before(new Date())) { throw new BizException("团购活动已结束"); } // 2. 扣减库存,这里用了条件更新防止超卖 int updated = groupBuyMapper.deductStock(groupId, items.size()); if (updated == 0) { throw new BizException("库存不足"); } // 3. 生成订单,状态 0 表示待支付 Order order = new Order(); order.setUserId(userId); order.setGroupId(groupId); order.setStatus(0); order.setCreateTime(new Date()); orderMapper.insert(order); return order; } }@Transactional是 Spring 的声明式事务注解,方法执行过程中只要抛出 RuntimeException,前面的数据库操作全部回滚。这里的扣库存和生成订单是在不同表上的操作,不放在一个事务里就会出现「订单生成了但库存没扣」或者「库存扣了但订单失败」的问题,这在答辩时属于必问点,提前把事务概念想清楚能省不少麻烦。
还是 MyBatis 的 Mapper XML:
<!-- OrderMapper.xml --> <insert id="insert" parameterType="Order"> insert into t_order(user_id, group_id, status, create_time) values(#{userId}, #{groupId}, #{status}, #{createTime}) </insert> <update id="deductStock" parameterType="map"> update t_group_buy set stock = stock - #{count} where id = #{groupId} and stock >= #{count} </update>注意deductStock这条 SQL,它在where里带了stock >= #{count}条件,只有库存够才会更新成功,返回受影响行数为 0 就说明库存不足。这比「先 select 查库存,再 update 修改」的写法安全,因为 select 和 update 之间存在时间差,并发请求下很容易超卖。这种条件更新的写法在秒杀、团购类系统里很常见,答辩时可以主动提一句,老师会觉得你有工程意识。
2.3 数据库表设计与订单主链路
社区团购项目的核心表大概有五到六张,下面这张表基本覆盖了主链路:
| 表名 | 业务作用 | 关键字段 |
|---|---|---|
| t_user | 小程序用户信息 | id, openid, nickname, phone, role |
| t_group_buy | 团购活动与商品 | id, goods_name, price, stock, start_time, end_time, status |
| t_order | 订单主表 | id, user_id, group_id, status, create_time, pickup_code |
| t_order_item | 订单明细 | id, order_id, goods_id, quantity, price |
| t_pickup_point | 自提点 | id, name, address, manager_id |
用户通过小程序加入某个团购活动后,系统先在 t_group_buy 里扣库存,再往 t_order 插入一条主单记录,同时往 t_order_item 插入商品明细,最后根据用户选择的自提点生成一个提货码。用户到自提点报号,团长核销后订单状态从待自提变成已完成。
这段逻辑在数据库层面是两张表协同工作:t_order 管状态,t_order_item 管商品快照。所谓商品快照,是把下单那一刻的商品名称、价格、数量复制进明细表,即使之后商品改价或者下架,历史订单依然有据可查。这个设计在老项目里经常没做,但做了之后答辩时讲数据一致性会非常有底。
查询某个用户的历史订单,SQL 大概是这样的:
SELECT o.id, o.status, o.create_time, g.goods_name, g.price FROM t_order o LEFT JOIN t_group_buy g ON o.group_id = g.id WHERE o.user_id = #{userId} ORDER BY o.create_time DESC这条 SQL 用 LEFT JOIN 把订单主表和团购活动表关联起来,取商品名称和价格。#{userId}是 MyBatis 的预编译占位符,可以防止 SQL 注入。用ORDER BY o.create_time DESC做倒序排列,让最新的订单排在前面——这在微信小程序端展示「我的订单」列表时是基础操作,相关热词里那个「微信小程序页面列表加载更多」就是在列表底部做分页加载,配合这条 SQL 的 LIMIT 条件实现。
3. 本地部署全流程:从建库到小程序跑通的六个实操步骤
3.1 环境版本对照与安装建议
SSM 项目对环境版本比较敏感,尤其是 JDK 和 MySQL 的版本选错,后面会反复出问题。建议按下面这张表准备环境:
| 工具 | 推荐版本 | 注意事项 |
|---|---|---|
| JDK | 1.8 | SSM 项目几乎都是基于 JDK 8 写的,别用 17 或 21,坑多 |
| Maven | 3.6.x | 依赖管理工具,IDEA 自带的也行 |
| Tomcat | 8.5 或 9.0 | 部署后端 war 包或直接 IDEA 集成 |
| MySQL | 5.7 或 8.0 | 5.7 兼容性最好,8.0 需要改驱动和 URL |
| Node.js | 14 或 16 | 跑 Vue 管理端,高版本可能编译报错 |
| 微信开发者工具 | 最新稳定版 | 导入小程序项目用 |
这里最容易被忽略的是 JDK 版本。有些同学机器上装了 JDK 17,导入 SSM 项目后 Tomcat 启动直接报错,因为老项目的 ASM 字节码库对高版本 JDK 支持不好。我一般会单独保留一个 JDK 8 的安装目录,用 IDEA 的 Project Structure 单独指定到 JDK 8,避免影响其他项目。
3.2 初始化数据库的两种方式
后端跑起来之前,先把数据库建好。压缩包里应该带 .sql 脚本,用命令行导入即可:
mysql -uroot -p < sql/community_group.sql执行后会创建数据库和全部表,包括初始的团长账号和测试商品数据。如果你的机器上只装了 MySQL 8,导入前先确认脚本里有没有使用ENGINE=InnoDB DEFAULT CHARSET=utf8这类语句,MySQL 8 对字符集的处理和 5.7 略有不同,但大多数毕设项目的脚本是兼容的。
如果不想用命令行,用 Navicat 也可以:新建连接,右键数据库选择「运行 SQL 文件」,选中脚本执行即可。导入完成后用SHOW TABLES;确认至少能看到 t_user、t_group_buy、t_order 这几张核心表。
3.3 把 SSM 后端导入 IDEA 并修改数据源配置
用 IDEA 的 Open 功能直接选中项目根目录,等待 Maven 把依赖下载完。这里有个经验:首次导入时 Maven 下载依赖可能很慢,建议配置阿里云镜像源,不然等半个小时是常态。
数据源配置集中在 jdbc.properties 文件里:
# jdbc.properties jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/community_group?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=你的数据库密码如果是 MySQL 8.0,驱动类名要改成com.mysql.cj.jdbc.Driver,URL 里最好加上serverTimezone=Asia/Shanghai,否则连接时可能报时区错误。字符集参数characterEncoding=utf8决定了中文数据在存储和读取时不会变乱码,这行一定不能删。
修改完配置后,直接用 IDEA 配置 Tomcat,把项目的 war 包部署上去,启动后能看到控制台输出 Spring 容器初始化的日志,没有异常就说明后端起来了。
3.4 管理端 Vue 项目与 .bat 脚本的用法
后端就绪后,管理端是独立于后端的 Vue 工程。找到包含三个 .bat 文件的目录,打开命令行执行:
# 安装依赖,只需要跑一次 1-install.bat或者手动执行:
npm install依赖装完后执行开发模式启动:
npm run serve启动成功后管理端一般跑在http://localhost:9527上,浏览器打开就能看到登录页。管理端负责商品上架、团购活动配置、订单管理这些后台操作,它本身不直接连数据库,而是通过 HTTP 请求调用后端的接口。这就是前后端分离在毕设项目里的实际形态。
3-build.bat 是打包命令,执行后生成 dist 目录,里面的静态文件可以扔到 Nginx 或者直接交给导师演示。如果你只是想本地看效果,跑 2-run.bat 就够了,不需要构建。
3.5 微信小程序端配置 API 地址并编译
小程序端是整个系统的用户入口。打开微信开发者工具,导入小程序目录,关键的一步是修改接口地址配置:
// config.js const BASE_URL = 'http://localhost:8080/api'; module.exports = { BASE_URL: BASE_URL };把 BASE_URL 指到你本地后端服务的地址。在开发者工具的「详情」-「本地设置」里,勾选「不校验合法域名」,因为小程序正式上线要求所有请求域名必须备案,但本地开发调试时没有这个限制,不勾选的话所有请求都会被拦截。
修改完配置后,编译一次小程序,首页应该能看到团购商品列表。如果列表为空,打开开发者工具的 Network 面板,看一下请求有没有返回数据,以及 Console 里有没有报错信息。这里大概率的问题都出在 BASE_URL 写错或者端口不对。
3.6 一条 curl 命令验证前后端联通
小程序端还没调通之前,先用 curl 直接打后端接口,确认后端服务本身是好的:
curl http://localhost:8080/api/goods/list返回一段 JSON 数据就说明后端和数据库正常。这时候再去查小程序端的问题,如果 curl 正常但小程序请求失败,问题一定出在小程序端的配置或者微信开发者工具的拦截上,而不是后端的问题。
这整套联调顺序很重要:先确认数据库,再确认后端,然后确认管理端,最后才轮到小程序。每加一层就验证一次,出了问题能立刻缩小范围,而不是在五六个环节里瞎猜。
4. 核心业务源码解读:团购下单链路与状态机的实现
4.1 微信登录授权与用户表绑定
小程序端用户登录走的不是传统的用户名密码,而是微信的wx.login接口。小程序端把code传到后端,后端再用这个 code 到微信接口换 openid。openid 是微信用户的唯一标识,后端拿它去 t_user 表里查,查到就返回用户信息,查不到就自动注册一个新用户。
小程序端代码大致是:
// pages/login/login.js wx.login({ success: (res) => { wx.request({ url: BASE_URL + '/user/login', method: 'POST', data: { code: res.code }, success: (resp) => { const userInfo = resp.data.data; wx.setStorageSync('userInfo', userInfo); } }); } });后端 Controller 收到 code 后,调用微信接口换取 openid:
// UserController.java @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { String openid = wechatService.code2Session(dto.getCode()); User user = userMapper.selectByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname("微信用户" + System.currentTimeMillis()); user.setCreateTime(new Date()); userMapper.insert(user); } return Result.success(user); }登录逻辑的要点是:openid 是业务主键而不是自增 id,所有后续订单都关联 t_user 表的 id,但鉴权依赖 openid。这种设计的好处是用户换手机或者重新打开小程序,无需再次输入账号密码,微信生态下体验最顺畅。答辩时如果能说清楚 openid 和 user_id 的区别,老师会觉得你确实理解了这个业务场景。
4.2 团购活动上下架与库存扣减
管理端通过 Vue 页面维护团购活动,核心操作是商品上下架和库存调整。这块在 MyBatis 里的实现就是普通的 update 语句,但上下架状态字段的设计有讲究。常见的做法是 t_group_buy 表里加一个status字段:0 表示下架隐藏,1 表示上架中,2 表示活动已结束。
小程序首页的商品列表查询语句:
SELECT id, goods_name, price, stock, end_time FROM t_group_buy WHERE status = 1 AND end_time > NOW() ORDER BY start_time DESC这里的筛选条件是status = 1 AND end_time > NOW(),前者过滤下架商品,后者过滤已过期活动。一个字段加一个时间条件就把「什么商品用户能看到」这个需求说清楚了。如果你想让列表按销量排序,可以再加一个sales_count字段,ORDER BY sales_count DESC,这就是二次开发时最简单的优化点。
4.3 订单状态机的流转实现
订单状态是整个项目中业务复杂度最高的部分。一般来说状态值这样定义:
| 状态值 | 含义 | 下一步动作 |
|---|---|---|
| 0 | 待支付 | 用户支付后变为 1 |
| 1 | 已支付待自提 | 团长核销后变为 2 |
| 2 | 已完成 | 不可变 |
| 3 | 已取消 | 不可变 |
状态机的关键点在于:状态只能按顺序正向流转,不能往回跳。待支付订单如果超过时限未支付,系统应该自动取消。实现方式可以是一个定时任务扫描超时订单,也可以在小程序端发起查询时「顺便」做一次状态修正。
后端的更新语句长这样:
UPDATE t_order SET status = 1, pay_time = NOW() WHERE id = #{orderId} AND status = 0注意 where 条件里带了status = 0,意思是只有当前状态是待支付的订单才能更新成已支付。这跟前面扣库存用stock >= #{count}是一个思路:在 SQL 层面上做并发控制,防止同一笔订单被重复支付或者重复发货。这种写法能应对大多数毕设答辩的追问——老师问「如果用户重复点击支付按钮怎么办」,你直接答「SQL 条件更新保证只有状态为 0 时才能变更」,基本就过关了。
4.4 后台管理端的权限拦截
管理端接口不能对所有人开放,否则任何小程序用户都能调后台接口下架商品。这里用的是 SpringMVC 拦截器,在进入 Controller 之前检查请求头里的 token。
// AuthInterceptor.java public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); if (StringUtils.isBlank(token) || !tokenService.isValid(token)) { response.setStatus(401); return false; } return true; } }拦截器注册到 SpringMVC 配置里,只拦截/api/admin/**路径,放行/api/goods/**这类用户端接口。管理端登录成功后,后端生成一个 token 返回给前端,前端每次请求都带上这个 token。这就是毕设里最常见的 token 鉴权方案。
这里有一个容易踩坑的地方:如果你的项目登录成功后 token 是存在服务端内存里的,重启后端服务用户就掉线了,需要重新登录。对毕设演示来说这不是 bug,但你要提前知道,别演示到一半把后端重启了发现管理端进不去,手忙脚乱。
5. 部署与演示避坑实录:五条值得提前踩的坑
5.1 微信开发者工具所有请求都被拦截
现象:小程序点击登录或获取列表时,Network 面板里所有请求都是红色的,提示request:fail,Console 报错说不在以下 request 合法域名列表中。
原因:微信开发者工具默认检查合法域名,本地开发用http://localhost:8080这种地址不在白名单里。
解决:在微信开发者工具的「详情」-「本地设置」里勾选「不校验合法域名」。这是一个只影响开发环境的开关,不影响正式上线。勾选后重新编译,请求就能正常发出。
5.2 MySQL 8 连不上数据库,报驱动类找不到
现象:Tomcat 启动时报ClassNotFoundException: com.mysql.jdbc.Driver,或者连接时提示Access denied for user。
原因:项目的 jdbc.properties 里驱动类名是com.mysql.jdbc.Driver,这是 MySQL 5.7 及之前的写法。MySQL 8.0 把驱动类改名成了com.mysql.cj.jdbc.Driver,而且 8.0 的驱动包需要单独下载。
解决:改jdbc.driver为com.mysql.cj.jdbc.Driver,URL 加serverTimezone=Asia/Shanghai,同时检查 pom.xml 里 mysql-connector-java 的依赖版本是否是 8.x。这三处对不上,连接成功是玄学。
5.3 前端管理页面中文全部变成问号
现象:Vue 管理端页面加载后,商品名称、按钮文字全都显示成???或者乱码。
原因:两个层面,一是 jdbc.url 没加characterEncoding=utf8,数据库读写中文时就乱;二是 IDEA 或命令行运行前端时,文件编码不是 UTF-8,Vue 文件里的中文在编译时被按其他编码解析了。
解决:把 jdbc.url 加上useUnicode=true&characterEncoding=utf8,再检查 IDEA 右下角的文件编码是不是 UTF-8。改完编码后要重启 Tomcat 和前端 dev server,只改不重启不生效。这件事我吃过亏,明明改了配置页面还是乱码,后来发现是 IDEA 的 Global Encoding 还是 GBK。
5.4 8080 端口被占用导致后端启动失败
现象:Tomcat 启动提示Port 8080 was already in use,后端一直起不来。
原因:本机有其他程序占了 8080,最常见的是其他 Java 进程、或其他开发工具的本地服务。
解决:打开命令行执行netstat -ano | findstr 8080,找到占用端口的 PID,然后在任务管理器里结束对应进程。如果想换个端口,改 Tomcat 的 server.xml 里的 Connector port,同时把小程序端 config 里的 BASE_URL 端口一起改掉。这里注意,只改一边就会变成「后端起来了但小程序永远连不上」。
5.5 真机预览连不上本地后端,PC 端却是好的
现象:微信开发者工具的「真机调试」模式一切正常,但用手机扫码预览时,页面请求全部失败。
原因:手机的 localhost 指的是手机自己,不是你的电脑。BASE_URL 里写localhost:8080,在手机端就成了访问手机自身的 8080 端口,当然连不上。
解决:把 BASE_URL 改成你电脑的局域网 IP,比如http://192.168.1.101:8080/api,同时保证手机和电脑在同一个 WiFi 下。另外 Windows 防火墙可能拦截 8080 端口的入站请求,需要在「防火墙高级设置」里放行该端口,或者在弹窗提示时点允许访问。这一步经常被忽略,改完 IP 还是连不上,多半是防火墙的问题。
6. 二次开发与答辩前的验证技巧:把 97 分项目做出自己的标签
6.1 给项目加一个「我的订单」聚合统计
很多人在答辩时被问「你这个项目有什么是你自己设计的地方」,这时候最稳的回答是拿出一个你亲手加的模块。一个成本低但效果好的思路是给小程序端的「个人中心」加一个订单状态统计:待支付、待自提、已完成、已取消,每个状态显示一个数字角标。这个功能数据上完全可以从 t_order 表里按 user_id 和 status 分组查出来:
SELECT status, COUNT(*) AS count FROM t_order WHERE user_id = #{userId} GROUP BY status返回一个数组,前端四个状态各显示一个数字,UI 上瞬间就比普通毕设丰富。后端加一个orderMapper.selectOrderCountByUser接口,小程序页面在 onShow 里调用一次,逻辑简单,不太可能出错,而且这是一个完整的前后端联动功能,答辩完全拿得出手。
6.2 答辩演示前强制走一遍六步验证法
正式答辩前,我建议你从头到尾走一遍下面这条链路,每一步都不能跳:
- 重启 MySQL 和后端服务,确认控制台无异常日志;
- curl 调一下用户接口,确认返回 JSON;
- 管理端登录,新建一个测试团购活动并上架;
- 小程序端首页确认能看到新上架的商品;
- 完整走一遍下单流程:选商品 → 提交订单 → 模拟支付 → 查看订单状态变化;
- 管理端把订单状态改为已完成。
这条链路加起来不到十分钟,但能暴露至少 80% 的演示翻车点。我在帮别人做答辩预演时遇到过最典型的例子:数据库里的测试订单已经堆了几十条,新下了一单排在几十名开外,演示时老师看不到新订单你以为出 bug 了。所以演示前最好清空业务表数据,让系统回到一个干净状态——你可以把 t_order 表DELETE FROM t_order WHERE 1=1,再重置自增 id,这样演示时每一条数据都是你现场造出来的,讲起来逻辑才顺。
还有一个习惯我从那以后一直保留:每次改完配置或代码,强制重启一遍后端并重跑这条链路。很多问题不是改出来的,是「没重启」或者「缓存没清」造成的假 bug,走一遍全链路就能分辨到底是真的坏了还是环境残留。希望帮到你。
本文还有配套的精品资源,点击获取