简介:面向Java初学者与课程设计选题的基于Java Web图书管理系统源码,采用MVC模式(JSP+Servlet+JavaBean)与B/S架构,覆盖普通用户注册登录、借书还书、个人信息修改以及管理员用户管理、图书信息维护、密码修改等双角色核心功能,系统含4张数据库表、约23张JSP页面,并已实现分页与代码注释,适配JDK1.8及MySQL5.5/5.7/8.0常见环境。资源包共157个文件,除Java源码、JSP页面、class字节码、SQL建表脚本和jar依赖包外,还包含2段操作演示视频与docx课程报告、项目结构文档,压缩包整体约65.85MB,可直接导入Eclipse或MyEclipse运行。已有8130人学习下载,是较受欢迎的双角色管理类实战项目。通过完整源码、数据库脚本与配套报告,读者可系统掌握JSP+Servlet+JDBC开发流程、Bootstrap响应式页面布局、MySQL数据表设计及Tomcat部署方法,便于快速理解MVC分层思想并用于课程设计参考或项目实训。
1. 图书管理系统,Java Web入门绕不开的那个完整项目
“基于Java web的图书管理系统”在课程设计和毕业设计里属于出场率最高的题目之一。它表面上是增删改查,实际上把Java Web的主干全部串起来了:请求怎么进、Filter怎么拦、Servlet怎么转发、JSP怎么渲染、MySQL怎么落库,任何一个环节没吃透,答辩或者自测时都会露馅。这篇文章不打算给你一个“能跑就行”的版本,而是把选型、建表、核心代码、部署排障整条链路讲清楚。适合正在做课设或毕设的人,也适合刚入行想练Web基本功的初级开发。系统不大,但值得认真做一遍。
2. 技术选型与项目骨架:Servlet/JSP、SSM、Spring Boot怎么选
同一个图书管理系统,业界有三种常见做法:纯Servlet+JSP、SSM、Spring Boot。选哪条路,取决于你做这个项目的目的是什么,而不是哪个技术听起来更高级。先把这个想清楚,后面所有代码结构都会跟着变。
2.1 三条路线的取舍:先看你的目标
如果老师指定了JSP,那Servlet+JSP就是唯一解。大多数课程设计都会限定“Java web”,这个标签下最常见的教学组合就是Servlet 3.0 + JSP + MySQL + Tomcat,项目打包成一个war包扔进Tomcat的webapps目录就能跑。它的优点是框架帮藏起来的东西全部暴露在你面前:HTTP协议、请求转发、Session管理、JDBC连接,全靠手写,练的是Java基础和Web原理。
SSM(Spring + Spring MVC + MyBatis)是前些年企业项目的标配,做毕设选这个方向的人也不少。它比Servlet+JSP多了一层Spring容器管理和MyBatis的SQL映射,写起来确实更省事,但学习曲线陡。如果你之前没接触过Spring,直接上SSM容易陷入“配置能跑但不知道谁调谁”的状态,答辩时被问一句“Spring MVC是怎么把请求分发给Controller的”就可能卡住。
Spring Boot是现在就业市场的绝对主流,也是自学者最喜欢的方向。它内嵌Tomcat、自动装配、约定大于配置,用Spring Boot写图书管理系统,代码量能比纯Servlet少一半还多。但要提醒一点:Spring Boot帮你封住了很多东西,一旦出现500错误,你如果连Servlet、Filter、连接池这些底层概念都没摸过,排查起来会非常痛苦。
我的建议是,如果是第一次做,老老实实选Servlet+JSP。如果你已经做过一次Servlet+JSP版本,再花一周用Spring Boot重写一遍,对比两个版本在处理请求、配置数据源、部署方式上的差异,这个过程中获得的认知比任何教程都值钱。
| 路线 | 适合场景 | 上手成本 | 核心考察点 |
|---|---|---|---|
| Servlet+JSP | 课设、理解Web原理 | 中 | HTTP请求、Filter、JSP九大内置对象、JDBC |
| SSM | 老项目风格毕设 | 较高 | Spring IoC、Spring MVC、MyBatis映射 |
| Spring Boot | 就业方向、快速开发 | 中低 | 自动配置、内嵌Tomcat、REST风格接口 |
2.2 推荐组合与Maven骨架:pom.xml先立住
选定Servlet+JSP路线后,项目要先用Maven搭起来。注意一点,Maven不是可有可无的加分项,而是必备工具,管理Servlet API、JSTL、MySQL驱动都要靠它。下面这个pom.xml是跑通图书管理系统的最小依赖集,可以直接抄。
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>book-manager</artifactId> <packaging>war</packaging> <version>1.0.0</version> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies> </project>这里有个参数值得专门说明:javax.servlet-api的scope是provided,意思是编译时用,打包时排除,因为Tomcat容器自己带了Servlet实现,如果你把这个依赖打进了war包,启动时经常出现类重复或版本冲突。mysql-connector-java用8.0.x,MySQL 5.7和8.0都能连,别选老的5.1.x,处理时区问题时不够干净。jstl 1.2是JSP页面里配合EL表达式做循环和条件判断用的,如果不用JSP而是纯粹的Servlet输出,可以不引入。
2.3 目录结构约定:源码放哪、配置放哪
Maven的web项目有固定约定,src/main/java放Java源码,src/main/resources放配置文件,src/main/webapp放JSP、CSS、JS、图片和WEB-INF。如果连这个结构都不按规矩来,后面打war包一定会走弯路。
book-manager/ ├── pom.xml ├── src/main/java/com/example/book/ │ ├── entity/ # 和表结构对应的类:Book, User, BorrowRecord │ ├── dao/ # 数据访问层,只写SQL和JDBC操作 │ ├── servlet/ # 控制器层,接收请求、调dao、转发JSP │ ├── filter/ # 登录拦截、编码设置 │ └── util/ # DBUtil 数据库连接工具、分页工具类 ├── src/main/resources/ │ └── db.properties # 数据库连接配置 └── src/main/webapp/ ├── WEB-INF/web.xml ├── login.jsp ├── book_list.jsp ├── book_add.jsp └── static/ # css、js、图片分包规则有两条硬性要求:entity里的类必须和数据库表字段一一对应,不要一个类里又塞业务逻辑又塞数据库字段;dao只写SQL和JDBC操作,servlet只做参数接收、调用dao、跳转页面,业务判断放在servlet里或单独写service包。千万别把大段Java代码嵌在JSP的<% %>脚本块里,页面会变得非常难维护,这也是答辩时老师最爱扣分的地方。
3. 数据库先行:图书、用户、借阅三张表的设计与SQL
写代码之前先把表设计好,这是一线开发的基本习惯。图书管理系统只需要三张核心表:图书表、用户表、借阅记录表。角色不单独建表,用户表里加一个role字段就够用。别一上来就设计权限表、角色表、菜单表,课设阶段做那么重,最后填数据都填得你怀疑人生。
3.1 表结构设计:三张表够用,别急着上权限表
book表用来存图书基本信息,字段包括id、isbn、书名、作者、出版社、分类、库存、状态、创建时间。status字段表示图书当前是否可借,0可借、1借出,这个字段的意义是让列表页和借阅操作能快速过滤,而不是每次都要现算。
user表存系统用户,username和password是登录凭证,role区分管理员和普通读者。这里要明白一个设计原则:系统里的“人”只有两类操作权限,管理员能增删改图书、查看所有借阅记录,读者只能查书、借书、还书、看自己的记录。用一个tinyint的role字段存0或1就够了,不需要复杂的权限框架。
borrow_record表是核心,每次借书插入一条记录,还书时更新return_time和status。三张表之间的关联关系是:借阅记录通过user_id关联user表,通过book_id关联book表,这是典型的一对多关系,在Java里体现为BorrowRecord类持有userId和bookId两个字段,查询时用JOIN把书名和用户名带出来。
3.2 建表SQL与初始化数据:直接可跑的版本
CREATE DATABASE IF NOT EXISTS book_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE book_db; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE COMMENT 'ISBN编号', name VARCHAR(100) NOT NULL COMMENT '书名', author VARCHAR(50) NOT NULL COMMENT '作者', publisher VARCHAR(100) COMMENT '出版社', category VARCHAR(30) COMMENT '分类', price DECIMAL(10,2) DEFAULT 0.00 COMMENT '定价', stock INT NOT NULL DEFAULT 1 COMMENT '总库存', status TINYINT NOT NULL DEFAULT 0 COMMENT '0可借 1全部借出', delete_flag TINYINT NOT NULL DEFAULT 0 COMMENT '0正常 1逻辑删除', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_name (name), KEY idx_category (category) ) ENGINE=InnoDB; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT '存MD5值', real_name VARCHAR(50) NOT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT '0管理员 1读者', status TINYINT NOT NULL DEFAULT 0 COMMENT '0正常 1禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT '读者id', book_id INT NOT NULL COMMENT '图书id', borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '借出时间', due_time DATETIME NOT NULL COMMENT '应还时间', return_time DATETIME NULL COMMENT '实际归还时间', status TINYINT NOT NULL DEFAULT 0 COMMENT '0借出中 1已归还 2逾期', KEY idx_user_status (user_id, status), KEY idx_book_id (book_id), CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINE=InnoDB; INSERT INTO user (username, password, real_name, role) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '管理员', 0), ('zhangsan', 'e10adc3949ba59abbe56e057f20f883e', '张三', 1); INSERT INTO book (isbn, name, author, publisher, category, price, stock) VALUES ('9787111111111', 'Java核心技术卷I', 'Cay S. Horstmann', '机械工业出版社', '编程', 89.00, 5), ('9787112222222', '深入理解Java虚拟机', '周志明', '机械工业出版社', '编程', 99.00, 3);初始化数据里密码那一串是123456的MD5值,这是最朴素的加密做法,千万别把明文密码直接存进数据库,答辩时老师看到明文密码会直接扣分,而且数据库一旦泄露,用户密码就全暴露了。
3.3 字段与索引的取舍:为什么状态用int不用varchar
图书状态、用户角色、借阅记录状态这三个字段,我都用了tinyint而不是varchar。很多人习惯写status='available'这种可读字符串,但维护时会发现,不同页面里'available'和'y'、'Y'、'yes'混着用,迟早翻车。数值枚举在Java里用常量类定义好,比如public static final int BOOK_STATUS_AVAILABLE = 0;,写起来同样清晰,排序和统计还更快。
索引设计按查询习惯来:isbn必查且要唯一,加UNIQUE索引;书名和分类经常作为搜索条件,建普通索引;借阅记录最频繁的查询是“某用户有什么书没还”,所以建(user_id, status)的联合索引,这个索引同时覆盖用户维度和状态维度。外键约束要保留,它保护的是数据完整性,防止借阅记录关联到不存在的用户或图书。表引擎必须用InnoDB,只有InnoDB支持事务和外键,MyISAM在并发写和崩溃恢复上都不太行。
4. 核心功能实现:登录拦截、分页查询、借阅归还的代码写法
这个系统的功能清单往下拆,最核心的就是三块:用户登录、图书列表分页、借书还书流程。这三块写完,整个系统的骨架就立住了。下面把每一个的代码写法和参数边界讲清楚。
4.1 登录拦截:用Filter守住所有受保护页面
图书管理系统的页面除了登录页,其余都要求登录后才能访问。用Filter实现这个拦截是所有方案里最干净的,因为拦截逻辑集中在一处,而不是在每个Servlet里重复判断Session。
@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; request.setCharacterEncoding("UTF-8"); String uri = request.getRequestURI(); String contextPath = request.getContextPath(); String path = uri.substring(contextPath.length()); // 放行登录页、登录接口、静态资源 if (path.equals("/login.jsp") || path.equals("/login") || path.startsWith("/static/")) { chain.doFilter(request, response); return; } // 校验Session中是否有用户信息 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }这段代码里有两个关键判断。第一,session == null || session.getAttribute("user") == null要同时判断,很多人只判断getAttribute,结果在Session过期后取到null,页面虽然跳回了登录页,但抛了NullPointerException。第二,静态资源路径必须显式放行,否则CSS和JS文件会被拦下来,登录页打开后光秃秃的没有样式,看起来像页面坏了一样。
放行的路径写的是/static/前缀,所以项目里所有CSS、JS、图片必须放在webapp/static目录下,这是用目录结构换拦截逻辑简单。登录成功时,Servlet里要执行session.setAttribute("user", user),建议存整个User对象,而不是只存一个用户名,这样JSP页面里可以直接取${user.realName}显示“欢迎你,管理员”,不用再查一次数据库。
4.2 分页查询:手写LIMIT与总页数计算
图书列表不能把全部数据一次性查出来,数据量上来后页面会越来越卡。分页是必修课,MySQL的做法是LIMIT加偏移量,Java这边分页参数从页面传过来。
public class BookDao { private static final int DEFAULT_PAGE_SIZE = 10; public List<Book> findPage(int page, int pageSize) throws SQLException { String sql = "SELECT id, isbn, name, author, publisher, category, price, stock, status " + "FROM book WHERE delete_flag = 0 " + "ORDER BY id DESC LIMIT ?, ?"; int offset = (page - 1) * pageSize; // 使用DBUtil获取连接并执行预编译语句,参数为offset和pageSize return DBUtil.queryList(sql, Book.class, offset, pageSize); } public int count() throws SQLException { String sql = "SELECT COUNT(*) FROM book WHERE delete_flag = 0"; return DBUtil.queryInt(sql); } }页面上把页码和每页大小作为请求参数传给Servlet,Servlet算出offset再传给dao,这是一个标准的得分点。写这段代码时容易忽略一个数学细节:MySQL的LIMIT从0开始计数,而页面显示的页码从1开始,所以查询第1页时offset必须是0,公式是(page - 1) * pageSize。如果直接写page * pageSize,第一页会跳掉前几条数据。
每页大小不要写死在代码里,让页面通过请求参数传pageSize,后台设一个默认值兜底。还有一处容易忽略:分页之后,搜索关键词条件要一起带进SQL,否则你搜索“Java”后翻到第二页,结果又变成全表数据了。常见做法是搜索时把keyword参数拼到where条件里,同时COUNT查询也要加同样的where条件,两个SQL必须保持一致。
4.3 借阅与归还:事务边界比SQL更重要
借书操作远不止“往borrow_record插一条记录”这么简单。它涉及四项数据变更:检查图书是否可借、检查读者是否达到借阅上限、修改库存和状态、插入借阅记录。这四步任何一步失败,前面已经做的变更都要撤销,否则会出现库存减了但借阅记录没生成的问题,这种数据不一致在图书管理里是灾难。
public boolean borrowBook(int userId, int bookId) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 BookDao bookDao = new BookDao(); Book book = bookDao.selectById(conn, bookId); if (book == null || book.getStock() <= 0) { return false; } int unreturnedCount = recordDao.countUnreturnedByUser(conn, userId); if (unreturnedCount >= 5) { return false; // 最多同时借5本 } recordDao.insertBorrowRecord(conn, userId, bookId); bookDao.decreaseStock(conn, bookId); // 执行UPDATE ... SET stock = stock - 1 WHERE id = ? conn.commit(); return true; } catch (Exception e) { if (conn != null) { conn.rollback(); // 任何一步异常,全部回滚 } return false; } finally { DBUtil.close(conn); } }这里的核心是setAutoCommit(false)和commit、rollback三件套。观察一下方法的写法,所有数据库操作都显式传了conn参数,没有在dao内部自己getConnection按个分别关闭连接。原因很简单:如果dao各自拿连接,事务控制就失效了,因为每个连接独立提交。传conn参数意味着所有SQL在同一个数据库连接上执行,事务才真正生效。
借阅上限5本这个参数,我没有写死成5,而是建议放到常量类里,或者在数据库字段上做成一个配置。归还逻辑和借阅相反:更新borrow_record的return_time和status,把书的库存加回来。归还要不要开事务?我一般也开着,因为更新记录和加库存是两件事。判断是否逾期,在查询时用一条SQL就够了,判断条件不是status,而是return_time IS NULL AND due_time < NOW(),这种字段当前值推导出的状态,不落库反而不会出现数据不一致。
5. 常见问题排查与避坑:部署运行中的5个高频雷区
这部分内容全部来自真实发生的排障经历。每一个问题都按“现象-原因-解决”来写,你遇到一个查一个,基本都能就地解决。
5.1 中文乱码:页面和数据库两头都要设
现象:列表页和输入框里输入中文后,页面显示一串问号或者乱码;往MySQL里存的中文,重启后还能读出来但界面显示异常。
原因:乱码从来不是单一原因。常见有两种:JSP页面没有声明UTF-8编码,或者请求参数在进入Servlet时被按ISO-8859-1解码了;另外数据库连接串没带characterEncoding,导致JDBC传给MySQL的字节流和MySQL客户端连接用的字符集不一致。
解决:三处必须同时检查。第一,JSP文件第一行确认是<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>,并且保证保存文件时编辑器也用的UTF-8编码。第二,web.xml里配置CharacterEncodingFilter,强制所有请求按UTF-8解析,或者复用前面登录Filter里的request.setCharacterEncoding("UTF-8"),注意这句必须放在读取任何参数之前执行。第三,JDBC连接串加useUnicode=true&characterEncoding=utf8,否则无论前后端多正确,入库时就乱了一半。
5.2 Tomcat 10下的javax.servlet包冲突
现象:Maven编译一切正常,代码里using javax.servlet.*,扔到Tomcat后启动报ClassNotFoundException,具体类名是javax.servlet.Filter或者jakarta.servlet.Filter其中之一。
原因:Tomcat 10是个分水岭。Tomcat 9及之前用的是javax.servlet命名空间,Tomcat 10起迁移到了jakarta.servlet命名空间。如果你用的是2024年后的IDEA自带Tomcat 10,但pom里还依赖旧版的javax.servlet-api,运行时类找不到是很正常的。
解决:要么把Tomcat换回9.x,这种方式最省事,对学习期的项目没有影响;要么升级依赖坐标改成jakarta.servlet-api 5.0以上版本,然后把代码里所有javax.servlet的import改成jakarta.servlet。注意这个改动会影响所有Servlet、Filter、Listener类,建议一次改完再编译,别混着用。
5.3 数据库连接报错:时区导致启动即失败
现象:启动后第一次请求报SQLException,提示信息里包含The server time zone value '�й���ʱ��' is unrecognized,或者Cannot create PoolableConnectionFactory。
原因:MySQL 8.0的驱动要求明确指定服务器时区,否则它会去读系统时区,在中文操作系统上时区格式带中文,驱动解析不出来直接放弃连接。
解决:数据库连接串里加时区参数。完整写法是jdbc:mysql://localhost:3306/book_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。不用Asia/Shanghai的话,也可以写serverTimezone=GMT%2B8表示东八区,但注意加号要URL编码成%2B,容易踩坑,建议直接用Asia/Shanghai更直白。
5.4 Filter顺序问题导致静态资源全部被拦
现象:登录跳转没问题,但登录成功进入主界面后,页面样式全丢了,按F12看到CSS请求全部返回302重新跳到login.jsp。
原因:日志里能看到的真相是Filter没有放行静态资源。初学者喜欢把Filter的@WebFilter配置成"/"并且拦截所有路径,同时对/css/style.css这类请求没做任何处理,静态资源请求也被当成需要登录的页面拦截了,浏览器拿到的不是CSS内容而是一个302跳转HTML。
解决:把项目所有静态资源放进webapp/static目录,Filter里显式放行path.startsWith("/static/")这个前缀。如果这样还不行,检查是不是多个Filter叠加了,比如你自己写了LoginFilter同时又配置了CharacterEncodingFilter,前一个放行了、后一个又给过滤掉,调整Filter的顺序也要在web.xml里显式配置filter-mapping。我一般只写一个Filter,把编码设置和登录拦截放在同一个文件里,少一个环节就少一处踩坑。
5.5 分页总页数少一页的整数除法陷阱
现象:数据库里11条记录,每页10条,列表底部只有1个页码,第二页永远点不进不去。
原因:Java里两个int直接相除,结果还是int,11 / 10得到1,小数部分被无情截断,所以总页数算成了1页。
解决:总页数计算有两种写法。一是先把被除数转成double再用Math.ceil向上取整:int totalPages = (int) Math.ceil(totalCount * 1.0 / pageSize);;二是用纯整数方式,加pageSize - 1再除:int totalPages = (totalCount + pageSize - 1) / pageSize;。两种结果一样,第二种还省了浮点转换开销。另外,分页SQL里LIMIT后两个参数类型是int,页面上传的页码一定要做数字校验,用Integer.parseInt加异常捕获,否则传个非数字进来直接500。
6. 部署上线与进阶:把war包扔进Tomcat,再想一步Token登录
6.1 用Maven打war包并部署到Tomcat
代码在IDEA里能跑,不代表部署环境也能跑。我见过太多人凭依赖IDE的“一键运行”躲过了很多问题,到了线上环境一脸懵。这里用最传统的方式走一遍:命令行打包,手动部署,全程不依赖IDE。
mvn clean package # 构建成功后 war包位于 target/book-manager-1.0.0.war cp target/book-manager-1.0.0.war /path/to/tomcat/webapps/ROOT.war cd /path/to/tomcat/bin ./startup.sh # 访问地址为 http://localhost:8080/把war包改名为ROOT.war再放进webapps,应用访问路径就不再带项目名前缀。如果不改名,就得访问http://localhost:8080/book-manager-1.0.0/,路径难看不说,还容易踩到页面里写死相对路径的坑。日志一定要看logs/catalina.out,启动失败时它最诚实,任何看不到报错就重启的行为都是浪费时间。
6.2 再往前走一步:Session换成Token
如果你的项目作为毕设的话,这里有一个能明显提高答辩分数的进阶方向:把Session登录改成Token模式。常见做法是登录成功后服务端生成一段UUID或者JWT返回给前端,前端存起来,之后每次请求都放到请求头里。Filter里校验通过就从缓存中取出用户信息放回请求上下文,校验失败直接返回401。
这个改动的意义不在代码量,而是让你理解无状态认证的动机:Servlet默认的Session是保存在服务器的,多台机器部署时Session同步会变成包袱,而Token放到请求头里,服务器不保存状态,天然能水平扩展。给答辩老师讲清楚这件事,比多写十个CRUD接口都管用。
我最早做这个系统时,把所有业务逻辑塞进JSP的脚本块,页面里满是<%= %>,功能倒是全的,可是每改一个字段要翻三个页面才找到代码。后来按MVC重写了一遍,才明白分层不是老师的规定动作,而是给自己留的后悔药。系统小,正适合把每一层都吃透。希望帮到你。
本文还有配套的精品资源,点击获取