SpringBoot+Vue+MyBatis+MySQL前后端分离宠物商城系统实战

发布时间:2026/9/15 10:09:03
SpringBoot+Vue+MyBatis+MySQL前后端分离宠物商城系统实战 先说结论我前后花了两周多时间把一个从零到一的前后端分离宠物商城系统完整做了出来技术栈就是标题里那套SpringBoot Vue MyBatis MySQL。这东西最大的价值不在于“商城”这个业务本身而在于它把前后端分离开发里最容易被新手卡住的环节全部串了一遍接口设计、Token鉴权、跨域处理、MyBatis操作、Vue路由拦截、打包部署。如果你正打算做毕设、练手项目或者刚入行想搞明白“前端怎么跟后端配合”这篇内容应该能帮你省掉不少自己摸索的时间。我写这篇文章的思路就是按实际开发顺序来讲从项目怎么拆、数据库怎么设计到后端代码怎么写、前端页面怎么调最后到怎么打包上线。每一步我都会交代清楚“为什么这样做”把我在开发过程中踩过的坑和当时的解决方案也一并整理出来。1. 项目整体设计与架构拆解1.1 为什么选择前后端分离而不是传统单体架构几年前做这种Web商城最常见的做法还是Thymeleaf或者JSP直接在服务端渲染页面SpringBoot返回一个完整的HTML页面前端JS只做点表单校验和弹窗。这种做法在小项目里确实简单但到了前后端需要并行开发、或者以后要扩展小程序和移动端的时候问题就来了后端渲染的页面没法复用每次加一个客户端就要重新做一套。前后端分离的核心思路是后端只负责提供JSON格式的数据接口完全不关心页面长什么样前端用Vue这类框架负责页面渲染、路由控制、状态管理。两边通过HTTP接口通信用Token来维持登录状态。这个宠物商城我用前后端分离来做主要看重三点前端开发和后端开发可以完全并行接口定义好之后互不阻塞。同一套后端接口以后做管理后台、微信小程序都能直接复用。前后端各自独立部署前端静态文件交给Nginx后端打成一个Jar包资源隔离清晰。当然前后端分离也有一些需要额外处理的问题最典型的就是跨域。前端跑在http://localhost:8080后端跑在http://localhost:9090浏览器会拦截跨域请求这就需要在后端配置CORS或者用Nginx反代来规避。1.2 技术栈选型的出发点这套技术栈组合现在几乎成了Java全栈项目的标配不是没有原因的。SpringBoot负责提供后端服务能力内置Tomcat简化了Spring的配置一个注解就能启动一个Web应用。对于中小型项目来说它的开发效率和稳定性都足够好而且生态非常成熟找资料、排查问题都很方便。Vue负责前端页面采用组件化开发数据驱动视图。宠物商城这种页面比较多、交互不算特别复杂的项目用它来做非常顺手。路由、状态管理、Axios请求封装等配套方案都已经很成熟社区里的踩坑记录也很多。MyBatis负责数据库访问层。相比Spring Data JPA那种“全自动”方案MyBatis的半自动特性在写复杂查询和动态SQL时更灵活SQL是手写的可控性强。商城项目里经常要根据多个条件组合查询商品比如价格区间、宠物类别、关键字搜索MyBatis的动态SQL在这种场景下特别好用。MySQL负责数据存储。免费、稳定、性能足够个人项目和学习项目用它最合适。整体流程是这样的Vue前端通过Axios发起HTTP请求请求到达SpringBoot的Controller层Controller调用Service层处理业务逻辑Service层通过MyBatis的Mapper接口操作MySQL数据库数据以JSON格式返回给前端渲染。2. 数据库设计与核心功能规划2.1 宠物商城的核心功能模块在动手写代码之前先把功能模块理清楚。我这个宠物商城面向两个角色普通用户和管理员。用户端功能包括浏览商品、搜索商品、查看商品详情、加入购物车、下单购买、管理个人收货地址和查看订单状态。管理员端功能包括商品上下架与库存管理、分类管理、订单发货及状态更新。麻雀虽小五脏俱全这些功能覆盖了一个交易类系统最常见的业务闭环。先把模块边界划清楚后面写代码和设计表结构才不会乱。2.2 数据库表结构设计数据库设计是整个项目的地基表结构没设计好后面所有业务代码都会写得很别扭。我当时设计了下面几张核心表。用户表user保存用户的账号信息包括用户名、加密后的密码、手机号、邮箱、头像地址、角色标识user或admin、注册时间。密码绝对不能明文存储这里用的是BCrypt加密。宠物商品表pet核心商品表字段包括宠物名称、类别ID、描述、价格、原价、库存、销量、主图地址、详情图片、状态上架/下架、创建时间。设计时把图片地址单独存字符串实际开发中图片会走对象存储或服务器静态目录。商品分类表category包括分类名称、父分类ID、排序字段。这里做了简单的两级分类比如“狗狗”下面还可以分“小型犬”和“大型犬”。购物车表cart关联用户ID和宠物ID记录数量。设计时给用户ID和宠物ID加上唯一索引避免同一用户重复添加同一商品而是直接更新数量。订单表orders保存订单主信息包含订单编号、用户ID、总金额、收货人姓名、联系电话、收货地址、订单状态待付款、待发货、待收货、已完成、已取消、下单时间。订单编号我采用的是时间戳加随机数方式生成保证唯一性。订单明细表order_item保存订单里的每个商品条目包含订单ID、宠物ID、商品名称快照、商品图片快照、购买单价、购买数量。这里做了一个很关键的操作商品名称和图片都做了快照防止商品信息后续修改影响历史订单的展示。表之间的关系是用户1对多购物车记录用户1对多订单订单1对多订单明细订单明细多对1宠物商品。2.3 为什么订单明细要存商品快照字段这一点值得单独拿出来说。我第一版设计的时候订单明细表只存了宠物ID和数量展示订单的时候直接去关联商品表查名称和图片。后面测试的时候发现问题了如果管理员把某个商品下架或者修改了名称历史订单里显示的商品名就会变得不对甚至查不到。这就是典型的“外键关联查询一时爽数据变动全翻车”场景。解决方式就是商品名称、图片、价格在生成订单的那一刻都拷贝一份到订单明细表里这就是“快照”概念。虽然会冗余一些存储但对于交易类系统来说保证历史数据的可追溯性比省那点存储重要得多。2.4 接口规划与统一返回格式后端接口设计我遵循了RESTful风格比如POST /api/user/login用户登录GET /api/pet/list分页查询商品列表GET /api/pet/{id}查询商品详情POST /api/cart/add添加购物车POST /api/order/create创建订单GET /api/order/list查询我的订单这里有一个细节值得新手注意所有Controller返回的数据格式必须统一不能一个接口返回{code: 0, data: ...}另一个又只返回一个裸数组。我当时封装了一个ResultT类包含code、message、data三个字段。前端Axios拦截器统一处理这个结构体代码会清爽很多。统一返回格式还有一个附带好处全局异常处理的时候不管业务哪里异常前端拿到的JSON结构都是一样的页面处理异常逻辑就非常统一。3. 后端核心实现与MyBatis实操细节3.1 SpringBoot项目搭建与分层结构后端项目我使用IDEA来创建Spring Initializr选择SpringBoot 2.7.x版本为什么不用更高的版本因为有些第三方整合包对SpringBoot 3.x的支持还不够稳定而且3.x要求JDK17以上我开发机器上用的还是JDK8没必要为了追求新版本给自己添麻烦。项目分层是标准的四层结构controller接收前端请求做参数校验调用Service层返回统一格式数据。service处理业务逻辑事务控制在这一层使用Transactional注解。mapperMyBatis的Mapper接口定义数据库操作方法。entity实体类对应数据库各张表。依赖方面核心就五个spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok、jjwt用于生成和校验Token。3.2 登录鉴权与Token处理用户登录成功后后端会生成一个Token返给前端。我这里用的是JWTJson Web Token把用户ID、用户名、角色封装进Token里服务端不需要存储会话状态天然支持分布式扩展。JWT生成之后前端每次请求都会在请求头里带上Authorization: Bearer token。后端通过一个拦截器HandlerInterceptor统一拦截需要登录才能访问的接口在拦截器里解析和校验Token。这就是前后端分离项目里非常关键的“无状态登录”。这里有几个细节必须注意Token里设置了过期时间我设的是24小时过期后需要重新登录。拦截器要放行登录接口和商品浏览接口其余接口一律校验Token。管理员接口不仅要校验Token还要检查Token里的角色是不是admin。前端在Axios请求拦截器里统一把Token塞进请求头在响应拦截器里统一处理“Token过期”这个错误码比如跳回登录页。为了让拦截器能拿到当前登录用户的ID我写了一个UserContext工具类用ThreadLocal存储当前请求的用户信息请求结束的时候清理掉。这样Service层就不用每个方法都把用户ID传一遍直接用UserContext.getUserId()就能取到。3.3 MyBatis中的几个关键配置与踩坑MyBatis接入SpringBoot最需要注意的是实体类字段和数据库字段的映射问题。我数据库表字段风格统一用的是下划线比如create_timeJava实体类属性风格统一用驼峰比如createTime。要让他们自动映射上必须在配置文件里开启驼峰映射mybatis: configuration: map-underscore-to-camel-case: true mapper-locations: classpath:mapper/*.xml这个配置不写的话查询结果里的create_time字段映射不到Java实体的createTime属性上全是null排查起来容易让人抓狂。再一个就是MyBatis的#{}和${}区别。#{}是预编译占位符会生成?可以有效防止SQL注入${}是字符串拼接直接把值拼进SQL里。我在实现动态排序功能和动态查询列的时候才会用${}但前提是传入值只能来自后端白名单枚举绝不直接接收前端传参。其他地方一律用#{}。3.4 动态SQL在商品搜索中的应用商品列表页的搜索筛选是这个项目里比较有代表性的MyBatis用法。用户可以通过关键字搜宠物名字、按分类筛选、按价格区间过滤、按销量或价格排序。这些筛选条件不是固定的用户可能选其中几个也可能一个都不选。这种场景用MyBatis的动态SQL来实现非常顺手。我写Mapper的XML如下select idselectPetList resultTypecom.example.entity.Pet SELECT * FROM pet where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /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 choose when testsort salessales DESC/when when testsort price_ascprice ASC/when when testsort price_descprice DESC/when otherwisecreate_time DESC/otherwise /choose /selectwhere标签会自动处理掉第一个条件前面的AND这个设计非常贴心。排序字段我用choose做了白名单映射传进来的sort参数不管是什么值最终拼接的SQL都是固定几个选项之一安全性和灵活性都有了。3.5 事务管理在订单创建中的重要性创建订单这个接口涉及多张表的写操作必须保证要么全部成功要么全部失败校验商品库存是否充足。插入订单主记录。插入订单明细记录。扣减宠物商品库存。清空该用户的购物车相关记录。这四个步骤跨四张表任何一步失败都要整体回滚否则就会出现库存扣了但订单没生成、或者订单生成了但库存没扣的数据不一致问题。解决办法就是给Service方法加上Transactional注解交给Spring统一管理事务。这里有一个很多人会忽略的坑事务注解默认只在抛RuntimeException时回滚如果代码里捕获了异常没有往外抛事务是不会回滚的。所以我在写业务代码时故意不做多余的try-catch让异常一层层抛出去由全局异常处理器统一处理并返回给前端。3.6 MyBatis缓存使用心得MyBatis自带一级缓存和二级缓存一级缓存默认开启作用范围是同一个SqlSession对象。但实际开发中SpringBoot整合MyBatis后每个Mapper方法都可能是不同的SqlSession所以一级缓存基本等于摆设。二级缓存默认关闭作用在Mapper namespace级别。我当时查了宠物商品列表因为商品信息变动不频繁就想着开启二级缓存试试效果。配置很简单在Mapper XML文件顶部加上cache/标签就行。但后来发现有个问题商品上下架操作不走缓存导致页面还显示已下架商品。因为缓存的生命周期和数据库操作不是实时同步的任何针对商品表的写操作都需要显式清缓存。小项目数据量不大、并发不高我后来直接把二级缓存关了只用MySQL的查询能力性能完全够用。所以我的建议是多表关联查询场景尽量不用MyBatis二级缓存缓存一致性维护太麻烦收益却不明显。4. Vue前端搭建与核心页面实现4.1 Vue环境准备与项目初始化前端部分我用的是Vue 2 Element UI这套组合。虽然Vue 3已经是主流但Element UI对Vue 2的生态最成熟网上资料也多做一个管理后台加商城前台完全够用关键是出问题好排查。环境方面首先需要安装Node.js我用的版本是16.x。Node自带npmVue CLI通过npm全局安装npm install -g vue/cli vue --version安装完脚手架之后创建项目vue create pet-shop-frontend选择Manually select features勾选Router和VuexCSS预处理选Less或者不装都可以。我这边还安装了Axios用于HTTP请求、Element UI作为UI组件库。4.2 前端路由与页面结构项目采用的是前端路由控制页面跳转我规划了这些页面首页/轮播图、热门宠物推荐、分类入口。宠物列表页/pet/list搜索、筛选、排序、分页。宠物详情页/pet/detail/:id商品大图、价格、库存、加入购物车按钮。购物车页/cart勾选商品、修改数量、结算跳转。订单确认页/order/confirm填写收货地址、确认金额。我的订单页/order/list订单列表查看不同状态的订单。登录页/login和注册页/register。管理后台/admin商品管理、分类管理、订单管理。路由守卫是前端鉴权的重要一步。在Vue Router的全局前置守卫里我判断了要访问的页面是否需要登录如果需要但Token不存在就强制跳转到登录页同时把来源路由记录下来登录成功后再跳回来。管理后台的路由还会额外校验用户角色不是admin就拒绝访问。4.3 Axios封装与Token处理前后端分离项目中前端对Token的处理直接关系到整个系统的登录体验我在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 }) // 响应拦截器统一处理返回数据 request.interceptors.response.use(response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(登录已过期)) } return res }, error { return Promise.reject(error) }) export default request这里有两个值得注意的地方。第一请求拦截器统一从localStorage取Token并塞进请求头这样页面代码里不用每次手动携带Token。第二响应拦截器里遇到401统一做退出处理后端返回这个状态码代表Token无效或过期前端直接清掉本地Token并跳回登录页。4.4 商城核心页面逻辑实现宠物列表页是这个项目前端比较有代表性的一个页面筛选条件多、URL参数需要同步。我用了Vue的watch监听路由变化筛选条件改变时通过this.$router.push更新URL的query参数然后根据新的参数重新请求数据。这样用户体验比较好刷新页面后筛选条件还在也能把当前筛选状态分享给朋友。宠物详情页主要处理商品数量选择和加入购物车逻辑。加入购物车时前端需要带上用户Token所以这个接口是走Axios拦截器自动带Token的。后端校验用户登录后把商品和数量插入购物车表。购物车页面我做了“选中计算合计”功能勾选商品后才把购物车条目ID传给后端生成订单。这里前端主要是在维护一个选中集合因为比较简单我用Vuex来管理购物车状态方便多个页面共享数据和同步更新。5. 打包部署全流程与踩坑记录5.1 后端项目打包与启动后端打包非常简单用Maven执行package命令即可mvn clean package -DskipTests打包完成之后在target目录下会生成一个Jar文件。生产环境启动命令我一般这样写nohup java -jar pet-shop-backend.jar --server.port9090 app.log 21 这里有个细节我把数据库连接信息、JWT密钥这些环境相关的配置抽离到application-prod.yml中启动时通过--spring.profiles.activeprod指定运行环境。这样开发环境、测试环境、生产环境的配置互不干扰。5.2 前端构建与Nginx配置前端部署的第一步是先构建产物npm run build构建完成后dist目录下就是纯静态文件里面包含HTML、CSS、JS全部扔给Nginx托管即可。这里最核心的问题就是跨域怎么处理。开发环境我用的是Vue CLI自带的代理在vue.config.js里配置/api的代理指向后端地址。但生产环境部署后如果后端接口域名和前端静态页面域名不一致还是要解决跨域。我的做法是在Nginx层做反向代理把/api开头的请求转发到后端Jar服务server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里try_files是SPA部署最关键的一行配置。Vue用的history模式路由切换时不会真的请求服务器路径。如果用户直接访问http://your-domain.com/pet/detail/3Nginx需要在磁盘上找不到对应文件时把请求重写到index.html让Vue Router接管。如果不写这行配置前端构建后本地跑没任何问题一部署到服务器上用户一刷新子页面就报404错误这个我当初排查了挺久。5.3 本地环境和服务器环境部署的差别本地开发的时候后端用IDEA启动前端用npm run serve启动两边通过代理通信非常方便。但部署到服务器之后有几个问题必须额外注意服务器防火墙要放行Nginx的80端口后端的9090端口不要对外开放只允许Nginx本地转发访问。MySQL数据库密码不要用弱密码JWT密钥要换成一串随机字符串不要用代码里写死的默认值。上传的商品图片要放到服务器固定的目录下Nginx单独配置一个/images路径映射到这个目录否则图片加载不出来。Jar启动时建议加-Xms512m -Xmx512m限制内存防止个人服务器内存不足被直接拖垮。5.4 MySQL安装与数据库初始化如果你是在本地新装MySQL我建议用官方安装包走标准流程步骤不复杂下载对应系统的安装包一路Next设置root密码装完后用Navicat或者MySQL Workbench连接验证一下。关键点在字符集设置一定要选utf8mb4否则存宠物介绍里的表情符号会出现乱码。数据库初始化我写了一个init.sql脚本里面包含建库、建表、插入初始分类数据和管理员账号的操作。第一次部署的时候直接执行这个脚本即可mysql -u root -p init.sql5.5 前端打包后布局异常问题有一个非常经典的问题前端本地开发时页面布局正常执行npm run build后用Nginx部署发现页面样式乱了、图片不加载、路由访问404这是把Vue项目部署到子路径导致的资源路径问题。解决办法是在vue.config.js里设置publicPath。如果你把前端部署在域名根路径下设置为/即可如果部署在http://your-domain.com/pet-shop/这种子路径下就要设置为process.env.NODE_ENV production ? /pet-shop/ : /。我这里直接部署在根路径下所以问题不大但如果你用宝塔面板或者云服务器默认目录很容易踩到这个坑。另外还有一点如果构建后发现首页能打开但图片404优先检查打包产物里静态资源的引用路径是不是以绝对路径开头确认publicPath配置。6. 常见问题速查与避坑心得6.1 高频报错与解决方案汇总开发过程中我总结了一张问题排查表这里直接分享出来遇到类似问题可以快速对照排查。问题现象大概率原因解决办法前端请求后端一直404后端接口路径和前端请求路径不一致用浏览器F12看NetWork面板对比请求URL和后端Controller的映射路径页面加载正常但数据都是nullMyBatis驼峰映射没开启在application.yml配置map-underscore-to-camel-case: true添加购物车提示未登录A请求没带上Token检查Axios拦截器是否把Token塞进请求头检查后端拦截器是否放行了该接口跨域请求被浏览器拦截前后端端口不同且未配置CORS开发环境用Vue代理生产环境用Nginx反向代理部署后刷新页面404Nginx没配置try_files在location / 配置try_files $uri $uri/ /index.html;打包后样式丢失静态资源路径错误检查vue.config.js的publicPath配置MySQL连接报Public Key Retrieval异常连接URL缺少allowPublicKeyRetrieval参数MySQL 8.x连接串加allowPublicKeyRetrievaltrueuseSSLfalse订单创建成功但库存没扣事务没有生效检查Service方法是否加了Transactional异常被catch后没有抛出就不会回滚Token过期后页面还在操作前端没有统一处理401状态码响应拦截器遇到401时清Token、跳登录页6.2 关于SpringBoot版本选择关于SpringBoot版本我见过太多人在这个上面卡住。现在网上很多教程直接用IntelliJ IDEA默认选的SpringBoot 3.x然后配合JDK 8开发结果启动直接报错提示JDK版本不支持。SpringBoot 3.x要求JDK 17及以上这是一个硬性门槛。如果你本地装的是JDK 8就直接在创建项目时把SpringBoot版本改成2.7.x。如果你要用JDK 17那SpringBoot 2.7.x也能跑。除非你需要用到SpringBoot 3的新特性比如GraalVM支持、Jakarta EE命名空间否则新项目用2.7.x稳定不折腾。另外版本太高还有一个问题就是很多老一点的第三方组件没有适配。比如我这次用的jwt依赖SpringBoot 3.x下的坐标都变了从javax.servlet变成了jakarta.servlet对不熟悉这套迁移的人来说就是白折腾。6.3 实际开发时是否需要写这么多手写SQL有朋友看完代码之后问我MyBatis里SQL都是手写的实际开发时是不是真的都这样写不会觉得麻烦吗我的看法是MyBatis的手写SQL反而是它的核心优势。像商品列表这种多条件动态查询手写SQL可以精确控制每一个查询条件性能可以针对索引做专门的优化。你如果用JPA复杂查询反而要花很多精力去理解框架的自动生成逻辑出了问题很不好排查。实际工作中我见过很多团队用MyBatis-Plus这种增强框架简单的增删改查可以调用现成方法复杂查询还是手写XML两边互补。6.4 如果把项目扩展到生产环境还需要做什么现在这个项目跑通完整流程完全没问题但如果真的要面向真实用户开放访问还有几件事是必须补上的使用HTTPS证书防止数据传输过程被窃听和篡改。引入Redis缓存热点商品数据和验证码减轻数据库压力。使用消息队列处理订单超时取消业务比如用户下单后30分钟未支付自动取消。引入短信或邮件服务做注册验证和订单通知。图片上传对接云存储而不是放在本地磁盘。日志用统一格式采集方便排查线上问题。这些属于“从能跑变成能商用”的跨档升级思路是清晰了每一块做起来又是一堆实践细节。7. 部署上线后的一些实践体会项目部署到服务器正常运行之后我自己又反复走了一遍核心流程注册新用户、浏览商品、搜索筛选、加入购物车、提交订单、管理后台发布新商品、修改库存状态。整套流程跑通的时候确实有一种“原来一个线上系统是这么转起来的”的完整感。这个过程中我个人体会最深的是两件事。第一前后端分离的难点从来不在某个技术本身而在于两端怎么配合接口字段怎么约定、状态码怎么统一、登录状态怎么维持、错误信息怎么透传这些沟通成本才是真正需要花心思的。如果你的接口返回值前段和后端各定各的联调的时候就会非常痛苦。第二数据库设计直接决定后面写代码的顺不顺。我在做订单查询的时候想过做多表联查后来发现把常用的关联字段快照存到订单表里查询逻辑一下子简化为单表查询性能也不差。冗余设计的思路在这个项目里体现得非常充分。最后再分享一个小技巧如果你打算把这个宠物商城项目作为毕设或者面试项目强烈建议在README文档里把系统架构图画清楚、把接口文档整理规范再写一份部署说明。很多面试官问项目一上来就是“这个项目怎么部署的”、“数据库怎么设计的”、“遇到过什么难点怎么解决的”。你这个项目如果这三问都能答上来其实已经比很多只跑通Demo的人强出一大截了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询