
简介这是一份基于SpringBootVueMySql实现的网上图书商城完整项目面向Java后端与前端学习者、毕业设计学生可直接作为课程设计或毕设原型参考。项目包含前后端全部源码、数据库脚本和部署说明下载后按文档配置环境即可运行适合需要快速搭建可演示商城系统的开发者。压缩包共845个文件约33.18MB核心文件类型包括139个Java源码、51个Vue组件、164个JavaScript脚本、53个CSS样式及1个SQL数据库脚本另有部署用bat脚本与配置文件目录结构清晰便于按模块阅读和二次开发。该系统经过严格调试后台管理、前台展示与数据库交互均已打通配套部署说明中给出了navicat、Tomcat等工具下载及运行教学指引。目前已有786人学习使用对于想要掌握SpringBoot整合Vue开发流程或快速完成毕业设计任务的读者具有不错的参考价值。1. 网上图书商城项目为什么这套组合值得你动手做你是不是也遇到过这样的需求做一个图书商城要能注册登录、浏览图书、加购物车、下单结算后台还得能管理图书和订单。技术上点名要用SpringBootVueMySql最后交付一个zip包里面装着源码、数据库脚本和部署说明。这套组合几乎成了商城类项目的默认答案——SpringBoot管后端接口和业务逻辑Vue管用户界面MySql管所有业务数据。我经手过好几个类似的交付说句实话技术栈本身不复杂但真正的分水岭不在写代码而在把「源码、数据库、部署说明」这三样东西组织好让项目换一台电脑也能跑起来。这篇文章适合两类人一是拿它做课程设计或毕设的学生二是要快速交付外包商城的小团队。往下读你会知道这套东西怎么从零搭起来以及那些最容易让人翻车的细节到底在哪。2. 把后端骨架先立起来SpringBoot 的项目结构、接口分层与 MySql 连接拿到一个网上图书商城的源码包第一步不是急着看业务代码而是先看目录结构和pom文件。常见做法是一个标准Maven工程外层是SpringBoot里面按controller、service、mapper、entity分层。这套分层本身不新鲜但它是整个商城的承重墙——图书查询、购物车、订单这些业务全部挂在这四层上。先把这堵墙砌好后面加功能才不会变成一坨乱麻。2.1 先定版本组合Spring Boot 2.7.x MyBatis-Plus MySql 8.0网上图书商城这类项目ORM我一般不会用原生MyBatis写一堆XML而是直接用MyBatis-Plus。单表CRUD零SQL分页查询一个插件搞定代码量能省三分之一。版本组合上Spring Boot 2.7.x配MyBatis-Plus 3.5.x是一套非常稳的组合避开Spring Boot 3.x在javax命名空间上的坑。下面是最小可跑的pom依赖去掉了无关的测试和插件parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies这里有个细节值得注意MySql 8.0对应的驱动artifact已经从mysql-connector-java改成了mysql-connector-jSpring Boot 2.7的依赖管理里已经内置了这个坐标所以不需要写版本号。MyBatis-Plus 3.5.x和Spring Boot 2.7的兼容性近乎完美自动配置能省掉大量样板代码。如果你非要上Spring Boot 3.x那就要把所有javax.*的包替换成jakarta.*这个坑我后面专门讲。2.2 application.yml 配置数据源、端口与 MyBatis-Plus 的四个关键参数配置文件是后端跑起来的第一道关卡。很多新手拿到项目换个环境就报错九成是数据源配置里的连接串不对。我一般会在配置文件里把字符集、时区、SSL和公钥获取这四个参数一次性写全server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto连接串里的四个参数每个都有讲究。useUnicodetruecharacterEncodingutf8解决中文乱码你和MySql之间传中文书名、作者名全靠它useSSLfalse是让本地开发别去做无谓的SSL握手否则会拖慢启动速度serverTimezoneAsia/Shanghai是MySql 8.0的硬性要求不写会直接报时区错误allowPublicKeyRetrievaltrue是8.0用caching_sha2_password认证时需要的不然首次连接会报Public Key Retrieval异常。这四个参数是我每次写配置的标配少一个就要在踩坑路上走一遍。MyBatis-Plus这边map-underscore-to-camel-case让数据库的create_time自动映射成Java的createTime省去手写resultMaplog-impl配上StdOutImpl开发阶段能在控制台直接看到每条SQL的执行情况排查问题比看业务日志快得多。id-type: auto表示主键走数据库自增配合建表语句里的AUTO_INCREMENT插入时不用手动给id赋值。2.3 一条图书列表接口的完整链路Controller → Service → Mapper后端不管多复杂最终都归结为「接口接收参数、Service处理逻辑、Mapper操作数据库」这条链路。用一个图书分页查询接口来演示是最能看清这套结构的。先写Controller层负责接收前端传过来的页码、每页条数和搜索关键词RestController RequestMapping(/api/book) public class BookController { Autowired private BookService bookService; GetMapping(/page) public ResultIPageBook page( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 12) int size, RequestParam(required false) String keyword) { return Result.ok(bookService.queryPage(page, size, keyword)); } }Controller只做参数接收和结果包装不写任何业务代码。Result是一个统一的返回体里面通常有code、message、data三个字段前端axios拦截器可以统一处理。RequestParam(defaultValue 1)保证了前端少传参数时接口不会报错这种防御性写法在前后端分离项目里是基本功。Service层继承MyBatis-Plus的ServiceImpl直接获得通用CRUD能力再补一个自定义的分页查询方法Service public class BookServiceImpl extends ServiceImplBookMapper, Book implements BookService { Override public IPageBook queryPage(int page, int size, String keyword) { PageBook p new Page(page, size); LambdaQueryWrapperBook wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Book::getTitle, keyword) .eq(Book::getStatus, 1) .orderByDesc(Book::getSales); return this.page(p, wrapper); } }这段代码里PageBook是MyBatis-Plus的分页对象LambdaQueryWrapper是条件构造器。like和eq的第一个参数是布尔值当keyword为空时like条件自动失效这种写法比手动拼接SQL安全得多永远不会出现空字符串导致的LIKE %%全表扫描。eq(Book::getStatus, 1)的意思是只查上架状态的图书这个字段我在建表时就会做索引查询效率从源头保证。orderByDesc(Book::getSales)让销量高的书排前面模拟真实商城的热销逻辑。有了Controller和ServiceMapper层甚至可以不用写代码。BookMapper接口继承BaseMapperBookMyBatis-Plus会在运行时自动生成实现类分页插件只需要在启动类加一个PaginationInnerInterceptor的Bean。这里有一个关键点分页插件不注册page方法不会真正执行LIMIT而是查全表再内存分页数据量小看不出来等图书列表涨到上万条就会明显卡顿。这个陷阱我帮人排查过多次都是因为少配了一个Bean。3. 数据库是商城的灵魂表结构设计、索引取舍与订单状态机商城项目的数据库设计决定了后期开发是顺手还是别扭。很多新手拿到源码包先急着跑起来但数据库脚本才是整个项目的地基。一个典型的网上图书商城核心表就六张用户表、图书表、购物车表、订单表、订单明细表外加一张管理员表。表不在多而在字段设计和关系划分是否合理。3.1 建库建表六张核心表的字段设计与字符集选择我曾经见过有人用latin1建库存中文全是问号后来整个库重建。所以建库第一步就要用utf8mb4它比utf8多支持Emoji和生僻字图书书名里偶尔出现的特殊字符也能正常存储。下面是核心建表脚本我加上了必要的外键关系和索引CREATE DATABASE IF NOT EXISTS bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE bookstore; CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, nickname VARCHAR(50) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE book ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, sales INT NOT NULL DEFAULT 0, cover VARCHAR(255) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status_sales (status, sales) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE cart_item ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, PRIMARY KEY (id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, book_id INT NOT NULL, book_title VARCHAR(200) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这些表的设计遵循了一个重要原则业务数据要冗余不要过度范式化。比如order_item表里我冗余了book_title和price字段这样即使图书信息后来被修改甚至删除历史订单里的快照数据依然完整。做商城项目最怕的是订单查出来书名变了用户投诉时你解释不清数据冗余就是为这种业务场景兜底。订单编号order_no我单独建了唯一索引。这个字段在真实项目中不能靠数据库自增id而是用时间戳加随机数生成确保并发下不重复。idx_user_status这个联合索引是为了支撑「我的订单列表按状态筛选」这个高频查询——用户进入个人中心看「待支付」「已发货」等列表时走这个索引就能快速定位避免全表扫描。3.2 为什么不建议用外键商城项目里的物理外键与逻辑外键之争你可能注意到我在建表脚本里故意没有写FOREIGN KEY约束。这是很多教科书写法与实际项目之间最大的分歧点教科书强调外键保证数据一致性但实际商城项目里物理外键会让插入和删除变慢而且订单、购物车这类表一旦被外键锁住并发一高就容易产生死锁。我一般只在cart_item.user_id、orders.user_id这些关系上保留索引让代码层去保证逻辑外键的一致性。你可以自己权衡一下用了物理外键删除图书时如果还有购物车引用直接报错而逻辑外键的做法下删除图书前先检查有没有活跃的购物车记录或未完成订单没有就删有就走下架流程。后者更符合商城业务。网上能下载到的图书商城源码绝大多数也是逻辑外键的方案因为InnoDB对不带外键的大表并发处理能力明显更强。3.3 订单状态机与库存扣减两个最容易出bug的业务点图书商城的业务核心不在CRUD而在订单流转。orders.status字段我定义了一个状态机0待支付、1已支付、2已发货、3已完成、4已取消。状态流转方向是单向的1不能跳回02不能跳回1这类校验我会放在Service层而不是靠前端按钮控制。库存扣减是另一个容易踩坑的点。新手常见的写法是先查询库存是否充足再执行更新Book book bookMapper.selectById(bookId); if (book.getStock() quantity) { // 更新库存 }这段代码在并发场景下必炸。两个用户同时下单同一本书都查到了库存5都扣成2但实际只该扣一次。正确的做法是把判断和扣减合并成一条SQLUPDATE book SET stock stock - #{quantity} WHERE id #{bookId} AND stock #{quantity}受影响行数为1表示扣减成功为0表示库存不足。下单前先扣库存、下单失败再回补这是商城类项目的基本盘。如果你拿到的源码里还是先查后改的写法建议一定要改成这种原子操作否则压测一上来就会翻车。图书这种SKU单一、不像服装有尺码颜色的商品库存扣减已经算简单了但该有的并发意识不能少。4. Vue 前端搭建与对接从项目初始化到接口联调后端接口就绪后前端要能把页面渲染出来并且和后端顺畅通信。网上图书商城的前端通常包含首页图书展示、图书详情、购物车、结算页、个人中心、后台管理这六大页面。Vue的工程化方案很多但这个项目场景下主流做法是Vue 3加Vite脚手架组件库用Element Plus。Vite比Vue CLI启动快得多热更新也顺滑新项目没必要再开Webpack那条老路。4.1 初始化项目与安装依赖Vite 创建工程的两个注意点用Vite创建Vue项目常见的命令是npm create vitelatest frontend -- --template vue。但直接生成的模板里没有集成Vue Router和axios需要再手动安装。这里有一个环境相关的坑如果你在本机用Windows开发安装依赖时经常卡在node-sass这类原生模块上Vite模板用的是dart-sass基本不会遇到那个问题但npm源如果很慢可以先设置淘宝镜像再安装。# 创建 Vue3 项目 npm create vitelatest frontend -- --template vue # 进入目录并安装基础依赖 cd frontend npm install # 安装路由、状态管理和 HTTP 库 npm install vue-router4 pinia axiosvue-router4是Vue 3配套的路由版本pinia是Vue 3官方推荐的状态管理库用来存购物车数据最合适。axios负责和后端接口通信。三个库装完就能开写业务代码。这里我特别说一下npm create vite之后项目里默认没有vue-router你必须自己装——这个步骤千万别漏漏了后面写路由时import直接报错。Vite开发服务器的默认端口是5173后端SpringBoot跑在8080两边端口不同就会触发浏览器的跨域问题。解决办法不是在后端加CrossOrigin到处注解而是在Vite的配置文件里做代理转发把/api开头的请求统一转到8080端口。这样浏览器看到的请求是同源的跨域问题从根上消失。4.2 axios 封装与接口拦截器统一处理 token 和错误码商城系统里大部分接口需要登录后才能访问所以axios的二次封装是必须做的一步。封装的核心目的一是自动携带token二是统一处理后端返回的错误码三是遇到401时自动跳登录页。我通常会建一个src/utils/request.js文件// src/utils/request.js import axios from axios import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) // 响应拦截器统一处理业务码和 HTTP 异常 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { alert(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } ) export default request这个封装是前后端分离项目的标准动作。baseURL: /api配合Vite的代理配置开发时把所有请求都发到/api前缀下部署时用Nginx或后端静态资源承接同样的路径前端代码一行不用改。这是网上图书商城项目里最值得抄的一段代码不管后端接口怎么写前端统一走这个出口维护成本最低。需要注意的是localStorage.getItem的token在服务端校验失败时返回401前端这里统一跳登录页就不会出现「接口报错但用户还停在页面上」的尴尬状态。4.3 首页图书列表页面一个完整的前后端调用示例有了封装好的request写一个页面就很快了。首页图书列表是最典型的例子——页面加载时请求后端分页接口拿到数据渲染卡片加载过程中显示loading状态。用Vue 3的组合式API写起来非常简洁script setup import { ref, onMounted } from vue import request from ../utils/request const books ref([]) const loading ref(true) onMounted(async () { try { const res await request.get(/book/page, { params: { page: 1, size: 12 } }) books.value res.data.records } finally { loading.value false } }) /script template div v-ifloading加载中.../div div v-else classbook-grid div v-forbook in books :keybook.id classbook-card img :srcbook.cover :altbook.title / h3{{ book.title }}/h3 p{{ book.author }}/p span{{ book.price }}/span /div /div /templateonMounted是Vue 3的生命周期钩子等价于Vue 2的mounted页面渲染完成后自动触发数据请求。res.data.records对应后端分页对象IPage里的records字段这条对应关系是联调时最容易对不上的地方——后端返回的是{ records: [], total: 100 }前端如果去拿res.data.list就会拿到undefined。拿到一个源码包时先看后端Result和IPage的字段结构再对前端的取值逻辑能省掉大量联调时间。购物车页面则用pinia做一个全局store把加入购物车的动作放在store里这样在不同页面之间切换时购物车数据不会丢失。后端购物车表只是持久化前端的实时状态靠pinia保证这就是状态管理存在的意义。路由方面vue-router配置好/home、/cart、/order这些路径再用router-view/挂载整个前端的骨架就完整了。5. 部署与运行避坑五件高频翻车现场和对应解法网上图书商城项目最难的部分从来不是写代码而是让项目在另一台机器上顺利跑起来。我拿到过很多「我本地能跑到你那边就报错」的求助总结下来全是环境差异导致的坑。下面这五条是我见过频率最高的踩坑记录每一条都值得你在部署时留个心眼。5.1 SpringBoot 版本太高javax 和 jakarta 的命名空间冲突现象项目启动时编译报错找不到javax.servlet.Filter或者引入的第三方库内部引用了javax.*而当前环境只有jakarta.*。原因Spring Boot 3.x把Java EE的命名空间从javax迁移到了jakarta所有javax.servlet、javax.validation都变成了jakarta.servlet、jakarta.validation。如果你拿到的项目依赖里混着Spring Boot 3.x的框架和Spring Boot 2.x写法的代码就必然炸。解决拿到源码先看pom.xml里的spring-boot-starter-parent版本如果是3.x开头就把代码里所有import javax.*批量替换成import jakarta.*IDEA里CtrlShiftR全局替换即可。我的习惯是明确告诉你本地开发图书商城这种中小型项目直接降回2.7.x最省事没必要为了新版本多折腾一遍。5.2 MySql 驱动连接串时区和公钥报错如何解决现象后端启动时报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或者第一次查询数据库时卡死并报Public Key Retrieval is not allowed。原因MySql 8.0更换了认证插件连接串少了时区参数或公钥参数就会触发这两个经典异常。老教程里的com.mysql.jdbc.Driver驱动类名在新版本也已被移到com.mysql.cj.jdbc.Driver。解决把驱动类名换成com.mysql.cj.jdbc.Driver连接串里带上serverTimezoneAsia/Shanghai和allowPublicKeyRetrievaltrue。这两处改完90%的连接问题都能解决。5.3 Vue 路由刷新 404history 模式的经典问题现象前端开发时一切正常打包部署后点页面内跳转没问题但按F5刷新或者直接访问/cart路径服务器返回404。原因Vue Router默认使用history模式路由路径是前端模拟的服务器上根本没有/cart这个真实文件。刷新时浏览器向服务器请求/cart后端找不到对应资源就返了404。解决点击页面内链接没问题刷新就炸直接换成hash模式最省心——把createWebHistory()改成createWebHashHistory()路径会变成/#/cart刷新由前端自己管理。如果坚持用history模式就要在后端加一个转发规则把未匹配到的路径转发到index.html我之前在第6章会介绍一种省事的部署办法能顺带解决这个问题。5.4 Windows 本地安装 MySql服务启动失败与登录环境变量现象在Windows 10上按教程解压MySql 8.0并执行mysqld --initialize后启动服务时提示服务无法启动或者命令行执行mysql -uroot -p提示命令不存在。原因一是初始化时数据目录权限不对二是没有把MySql的bin目录加到系统PATH环境变量里。很多人卡在第三步进程启动后马上退出日志文件又没开黑匣子一个。解决Windows下用解压版装MySql资深做法是新建一个my.ini配置文件指向数据目录然后执行mysqld --initialize-insecure生成一个无密码的root账号再用mysqld install注册成Windows服务。环境变量问题更简单把C:\Program Files\MySQL\bin加到PATH然后重开命令行窗口。如果你用的Docker下面有专门一条说容器方式的坑。5.5 Docker 安装 MySql 失败端口占用与容器权限现象执行docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD123456 mysql:8.0后容器几秒就退出docker logs隐约看到Cant start server: Bind on unix socket或权限相关报错。原因排在最前的是宿主机3306端口被占用——本机装了MySql服务或其它程序占了3306Docker映射失败容器直接退出。其次是MySQL官方镜像的/var/lib/mysql目录权限问题这在部分Windows和Linux发行版上会触发初始化失败。解决先查占用netstat -ano | findstr 3306Windows或ss -lntp | grep 3306Linux有程序占用就把Docker的映射端口改成-p 3307:3306。权限问题加--privileged再跑一次或者挂载一个具名数据卷docker run -d -p 3307:3306 -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD123456 mysql:8.0。这两个是Docker跑MySql最常见的翻车点按顺序排查基本都能救回来。6. 把 Vue 打包进 SpringBoot单端口部署的进阶技巧如果你把前端和后端分开部署就得同时管两个端口、配置Nginx转发、处理跨域这对图书商城这种小型项目来说有点臃肿。更轻量的做法是把Vue打包后的静态文件直接塞进SpringBoot的src/main/resources/static目录让后端同时充当接口服务和静态资源服务器对外只暴露8080一个端口。这个方案在课程设计交付和中小型外包项目里非常实用部署说明里一句话就能写清楚java -jar bookstore.jar完事。具体的操作脚本我一般写成下面这个样子的一条命令完成前端打包、静态资源替换、后端打包和启动#!/bin/bash # 1. 前端构建产物输出到 frontend/dist cd frontend npm run build # 2. 清空 SpringBoot 的静态资源目录替换为最新前端产物 rm -rf ../src/main/resources/static/* cp -r dist/* ../src/main/resources/static/ # 3. 后端打包并后台启动 cd .. mvn clean package -DskipTests nohup java -jar target/bookstore-0.0.1-SNAPSHOT.jar bookstore.log 21 echo 部署完成访问 http://localhost:8080这个方案有一个前提必须注意前端路由如果用的history模式打包放进static后刷新非首页页面依然会404。因为/cart这个路径在SpringBoot静态资源里找不到对应文件。解决方式有两个一是前端路由改hash模式这是最简单可靠的办法二是在SpringBoot里加一个转发Controller把未匹配的路由转发到index.html。但后者要小心处理接口路径和静态资源的优先级容易把自己绕进去。我做这个方案时默认建议直接上hash模式利益最大化、坑最少这也是我踩过一次路由转发优先级后的血泪教训。我习惯把这个脚本命名为deploy.sh放在源码包根目录和部署说明放在一起。新环境只需要装好JDK 8、Maven、Node.js和MySql然后跑一次这个脚本就能看到完整商城。这比我见过的一堆手写步骤高效得多也是「部署说明」这份文档的核心——告诉对方先改哪个配置文件、再跑哪条命令、最后访问哪个地址。这个单端口部署的思路顺带解决了打包后跨域的问题因为前端和后端同源了浏览器的同源策略不再构成任何障碍。希望这个技巧能让你在交付这个图书商城时少走一段弯路也帮到你的实际项目里。本文还有配套的精品资源点击获取