SpringBoot+Vue+MyBatis+MySQL图书电商系统实战:从数据库设计到部署
发布时间:2026/9/15 21:26:47 作者:尧图编辑部 阅读量:1,286

1. 项目定位与技术选型为什么是SpringBootVueMyBatisMySQL1.1 这个系统到底解决什么问题先说结论这是一个典型的前后端分离全栈实战项目核心目标是做一个可以真正上线的图书电商网站。它覆盖了一条完整的业务链路——前台用户浏览图书、搜索、加入购物车、下单支付后台管理员维护图书信息、处理订单、管理分类和用户。你把这个项目啃透就等于把Java后端和Vue前端的常用技能点全部过了一遍。我在帮团队搭建这个项目的时候感受到最大的价值不是“能跑”而是它把电商系统里最经典的问题都串起来了商品的增删改查怎么做、购物车这种临时状态怎么存、订单和库存这种强一致性的数据怎么处理、权限怎么控制。这些场景不是面试题里那种孤立的知识点而是真正要落到代码里的细节。项目本身是源码级别的交付所以适合的人群很明确学过Java基础和前端基础、想找一个完整项目练手的人准备面试、需要拿项目经历撑场面的应届生以及想快速搭一套图书商城做二次开发的技术人员。尤其适合那种“看了一堆视频但没实际跑过完整项目”的状态这套源码能帮你把知识拼图补全。1.2 技术栈拆解这套组合拳为什么经典先聊后端。SpringBoot能成为Java开发的绝对主流核心在于它把“配置地狱”干掉了。传统SSM项目你要写一堆XML配置文件数据源、事务、扫描路径全都要手动声明。SpringBoot用自动配置和约定大于配置的方式把大部分样板代码直接省掉一个注解就能启动整个Web容器。省下来的时间你可以专心写业务逻辑。再聊前端。Vue这个框架的优势在于渐进式——项目简单的时候你可以在一个页面里引入CDN就开干项目复杂了再用Vue Router做路由、Vuex或Pinia做状态管理、Vite或Webpack做打包构建。图书电商这种页面多、交互多的场景Vue的组件化思维刚好匹配。商品卡片、搜索栏、轮播图、购物车每个都能抽成独立组件团队协作时互不干扰。然后是MyBatis这块。选它而不选JPA说穿了就一条理由SQL可控。图书商城的查询场景非常杂按书名模糊搜索、按分类过滤、按价格区间筛选、多表关联带出分类名和库存信息这些复杂查询用JPA的规范方法名去拼写起来很痛苦。MyBatis让你直接写SQL动态SQL又允许根据条件拼接查询语句性能怎么优化、索引怎么命中你心里都有数。最后是MySQL。这个选择基本不需要讨论互联网行业用MySQL的比例太高了文档多、社区活跃、招聘需求常年排在前列。搭配InnoDB存储引擎支持事务、行级锁、崩溃恢复对于图书电商这种需要处理订单和库存一致性的场景是足够可靠的。1.3 前后端分离架构的核心请求链路搞懂这个项目最关键的是理解数据是怎么流动的。整体架构是这样Vue客户端在浏览器运行通过HTTP请求调用后端接口接口统一走RESTful风格SpringBoot接收请求后先由Controller层做参数接收和响应封装然后调用Service层处理业务逻辑Service层再通过Mapper接口也就是MyBatis的映射器操作MySQL数据库数据以JSON格式返回给前端渲染。这个链路里有几个值得注意的设计点。第一接口返回结构要统一。我习惯封装一个Result对象里面包含code、message、data三个字段前端根据code判断请求是否成功这样比裸返回一个Map或者直接返回实体类规范得多。第二跨域问题必须提前处理。前端项目跑在8080端口、后端跑在8081端口浏览器会拦截跨域请求所以后端要配置CORS或者通过网关转发。第三前后端通过Swagger或接口文档约定字段名避免联调时你等前端、前端等你的尴尬。2. 数据库设计图书电商系统的核心表与索引实践2.1 核心表结构拆解一个图书电商系统的数据库说复杂也复杂说简单也就那几张核心表。我按业务模块归纳一下。第一块是商品模块。book表是核心字段大概包括id主键、book_name书名、author作者、isbn国际标准书号、category_id分类ID、price定价、discount_price折扣价、stock库存、cover封面图URL、description简介、publish_date出版日期、status状态上架/下架。category表存图书分类用parent_id字段支持多级分类比如“计算机”下面可以有“Java”“Python”等子类。第二块是用户模块。user表存账号密码、昵称、手机号、邮箱、头像、注册时间。密码一定要加密存储别用明文。我习惯用BCrypt加密Spring Security自带这个工具加盐处理安全性有保障。用户角色用role字段区分普通用户和管理员也可以单独拆一张role表做RBAC权限模型看你的项目规模。第三块是交易模块。cart购物车可以单独建表也可以用Redis做临时存储这个项目里为了降低复杂度直接建表关联user_id和book_id再加一个quantity数量字段。order订单表和order_item订单明细表是一对多关系order表存订单编号、用户ID、总金额、订单状态待支付/已支付/已发货/已完成/已取消、收货地址、创建时间order_item表存订单ID、图书ID、书名快照、单价、数量、小计。订单明细为什么要做数据快照因为商品价格和书名以后可能改但订单里的历史信息不能跟着变。2.2 索引设计从一张图书表看MySQL索引实践很多人建表不重视索引数据量小的时候感觉不到问题等到表里有个几十万条数据一个不带索引的模糊查询能直接把数据库拖垮。这本书城系统里索引设计我建议这样考虑。book表的查询场景主要集中在按分类查、按书名模糊查、按价格排序。那么首先book_name要建索引但这里有个细节——如果用户输入的关键字是在中间位置的模糊查询比如查询所有书名中包含“Java”的图书那直接用LIKE %Java%是没法命中索引的因为最左前缀原则失效了。更聪明的做法是引入搜索引擎ES或者用全文索引MySQL 5.7之后的ngram全文索引但作为这套教学项目直接在SQL层面做前缀模糊匹配LIKE Java%加上合理分页就够了用户的使用习惯也能接受。其次category_id和status组合起来建一个联合索引。因为首页最常用的场景是“查某个分类下已上架的所有图书”这个查询的WHERE条件是category_id ? AND status 1单查category_id命中索引后还要回表过滤status建联合索引category_id, status可以避免回表效率高一截。再配合排序字段如果是按价格排序可以再加一列price到联合索引里做成category_id, status, price这样索引覆盖了查询的WHERE、ORDER BY两个环节。order表呢user_id和create_time要建索引因为用户查询“我的订单”非常高频而且通常会按时间倒序排列。这个联合索引user_id, create_time是性价比极高的设计。2.3 存储过程能不用就别用但面试要会聊关于这个系统里要不要用存储过程我的观点很直接业务逻辑尽量写在Service层用代码来控制不要迷信存储过程。存储过程确实能把复杂的SQL逻辑封装在数据库端执行效率高但代价是维护成本飙升——你没法用IDE调试没法做单元测试版本管理也是难题。不过现在互联网公司面试还是喜欢问存储过程的原理和适用场景。我的理解是如果一段数据操作逻辑涉及多个表的读写、并且需要保证事务性比如下单时同时扣库存、生成订单、清除购物车与其在应用层写一大段代码不如把这几个SQL操作放进一个事务里执行用Spring的Transactional注解管理。存储过程真正有价值的场景是ETL数据清洗、批量报表计算这种纯数据密集型的任务电商交易场景直接走应用层事务就行。需要特别提醒的是存储过程里如果用到动态SQL拼接一定注意参数校验和防注入问题。用PreparedStatement的方式传参数别把用户输入直接拼进SQL字符串里这是底线。3. 后端实现SpringBoot整合MyBatis的关键细节3.1 项目分层与包结构设计拿到这套源码第一件事应该是看包结构一个好的分层决定了项目的可维护性。我推荐的分层方式是这样entity实体层存放数据库表的映射实体类字段和表结构一一对应mapper数据访问层定义接口方法Mapper.xml里写SQL语句service业务层写具体业务逻辑比如下单时验证库存、计算金额、生成订单号controller控制层接收前端请求、调用Service、返回给前端Result封装对象config配置类放跨域配置、拦截器、静态资源映射等common公共模块放统一的返回结果、异常处理类、工具类。这套分层的核心原则是Controller层不写业务逻辑只做参数接收和结果返回Service层不直接操作数据库只调用Mapper接口Mapper层只有SQL和数据映射不掺业务判断。每层各司其职代码出了问题定位起来非常快。3.2 Mapper层实战动态SQL与批量操作这个项目里最有技术含量的SQL都集中在图书的多条件查询上。前端搜索页可能有多个筛选项书名、分类、价格区间、是否上架。你不可能为每个组合写一条SQL所以要用MyBatis的动态标签。比如这个典型的多条件分页查询select idsearchBooks resultTypecom.example.entity.Book SELECT * FROM book where if testbookName ! null and bookName ! AND book_name LIKE CONCAT(%, #{bookName}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if AND status 1 /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select标签会自动处理AND前缀问题不需要你写WHERE 11这种丑陋的写法。 标签按条件拼接SQL参数为null或空字符串时自动跳过对应的过滤条件。这就是MyBatis相比JPA最大的优势——SQL完全在你的掌控中每一条都是白纸黑字性能瓶颈一目了然。还有一个容易被忽略的高频场景批量写操作。比如管理员批量上下架图书、批量调整库存。很多新手喜欢在循环里一条一条update这种做法性能极差正确姿势是用MyBatis的 标签拼批量更新update idbatchUpdateStatus UPDATE book SET status #{status}, update_time NOW() WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /update这就牵扯到MyBatis里#{}和${}的区别了。#{}是预处理参数占位符会生成PreparedStatement的?自动做类型转换和防注入${}是字符串直接拼接有SQL注入风险一般只在动态指定表名、列名时才用。一句话总结能用#{}坚决不用${}该用${}时必须手工校验参数。3.3 MyBatis缓存机制一级缓存与二级缓存MyBatis缓存经常被面试问到也是实际开发中容易踩坑的点。先理清概念一级缓存是SqlSession级别的默认开启且无法关闭。同一个SqlSession中执行两次相同的查询第二次会直接命中缓存不再查数据库。看起来很好但在Spring整合环境下每次Mapper操作都会新建和关闭SqlSession一级缓存的生命周期非常短实际作用没有想象中大。二级缓存是Mapper级别的可以跨SqlSession共享。开启方式是在Mapper.xml里加一行 配置默认的缓存实现是PerpetualCache底层就是一个HashMap。这里有个大坑如果开启二级缓存实体类必须实现Serializable接口否则反序列化时会报错。更麻烦的是一旦表数据被更新insert、update、delete默认会清空整个Mapper的缓存导致缓存命中率很低频繁读多写少的场景还可能读到脏数据。我的建议是对于教学项目二级缓存可以不开启或者只在city表、category表这种极少变更的基础数据上开启。图书库存、订单这些高频更新的数据宁可每次查数据库也不要走缓存等后面接Redis再做真正的缓存层。面试如果被问到能说清楚“开了一级缓存二级缓存没开是因为数据一致性风险太高”就是加分回答。还有一个冷门细节当MyBatis的查询参数是单个数字或字符时可能会走到类型判断的坑里建议传参时用Param注解指定参数名避免SQL里通过parameterType推断出错。3.4 事务与并发控制库存扣减的正确姿势做电商系统库存和订单这环最容易出安全事故。先说你最容易犯的错扣库存的时候先查库存再判断够不够然后update这个过程在并发场景下会产生超卖——两个人同时买到同一件商品的最后一件。正确的扣库存SQL是一条带条件的原子updateupdate iddeduceStock UPDATE book SET stock stock - #{quantity} WHERE id #{bookId} AND stock gt; #{quantity} /update执行这条SQL后判断影响行数如果返回1说明扣减成功返回0说明库存不足。这个操作不需要加锁也不会有超卖因为SQL语句本身是数据库原子执行的。当然更严格的方案是引入乐观锁就是加一个version字段update的时候带上version条件更新成功后version加1。关于事务我在Service层加了Transactional注解来保证下单操作的原子性。这个注解的原理是Spring AOP它会在方法执行前开启事务方法正常执行完提交抛异常则回滚。这里有个非常隐蔽的坑自调用事务失效。假设A方法是普通的类内部方法B方法加了Transactional你在A方法内部调用B方法B上的事务注解不生效。因为Spring AOP是基于动态代理实现的内部调用绕过代理对象直接调用了原始方法。解决办法是把B方法抽到另一个Service类里或者自己注入自身代理。4. 前端实现Vue电商界面的工程化落地4.1 Vue工程创建与环境配置前端部分的第一步是把开发环境搭好。Node.js是必装的装好之后npm就能用。我建议Node版本选LTS稳定版太新的版本有时候碰到老项目依赖不兼容会很难受。创建Vue项目的命令很简单npm create vitelatest book-mall-web -- --template vue cd book-mall-web npm install npm run devVite创建的项目启动速度快开发体验比Webpack好很多现在已经是Vue生态的默认选择。项目结构里src/router放路由配置src/store放状态管理src/api放接口请求封装src/views放页面组件src/components放公共组件。一般还要装几个依赖axios用于HTTP请求element-plus用于UI组件库pinia用于前端状态管理。这里有个经验之谈npm install的时候经常因为网络问题卡住可以换成淘宝镜像源命令是npm config set registry https://registry.npmmirror.com/。装依赖的时候如果报权限错误别用sudo强行解决把node_modules删掉重新install通常更靠谱。4.2 路由与状态管理前端“导航系统”怎么搭路由配置是整个前端骨架。图书商城典型的路由设计包括首页、图书列表页、图书详情页、购物车页、登录页、注册页、个人中心、后台管理布局嵌套子路由。图书详情页的路由需要动态传参推荐用带参数的路径定义const routes [ { path: /book/:id, name: BookDetail, component: BookDetail }, ]跳转的时候用query方式传参数也可以但刷新页面会丢参数。用路由参数的方式更可靠详情页里通过this.$route.params.id拿参数。在页面组件里跳转官方推荐命名路由this.$router.push({ name: BookDetail, params: { id: bookId } })还有一个容易被问到的问题路由守卫。图书商城的后台管理页面必须登录后才能访问用路由的全局前置守卫来拦截router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })登录成功后跳回原始页面这个体验细节很多项目都没做。状态管理我用的是Pinia它是Vuex的替代者API更简洁对TypeScript支持更好。购物车的状态图书列表、数量、总金额很适合放在Pinia里因为多个页面商品列表、详情页、购物车页都要读写这些数据用状态管理可以避免各个组件之间互相传值传得乱七八糟。4.3 页面交互商品展示到购物车的完整链路商品展示这块首页就是几个组件嵌套搜索栏、分类导航、图书列表卡片。图书列表卡片组件接收一个book对象作为prop点击卡片跳转到详情页加入购物车则触发store里的action。购物车交互是前端最值得细细设计的地方。加购操作不是简单的把商品塞进列表要处理几种情况该图书是否已经在购物车里有了就更新数量没有就新增一条。购物车页面里数量加减、勾选、删除、计算总价这些状态都在Pinia里维护组件通过计算属性自动响应更新不需要手动操作DOM。还有一个实操技巧图书详情页可能包含作者简介视频、出版社宣传片之类的富媒体内容。有些视频是m3u8格式的流媒体地址我在Vue里用hls.js这个库实现过播放用法很简单if (Hls.isSupported()) { const hls new Hls() hls.loadSource(url) hls.attachMedia(videoEl) }这个功能上线后图书详情页的停留时长明显提高了视频比文字描述更直观用户下单意愿也更强。4.4 Vite打包与部署的布局异常排查很多新手在本地开发一切正常打包上线后页面就白屏了。这个十有八九是资源路径问题。默认情况下Vite的build配置里base是/部署到子目录时资源会找不到。解决办法export default defineConfig({ base: ./, // 使用相对路径 build: { outDir: dist } })打成相对路径后dist目录的index.html就能通过Nginx任意路径访问了。Vue Router如果用history模式上线后还有一个经典问题用户直接访问某个子路由比如访问/login页面时Nginx会返回404。因为Nginx找的是/login这个静态文件但实际文件不存在。解决办法是Nginx配置try_files指令把请求兜底到index.htmllocation / { try_files $uri $uri/ /index.html; }换句话说开发时用history模式很爽部署时必须把服务端兜底配置好否则会有无数个404找上门来。5. 环境搭建与部署从零跑通一个全栈项目5.1 JDK、MySQL、IDEA的前置准备后端跑起来前环境要先理干净。我遇到过最典型的坑是SpringBoot版本和JDK版本不搭尤其是SpringBoot 3.x要求JDK 17及以上如果你的JDK还是8直接报错。这套项目我建议用SpringBoot 2.7.x搭配JDK 8既有稳定生态又不会有太高学习门槛。MySQL安装这块Windows用户最省心的是下载msi安装包一路NEXT就行但记住安装过程中会让你设置root密码别设太复杂的开发环境root/123456就够了。还有一步千万别漏选择字符集时一定要选utf8mb4否则后面存中文乱码。Linux服务器上我习惯用免安装版解压然后通过配置文件绑定端口和字符集[mysqld] port3306 character-set-serverutf8mb4IDEA创建SpringBoot项目的入口在File - New - Project - Spring Initializr选好JDK版本后依赖选Spring Web、MyBatis、MySQL Driver就够起步了。如果网络不好卡在Initializr加载可以改成国内的阿里云镜像地址。5.2 配置文件与SQL日志开启后端项目的配置集中在application.yml里几个关键配置项数据源连接信息、MyBatis映射文件位置、日志输出级别、端口号。开发阶段我强烈建议把MyBatis的SQL日志打开这样每个请求到底执行了什么SQL、参数是什么、耗时多久控制台全部看得到排查问题极其方便。spring: datasource: url: jdbc:mysql://localhost:3306/book_mall?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpldataSource URL里的serverTimezone一定要配否则连接MySQL 8时会出现时区报错。map-underscore-to-camel-case这个配置也很关键它能帮我们把数据库的snake_case字段如book_name自动映射到实体类的camelCase属性bookName省去大量resultMap配置。5.3 Nginx部署与前后端联调后端打包用Maven的package命令生成一个可执行的jar包服务器上执行java -jar book-mall.jar就能启动。如果想后台运行用nohup命令nohup java -jar book-mall.jar log.log 21 前端打包后得到dist目录把dist目录的文件放到Nginx的html目录下然后配置Nginx把前端页面和后端接口都转发出去server { listen 80; server_name localhost; # 前端页面 location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端接口 location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }前端请求地址统一写成相对路径/api/xxx这样用户访问时Nginx自动把请求转发给后端的8081端口还顺带解决了跨域问题比在后端代码里配CORS要干净得多。6. 高频问题排查与面试考点串联6.1 MySQL与MyBatis的常见坑先整理几个我在项目里实操时踩过、或者是面试中高频出现的MySQL问题。第一个是MySQL 8的默认认证插件。如果你连数据库时报“Public Key Retrieval is not allowed”除了数据源URL加allowPublicKeyRetrievaltrue之外还可以检查一下root用户的加密规则是不是caching_sha2_password老客户端不认识这个规则手动改成mysql_native_password就能解决。第二个是MyBatis里单个数字字符的比较。有个冷门但真实存在的坑当你在XML里写name #{name} 且name传的是单字符的值比如“1”时MyBatis在OGNL表达式中会把char类型和String类型搞混导致判断结果异常。我在商品状态查询时遇到过解决方法是传值时用String类型并且在XML里用toString()强制转换或者用Param注解显式声明参数类型。第三个是分批操作慢的问题。批量插入一万条数据一条一条insert循环插入可能耗时十几秒而且会频繁提交事务。使用MyBatis的 标签拼接成一条多值的INSERT语句插入时间能缩短到一秒以内。insert idbatchInsert INSERT INTO book (book_name, author, isbn, price, stock) VALUES foreach collectionlist itembook separator, (#{book.bookName}, #{book.author}, #{book.isbn}, #{book.price}, #{book.stock}) /foreach /insert6.2 从项目反推面试高频题这个项目做完之后你去面试被问到的高频问题其实都能从项目里反推出来。SpringBoot部分最主流的问题是自动配置原理。结合这个项目理解起来容易得多spring-boot-starter-web依赖引入了spring.factories文件里声明的自动配置类条件注解ConditionalOnClass判断当前classpath下有没有对应类比如spring-webmvc存在时自动配置DispatcherServlet和Tomcat。所以你在pom里加了MySQL驱动SpringBoot就自动帮你创建一个DataSource连接池的Bean不加它也不会报错打断启动。这就是约定大于配置的落地逻辑。MyBatis部分最常见的面试点是缓存机制和#{}与${}的区别。缓存机制前面已经讲过了#{}和${}的区别也要结合项目回答推荐全部用#{}。要警惕动态SQL里的字符串拼接比如order by 字段这种无法预编译的场景做白名单校验再允许拼接避免SQL注入。Vue部分的常见问题有这么几个生命周期函数、组件通信方式、Vue响应式原理、Vue Router的两种模式。组件通信最常被问这个项目里我用到过父传子用prop子传父用emit跨页面共享状态用Pinia不相关的组件通信用事件总线或Pinia统一管理。回答时结合具体业务例子说清楚比你背几条理论要有说服力得多。6.3 系统上线后的稳定性问题排查思路项目跑起来只是第一步稳定上线才是真正的考验。实际生产环境出过的问题千奇百怪我挑几个高频的整理成速查表现象原因解决办法前端请求404路由history模式刷新子页面Nginx配置try_files至index.html接口跨域报错前后端口不同Nginx反向代理同源或后端CORS双击提交重复下单前端没有防重复提交、后端没有唯一订单号约束幂等性设计后端去掉重复请求数据库连接池爆满请求量突增连接池默认配置太小hikari配置适当调大maximum-pool-sizeSQL执行suddenly变慢缺少索引、SQL书写不合理EXPLAIN分析执行计划优化SQL加索引JSON字段返回null实体类字段名和表字段映射不规范开启map-underscore-to-camel-case关于接口性能优化给一个很容易见效的思路热点接口加缓存。商品详情页这种读多写少的接口第一次查数据库后把结果存到Redis里设置5分钟的过期时间后面直接查缓存响应时间能从200ms降到20ms以内。虽然这个项目主体没用Redis但它绝对是你做技术答辩时最好的亮点之一。6.4 扩展思路这套源码还能怎么升级项目跑通了懂原理了面试能讲清楚这只是第一步。如果想让它成为你简历上更有分量的项目下面这几个方向值得投入精力。第一引入Redis做缓存。商品列表页、首页的轮播图数据这些热点数据都可以缓存起来购物车也可以从MySQL表改成Redis的Hash结构性能提升非常明显。第二接入Elasticsearch做搜索引擎。MySQL的LIKE %关键词%查询在百万级数据量下性能堪忧把书名、简介、作者信息同步到ES里用倒排索引做全文检索体验完全是两个档次。第三集成支付网关。接入沙箱环境完成真实的支付流程交互把这个模块做出来整个电商闭环才真正完整。第四用Docker编排容器化部署。把MySQL、后端jar包、前端Nginx分别做成镜像用docker-compose一键启动这个技能在简历上的价值甚至超过业务代码本身。扩展不在多把一个方向做深就足够了。我见过太多简历上列满了技术名词结果面试官一问细节就露馅。宁可只加一个Redis把缓存穿透、缓存击穿、缓存雪崩都考虑一遍写上每个方案取舍的理由这比单纯堆技术栈要有说服力得多。最后说点掏心窝的话。代码敲完、项目上线的那一刻成就感确实是实实在在的。但更有价值的是你在这过程中训练出来的排查问题的思路——遇到报错不慌先看日志、再定位问题、然后逐步修复。这套源码在GitHub上已经有不少人跑通偶尔有人提issue问一些环境问题我这边的经验是这个组合拳对新手非常友好学习曲线称得上平缓。后续如果想深挖可以在我在上面列的扩展方向上继续沉淀做成一版真正能商用上线的图书商城也不是什么遥不可及的事。