ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue校园二手书与笔记共享平台开发实战与避坑指南

SpringBoot+Vue校园二手书与笔记共享平台开发实战与避坑指南 简介基于SpringBoot与Vue的校园二手书循环与知识笔记共享平台是一套高质量毕业设计项目资源面向计算机相关专业正在准备毕设、课设或期末大作业的学生。项目经导师指导并在评审中获得99分代码完整、可运行覆盖二手书发布、浏览、交易循环以及知识笔记分享等核心模块技术栈涉及SpringBoot、Vue、MyBatis等。压缩包共包含2000个文件总大小55.92MB其中1342个JS、192个CSS为前端交互与样式资源246个Java文件负责后端业务逻辑166个MD文档提供开发说明46个JSON与2个XML用于配置管理6个SQL脚本可直接初始化数据库。已有52人学习浏览过该资源。通过这套资料可以获得完整项目源码、数据库设计脚本、项目说明文档及部署指引既能辅助理解前后端分离开发思路也能作为毕业设计、课程设计的高分范本同时适合Java全栈入门者动手实战。1. 这个SpringBootVue校园二手书与笔记共享平台解决的不只是毕设每年到毕业季宿舍楼下和楼道里堆着成堆的二手教材和专业书书的主人嫌搬走麻烦学弟学妹又不知道该去哪找靠谱的旧书。这套基于SpringBootVue的校园二手书循环与知识笔记共享平台做的就是把这个线下场景搬到线上用户注册登录后可以发布自己的闲置图书按学科和关键词搜索别人的笔记下单买书的流转过程也按真实交易那样拆成待付款、待发货、待收货、已完成几个状态。后端SpringBoot提供接口前端Vue做页面交互MySQL存数据。适合两类人一是需要一份能跑通、能答辩讲清楚的Java毕业设计源码的同学二是想弄懂前后端分离项目里接口如何设计、数据如何流转、部署有哪些隐藏坑的实习生。下面按真实开发顺序拆解。2. 技术栈全景与模块拆解为什么这套组合适合校园场景2.1 后端SpringBoot为骨架三层结构与核心依赖这个项目后端用的是SpringBoot 2.7.x搭配MyBatis-Plus 3.5.x和MySQL 8.0。选这套组合不是图新是图稳SpringBoot的自动配置省掉了SpringMVC和Tomcat那一堆XML配置MyBatis-Plus让单表CRUD不用手写SQL校园级数据量用MySQL完全够。如果你在别处见过这个项目有其他版本大概率也是在这个技术栈上做增减核心骨架不变。我拿到手一般先看pom.xml依赖大致是这几类spring-boot-starter-web提供Web能力、mybatis-plus-boot-starter负责数据访问、mysql-connector-java 8.x版本对应数据库驱动、jjwt 0.9.1做登录token的生成与解析、lombok简化实体类的getter/setter。这里注意一个点jjwt 0.9.1对Java 8兼容最好SpringBoot版本也不要一上来就选3.x第三章和第五章会专门说版本问题。后端代码分Controller、Service、Mapper三层。Controller用RestController加RequestMapping(/api/...)暴露接口只负责收参数和返回结果Service层写业务逻辑比如下单时校验图书状态、锁定库存Mapper层用MyBatis-Plus的BaseMapper接口完成SQL操作。所有接口统一返回一个Result对象结构是code、message、data三个字段前端只需要解析这一种结构不用每个接口单独适配字段格式。package com.bookshare.common; public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }逻辑说明code字段约定200为成功、500为业务失败、401为未登录前端axios拦截器拿到非200会统一弹提示。这里写两个静态工厂方法一个包装成功数据一个包装错误信息业务代码里直接Result.ok(...)返回不用每次new对象。2.2 前端Vue SPA与Element UI的页面组织方式前端是Vue 2.x加Element UI组件库用Vue Router做路由Axios做接口请求。项目里的src目录按views、components、api、router四块拆开views放页面级组件比如图书广场、图书详情、订单列表、笔记分享页components放复用组件api目录把请求按书、订单、笔记、用户拆成独立模块router里统一配路由表。我习惯把api目录单独拆成四个文件user.js、book.js、order.js、note.js每个文件里导出一个函数对应后端一个接口。这样做的好处是页面里不直接写URL和请求方式后端接口地址变了只改api目录页面代码不用动。axios实例在src/api/request.js里统一创建设置baseURL为/api再挂一个请求拦截器。// src/api/request.js import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) export default request逻辑说明baseURL用/api而不是完整地址开发环境靠vue.config.js的proxy转发到后端部署后前端打包进SpringBoot的static目录/api路径由后端自己接收这样前后端联调阶段不用改代码。拦截器里把登录时存进localStorage的token塞进请求头后端JwtInterceptor从Authorization头里解析用户身份。2.3 数据库设计用户、图书、笔记、订单四张核心表数据库是业务的基础这个项目核心表就四张理解了它们就理解了业务主线。用户表user存账号密码和基本信息图书表book是二手书主体外键关联发布者笔记表note存共享的知识笔记订单表orders记录交易流转。排序规则一定要选utf8mb4_unicode_ci否则后面中文乱码问题会逼疯你。表名关键字段作用userid, username, password, nickname, avatar, phone, role登录身份与用户资料role区分普通用户和管理员bookid, seller_id, title, author, publisher, isbn, price, original_price, cover, status, create_time二手书主体status控制在售/锁定/已售noteid, user_id, title, content, subject, course_name, create_time知识笔记subject存学科分类用于筛选ordersid, book_id, buyer_id, seller_id, amount, status, create_time订单流转status对应不同交易阶段这里特别注意book表的status字段0在售、1锁定、2已售。用户下单后先把status改成1锁定这本书防止被其他人重复下单买家确认收货后改成2。列表页查询只捞status0的数据这样不会出现一本书同时在多个订单里挂着的情况。2.4 登录鉴权JWT在前后端分离下的落地方式前后端分离的场景下不能用传统Tomcat session做会话管理因为前端部署和后端接口可能不在同一个端口甚至不在同一台服务器session跨域拿不到。这个项目的做法是JWT用户登录成功后后端生成一个token字符串返回前端存到localStorage之后每个请求在Authorization头带上它。后端用拦截器统一校验通过后把userId放进request属性后续Service层直接拿。token生成和验证通常抽成一个JwtUtil工具类生成时把userId放进claims过期时间设7天。校园用户登录频率不高7天不用重登体验比较好。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 预检请求直接放行否则浏览器CORS预检会被拦截 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !JwtUtil.verify(token)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } request.setAttribute(userId, JwtUtil.getUserId(token)); return true; } }逻辑说明preHandle返回false就中断请求并返回401前端axios拦截器统一跳转登录页。注意开头对OPTIONS请求的放行浏览器发起带Authorization头的跨域请求时会先发一个预检请求如果拦截器直接拦了前端log里就会看到CORS报错实际是预检请求没通过。3. 从零跑起来环境准备与项目启动3.1 环境清单与版本匹配拿到源码先把环境对齐版本不对后面全是幺蛾子。我踩过的版本组合是JDK 1.8不要用Java 17SpringBoot 2.7虽然支持但部分依赖会有兼容警告、Maven 3.6.3、Node 16.20.2Node 18以上偶尔会遇到node-sass编译失败、MySQL 8.0、IDEA 2022或2023版本。前端如果用的是Vue2加Element UINode版本锁16最稳npm install时候报错少很多。环境推荐版本备注JDK1.8兼容性最好避免升级后lombok报错Maven3.6.3阿里云镜像配置后拉依赖稳定Node.js16.20.2Vue2项目npm install不报错MySQL8.0.x5.7也能跑注意8.0连接串要加时区参数IDEA导入Maven项目后第一次拉依赖会比较慢建议在settings.xml里配阿里云镜像mirror的mirrorOf写central这样大部分依赖都能从国内仓库拉。3.2 数据库初始化建库、建表与初始数据项目里一般带一个sql文件用Navicat或者命令行执行就行。执行之前先手动建库注意字符集和排序规则要指定CREATE DATABASE bookshare DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;逻辑说明建库指定utf8mb4而不是直接执行sql文件是因为有些sql文件里的建表语句没写DEFAULT CHARSET数据库默认字符集会继承下去。utf8mb4和utf8的区别在于前者支持emoji和生僻字笔记内容里学生偶尔会贴表情符号选utf8mb4能存进去。排序规则utf8mb4_unicode_ci在中文场景下按拼音排序搜索体验好一点。初始化完成后进去检查几张表有没有成功创建admin账号有没有插入如果sql文件里没有管理员账号自己在user表插一条role为admin的数据后头就能用了。3.3 后端启动IDEA导入与SpringBoot配置后端跑起来的前提是application.yml里数据库连接串和你的环境一致。重点看三个配置端口号、数据库连接、文件上传路径。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/bookshare?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis-plus: configuration: map-underscore-to-camel-case: true upload: path: D:/bookshare-upload/参数说明连接串里的characterEncodingutf8mb4保证请求参数和写入数据是UTF-8useSSLfalse避免本地MySQL没有配置SSL证书导致连接警告serverTimezoneAsia/Shanghai是MySQL 8.0驱动的强制要求不写会报时区错误。上传路径upload.path配一个本地绝对路径Windows用D:/开头的路径Linux换成/usr/local/bookshare-upload/。password那里换成你自己的数据库密码。启动方式找到主类Application类上有SpringBootApplication注解的那个右键Run。控制台出现Tomcat started on port 8080就说明成功了。启动过程中如果报数据库连不上优先检查密码和端口。3.4 前端启动npm install与dev开发模式前端目录一般在项目下的frontend或vue目录进入目录先执行npm install装依赖。国内环境建议先把npm源切到镜像仓库不然装Element UI和vue-router这些包会卡半天。cd frontend npm config set registry https://registry.npmmirror.com npm install npm run dev安装成功后npm run dev启动开发服务器端口默认8081。此时打开http://localhost:8081如果页面能打开但接口报404大概率是代理没配。开发模式下前端页面跑在8081后端接口在8080两者端口不同浏览器会直接跨域报错。// vue.config.js module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }配置说明proxy的作用是把前端页面里发往/api开头的请求转发到后端8080端口浏览器看到的请求始终发到8081由devServer在中间做转发这样前端代码里就完全不用写后端地址避免跨域。changeOrigin设置为true是为了让后端拿到的Host头是localhost:8080有的后端代码里会校验域名来源。4. 核心模块实战二手书交易与笔记共享的关键代码4.1 图书发布与图片上传MultipartFile到本地磁盘图书发布是整个业务的入口。前端表单填书名、作者、ISBN、价格、成色描述封面图先单独调上传接口拿到URL再随表单一起提交。这样拆的好处是发布失败时图片已经传上去了也不会丢下次编辑还能用。RestController RequestMapping(/api) public class UploadController { Value(${upload.path}) private String uploadPath; PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) throws IOException { String oldName file.getOriginalFilename(); String ext oldName.substring(oldName.lastIndexOf(.)); // 只允许图片类型防止上传可执行文件 ListString allowTypes Arrays.asList(.jpg, .jpeg, .png, .gif); if (!allowTypes.contains(ext.toLowerCase())) { return Result.error(仅支持jpg、png、gif格式的图片); } String newName UUID.randomUUID().toString().replace(-, ) ext; File dir new File(uploadPath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, newName)); return Result.ok(/upload/ newName); } }逻辑说明文件名用UUID重命名是为了防止两个用户上传同名的图片互相覆盖UUID去横线后是32位字符串加上扩展名做新文件名。transferTo是Spring封装的方法底层把临时文件移动到目标位置。返回的路径是相对路径/upload/xxx.jpg前端存到数据库的book表cover字段页面里直接拼上接口域名就能访问。这里注意几个参数multipart的max-file-size在application.yml里配的是10MB超过会直接报错扩展名校验用白名单而不是黑名单黑名单永远堵不完。图书发布接口本身逻辑比较简单把表单数据set到Book实体sellerId从当前登录用户拿status默认0createTime用MyBatis-Plus的自动填充注解。发布的SQL用insertMyBatis-Plus的insert方法会自动把实体非null字段插入。4.2 订单流转从下单到确认收货的状态设计订单状态这个模块是整个项目最容易出逻辑漏洞的地方。常见的坑是用户买自己的书、一本书被两个人同时下单、订单取消后图书状态没有恢复。这个项目把状态设计成整数枚举0待付款、1待发货、2待收货、3已完成、4已取消。下单接口的核心逻辑是校验加锁定两步。Transactional public ResultVoid placeOrder(Long bookId, Long buyerId) { Book book bookMapper.selectById(bookId); if (book null || book.getStatus() ! 0) { return Result.error(图书不存在或已下架); } if (book.getSellerId().equals(buyerId)) { return Result.error(不能购买自己发布的图书); } // 锁定图书防止并发下重复订单 book.setStatus(1); bookMapper.updateById(book); Order order new Order(); order.setBookId(bookId); order.setBuyerId(buyerId); order.setSellerId(book.getSellerId()); order.setAmount(book.getPrice()); order.setStatus(0); orderMapper.insert(order); return Result.ok(); }参数说明Transactional注解让图书锁定和订单创建两个操作在同一个事务里中间任何一步抛异常两个操作一起回滚不会出现订单创建了图书状态没锁住的情况。选择图书时先selectById再判断status这一步存在并发窗口两个请求同时读到status0就可能都下单成功。严格一点的做法是用乐观锁给book表加version字段update时带上where version 旧值更新行数为0就说明被人抢先了。校园项目并发量不大先锁再查也能接受但答辩时能说出并发风险和改进方案是加分项。后续状态流转靠几个独立接口驱动买家点付款走payOrder把status从0改1卖家点发货走consignOrder把status改成2同时更新发货时间买家点确认收货走confirmOrder把status改成3图书status改成2。取消操作单独设计待付款状态取消后图书状态要恢复成0这个最容易漏掉写代码时一定要记得回滚库存。4.3 笔记检索分类筛选与关键词搜索的实现笔记共享模块的核心是检索体验。用户按学科分类点筛选或者输入关键词搜标题和正文。MyBatis-Plus的LambdaQueryWrapper可以链式拼接条件代码比写XML简洁不少。public ListNote search(String keyword, String subject) { LambdaQueryWrapperNote wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(subject)) { wrapper.eq(Note::getSubject, subject); } if (StringUtils.hasText(keyword)) { wrapper.and(w - w.like(Note::getTitle, keyword) .or().like(Note::getContent, keyword)); } wrapper.orderByDesc(Note::getCreateTime); return noteMapper.selectList(wrapper); }逻辑说明分类筛选用eq精确匹配subject字段存的是预设的学科枚举值前端下拉框选项和后端字段值要对齐。关键词搜索用like模糊匹配标题和正文只要有一个命中就返回。wrapper.and的作用是保证关键词条件被括号包起来否则SQL拼出来变成subject? AND title like ? OR content like ?优先级错了就搜出无关数据。orderByDesc按创建时间倒序最新笔记排前面。这套实现的好处是代码短、好读缺点是LIKE %keyword%不走索引全表扫描。校园笔记量几千条时性能无感知但如果以后要扩展成全校几万条笔记建议换MySQL全文索引或者接ElasticSearch答辩时候提到这个优化点就能看出你考虑过数据规模。5. 避坑指南从开发到部署的五个真问题5.1 Vue打包进SpringBoot后刷新404现象npm run build生成dist目录把static和index.html复制到SpringBoot的resources/static下启动后访问首页正常但点路由跳转到其他页面后按F5刷新浏览器直接404。原因vue-router默认使用history模式URL里没有#号例如/book/detail/123。浏览器刷新时会带上这个完整路径请求后端SpringBoot默认找不到对应的Controller于是返回404。开发模式下刷新没事是因为devServer内部帮你把不存在的路径回退到了index.html。解决两种方案任选。第一种最省事路由history模式改成hash模式URL变成/#/book/detail/123刷新时#后面部分不会传给服务器就不会404了。第二种是在后端加一个转发规则把非/api开头的路径都转到index.html。我一般选hash模式适合毕设这种不需要追求SEO的场景。const router new VueRouter({ mode: hash, // 改之前是history routes })参数说明mode改hash后URL多一个#大多数人不会注意功能不受影响。如果你的项目里已经有history模式且不想改就在SpringBoot的Controller里配置一个转发把请求转发到index.html注意只能匹配非/api的路径否则接口请求会被抢走。5.2 SpringBoot版本太高导致的启动失败现象按网上的教程新建SpringBoot项目默认创建的是3.x版本导入这个源码后启动报错提示Invalid value type for attribute factoryBeanObjectType或者mybatis-plus的mapper扫描不到。原因SpringBoot 3.0是分水岭底层把javax包全部换成jakarta命名空间MyBatis-Plus老版本的starter里还是javax直接冲突。另外SpringBoot 3.x要求JDK 17以上如果你的环境是JDK 8连编译都过不去。解决统一锁SpringBoot 2.7.18这是2.x系列最后一个版本持续维护过一段时间兼容性最好。改pom.xml的parent声明parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent参数说明改完parent版本后让Maven重新reimport同时检查dependencies里有没有显式声明其他Spring Boot相关依赖版本如果有就删掉版本号让它继承parent的依赖管理。mybatis-plus-boot-starter用3.5.3这是和SpringBoot 2.7兼容性经过大量验证的版本组合。5.3 图片上传后访问404现象上传接口返回成功数据库里存的路径也正确但浏览器直接打开图片地址显示404或者页面里图片裂开。原因文件确实传到了upload.path指定的磁盘目录但SpringBoot默认只把classpath:/static/目录映射为静态资源路径自定义的磁盘目录没有对应的HTTP映射。Tomcat收到/upload/xxx.jpg请求时去static目录里找找不到自然就404了。解决写一个WebMvcConfigurer的配置类把/upload/**的请求映射到磁盘路径。Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${upload.path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }参数说明addResourceHandler里的/upload/**是浏览器访问的URL模式addResourceLocations里的file:前缀不能丢它告诉SpringBoot这是一个文件系统路径而不是classpath路径。uploadPath末尾加不加斜杠都行但addResourceLocations拼接时如果缺了file:会出现路径解析成classpath的情况图片一样出不来。改完配置重启再访问图片地址就正常了。5.4 MySQL中文乱码与emoji存不进去现象接口请求和返回中文都正常但打开数据库表看数据是??或者中文正常但笔记内容里的emoji表情变成乱码。原因数据库连接串里没加characterEncoding参数或者建库时用的默认字符集是latin1。MySQL 8.0默认字符集虽然是utf8mb4但如果你是用旧的方式执行的建表SQLCREATE TABLE里没有DEFAULT CHARSET继承的就是库集latin1。emoji存不进去的另一个常见原因是列字符集是utf8而不是utf8mb4emoji本身不在utf8字符集范围内。解决三层都改。第一层建库时强制指定字符集用3.2节那条CREATE DATABASE语句第二层application.yml连接串加上characterEncodingutf8mb4第三层已经建好的表执行ALTER TABLE转换ALTER TABLE note CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;参数说明CONVERT TO会同时转换表里所有文本列和已有数据的字符集比单独ALTER COLUMN省事一张表一条命令搞定。执行成功后再检查连接串是否带了characterEncoding参数不带的话Java驱动默认用平台字符集Windows下可能又是GBK。5.5 前后端联调跨域配置缺项现象前端跑在localhost:8081后端在8080发送请求后浏览器控制台报CORS错误说Access-Control-Allow-Origin不存在但后端的addCorsMappings明明已经配置了。原因跨域配置里allowedMethods没有写全或者allowedHeaders里没有包含Authorization。浏览器发起带Authorization头的请求时会先发OPTIONS预检后端返回的响应头里如果没包含允许Authorization的声明浏览器直接判定跨域失败真正的POST请求根本没发出去。解决一个完整的跨域配置要同时覆盖来源、方法、请求头三项。常见配置如下Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8081) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(Authorization, Content-Type) .allowCredentials(true) .maxAge(3600); }参数说明allowedMethods里OPTIONS必须写预检请求本身就是OPTIONS方法不写的话预检直接失败。allowedHeaders里除了默认的Content-TypeAuthorization要显式加进去否则前端带token的请求全被挡。allowCredentials(true)允许携带cookie如果项目用的是JWT其实不需要cookie但加上无害。allowingOrigins如果前端用localhost加端口就要写全协议加域名加端口不要写*浏览器规定allowCredentials(true)时不能配*。6. 从毕设到作品代码走查与两个扩展方向6.1 上线前代码走查清单能跑和能答辩是两回事。跑通之后我一般会按这张清单把代码过一遍不花太长时间但能帮你挡住一半答辩时被问到的问题。检查点检查内容常见问题密码安全user表的password字段是否加密存储明文密码存库是最明显的安全硬伤权限控制图书删除、订单操作是否校验当前用户任何登录用户都能删别人的书就是漏洞事务处理涉及多表更新的方法是否有Transactional下单锁书与建订单不同步会出现脏数据异常兜底是否有RestControllerAdvice统一异常处理数据库异常直接堆栈抛给前端体验很差硬编码排查图片路径、状态值有没有写死在代码里状态用魔法数字还是枚举答辩时会被追问密码加密建议用BCryptSpring Security里自带BCryptPasswordEncoder或者用hutool的BCrypt工具类。注意加密后的密码字段长度要够varchar(255)BCrypt生成的字符串60位。删除图书的接口Service层先查book的sellerId和当前userId比对不一致直接返回无权操作。6.2 值得加的两个功能评论点赞与推荐这个项目交到手里是能跑的想要在开题和答辩时显得有工作量我会建议加两个扩展点。第一个是笔记评论点赞新增note_comment表id、note_id、user_id、content、create_time前端在笔记详情页加载该笔记的所有评论后端两个接口查询列表和发表评论。这个功能表结构简单、逻辑清晰两三天就能做完但完整覆盖了新增表到新增接口再到前端页面的全流程。第二个是图书推荐。二手书场景里有个天然的数据基础图书表有subject分类字段用户可以加一个preferred_subject偏好字段存到user表。推荐逻辑按用户偏好分类拉同分类下最近发布的在售图书做推荐列表。不需要算法一条SQL加一个set操作就能实现。这两个功能做完简历和答辩PPT里可以写“主导了带有评论互动与个性化推荐功能的校园二手书平台”项目从单薄变得有层次。说个我自己的习惯每次拿到一套陌生源码第一件事不是急着跑起来而是先看pom.xml和application.yml确认版本组合再翻一遍数据库sql文件看表结构最后才启动。前年有一回接一个别人留下来的老项目一上来就用自己的默认SpringBoot 3模板导入mybatis-plus怎么调都不对折腾一晚上最后发现是parent版本问题。从那以后我每次导入Java项目都强制先看parent版本号再动手。这种做法帮你少走很多弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表