
做扶贫助农类系统我一直觉得技术栈的选择要比功能设计更早敲定因为后面所有模块都建立在选型之上。这个项目用的是 Java SpringBoot Vue3 MyBatis MySQL典型的前后端分离架构既兼顾了后端生态的成熟稳定又让前端开发足够灵活。整套系统源码包含用户端、农户端和管理端三个维度核心围绕农产品展示、助农订单、扶贫项目跟踪这些业务展开。适合正在做毕业设计、接外包项目或者刚转前后端分离开发的同学参考尤其是想了解 SpringBoot 配合 Vue3 怎么组织接口、怎么处理跨域、怎么打包部署的这篇能给你一条清晰可落地的路径。我实际把这个项目完整搭过一遍从数据库建表到后端接口再到 Vue3 页面联调踩了不少坑。这篇文章会按项目设计、数据库、后端、前端、部署排查五个部分讲尽量把关键步骤和参数选择都写清楚而不是只给一个骨架让你自己猜。1. 项目整体设计与技术选型1.1 为什么是 SpringBoot Vue3 MyBatis MySQL先说后端。SpringBoot 在这个项目里属于“理所当然”的选择不是因为它最先进而是因为它最适合这种快速落地、业务相对标准的系统。SpringBoot 的自动配置能省掉大量 XML 配置内嵌 Tomcat 让项目打成 Jar 包就能跑不需要额外装容器。对扶贫助农这种以 CRUD 为主的业务系统来说SpringBoot 的稳定性、社区资料成熟度以及和 MyBatis、MySQL 的配合程度都足够稳妥。MyBatis 是我个人比较偏好的持久层框架因为它把 SQL 控制权完全交给你。SpringBoot 官方推荐的 Spring Data JPA 虽然省事但在多表联查、复杂统计、动态条件查询上反而不如 MyBatis 直观。扶贫助农系统里有一个很典型的场景农产品列表需要按分类、产地、价格区间、上架状态多个条件组合筛选用 MyBatis 的动态 SQL 可以很自然地拼出条件语句每条 SQL 都是自己写的性能好坏心里有数。Vue3 是前端的核心。相比 Vue2Vue3 的组合式 API 在逻辑复用上明显更好一个商品列表页的搜索条件、分页状态、接口返回数据处理可以按功能拆成多个 composable 函数而不是把所有逻辑堆在 data 和 methods 里。加上 Vite 的开发服务器启动速度非常快改完代码热更新基本是毫秒级反馈开发体验比 Webpack 时代的 Vue2 舒服很多。MySQL 的选择不需要过多解释开源、免费、资料多5.7 和 8.0 都能很好支持这个项目。需要注意的是如果本地装的是 8.0驱动和连接参数要和 5.7 区分开这一点后文部署部分我会单独讲。1.2 项目模块划分与整体结构我设计这个项目时没有拆微服务而是保持单应用多模块的形态。后端拆成 controller、service、mapper、entity、common 这几个包前端按页面功能拆成 views、components、api、router、stores。前后端完全分离后端只提供 JSON 接口前端负责页面渲染和交互。优势在于分工明确以后想加个移动端 H5 或者小程序后端接口可以直接复用不用重新写业务逻辑。后端目录结构大致是这样src/main/java/com/example/farmhelp/ ├── common // 通用返回结果、异常处理、工具类 ├── config // 跨域配置、拦截器配置、WebMvc配置 ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // MyBatis Mapper接口 ├── entity // 数据库实体类 ├── dto // 前端传输对象 └── FarmHelpApplication.java前端目录结构src/ ├── api // 接口请求封装 ├── assets // 静态资源 ├── components // 通用组件 ├── router // 路由配置 ├── stores // Pinia状态管理 ├── views // 页面组件 │ ├── admin // 管理端 │ ├── farmer // 农户端 │ └── user // 用户端 ├── utils // 工具函数 └── App.vue这个划分的核心理念是前端按角色分目录后端按技术层次分目录。前端按角色分的好处是管理后台和用户商城页面逻辑完全不同拆开后各自维护不干扰。后端按层次分则是为了遵守单一职责controller 只做参数接收和结果返回service 只写业务逻辑mapper 只写数据库操作这样无论是排查问题还是后续扩展都比较容易。2. 数据库设计与核心表结构2.1 业务核心表设计思路扶贫助农系统的数据模型核心要覆盖用户、农产品、订单、助农活动、帮扶项目这几类业务。我建表的时候遵循一个原则能拆的表不要合并能加冗余字段的不要频繁联表。这句话听起来有点矛盾但实际做下来是有先后顺序的优先保证业务边界清晰在查询频率高的字段上做适度冗余。用户表是系统的基础。系统里有三种角色普通用户购买者、农户卖家、管理员。可以在用户表加一个 role 字段区分也可以拆成三张表。我采用的是单表加 role 字段因为三个角色的公共字段很多都是昵称、手机号、密码、头像、地址这些强行拆表反而增加关联查询。核心字段如下字段名类型说明idbigint主键自增usernamevarchar(50)登录账号passwordvarchar(100)BCrypt加密后的密码nicknamevarchar(50)昵称phonevarchar(20)手机号roletinyint1用户 2农户 3管理员create_timedatetime创建时间农产品表是商品模块的核心。这里要注意的字段是 status代表上架和下架状态。很多新手会把这个状态放在前端用布尔值直接传但更稳妥的做法是后端统一根据 status 过滤防止用户直接调用接口下架别人的商品或者看到未上架的商品。农产品表字段包括商品名称、分类、产地、价格、库存、图片、描述、农户ID、状态。其中 price 字段我建议用 decimal(10,2)不要用 double避免金额计算精度问题。订单表需要同时关联用户表和农产品表但为了查询方便我在订单表里冗余了商品名称、商品图片、商品单价这几个字段。理由是订单生成后商品可能改价、改名甚至下架如果订单里不冗余这些信息用户查看历史订单时就要去关联商品表一旦商品被删除订单详情直接报错。这种冗余在电商类系统里非常常见不是在设计上偷懒而是保证历史数据的稳定性。助农活动表和帮扶项目表是系统区别于普通商城的地方。助农活动表记录活动的标题、时间、地点、参与人数、活动描述帮扶项目表记录项目名称、帮扶对象、进展情况、资金使用情况。这两块业务不需要特别复杂的设计但要注意管理员审核状态避免未审核的活动直接展示给用户。2.2 MyBatis 映射与动态 SQL 实战数据库表建好后MyBatis 层要做的是把实体类和数据表对应起来。我习惯用 XML 文件写 Mapper而不是注解方式。原因很简单动态 SQL 在 XML 里可读性强得多尤其是条件多的时候注解方式拼字符串能让人看到崩溃。商品列表查询就是典型的多条件动态 SQL。用户可能按分类筛选可能按价格区间筛选可能只看了某个产地也可能搜索商品名称关键字。Mapper 接口定义如下ListProductVO getProductList(Param(categoryId) Long categoryId, Param(minPrice) BigDecimal minPrice, Param(maxPrice) BigDecimal maxPrice, Param(origin) String origin, Param(keyword) String keyword, Param(offset) int offset, Param(pageSize) int pageSize);对应的 XML 里面用where标签动态拼接条件select idgetProductList resultTypecom.example.farmhelp.dto.ProductVO select id, name, category_id, origin, price, stock, image, description, farmer_id, status from product where status 1 if testcategoryId ! null and category_id #{categoryId} /if if testminPrice ! null and price gt; #{minPrice} /if if testmaxPrice ! null and price lt; #{maxPrice} /if if testorigin ! null and origin ! and origin like concat(%, #{origin}, %) /if if testkeyword ! null and keyword ! and name like concat(%, #{keyword}, %) /if /where order by create_time desc limit #{offset}, #{pageSize} /select这里有几个关键点。第一status 1直接写在where内作为恒定条件防止用户传入异常参数。第二gt;是 SQL 中小于等于的正常写法因为 XML 里会被解析成标签开头必须转义。第三分页用的是 MySQL 的 limit 语法offset 从 0 开始前端传 pageNum 时记得减一。第四like concat(%, #{keyword}, %)这种方式可以避免 SQL 注入比直接拼接字符串安全得多。我实际开发中遇到过一个问题如果多个条件都是空的整条 SQL 会变成select ... from product where status 1 order by ...这是正确的。但如果用了trim prefixWHERE prefixOverridesAND |OR并且前面还自己加了 status 条件就很容易拼出where and status 1这种报错。新手建议直接用where标签它自动处理开头的 AND 和 OR比 trim 更省心。3. 后端接口开发与业务实现3.1 SpringBoot 项目搭建与核心配置创建 SpringBoot 工程我一般从 Spring Initializr 生成基础骨架选 Java 8 或 Java 11SpringBoot 版本我用 2.7.x。这里提醒一点SpringBoot 3.x 对 Java 版本要求是 17 以上如果你本地环境是 JDK 8不要强行用 3.x不然编译直接失败。项目依赖需要加入 spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok、spring-boot-starter-validation 这几个。pom.xml 里 MyBatis 的依赖坐标我踩过坑早期用的是mybatis-spring-boot-starter而不是mybatis-spring-boot-startercore 版本和 starter 版本不一致会导致 mapper 扫描失败。推荐用org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.2搭配 SpringBoot 2.7.x这个组合我实测没有问题。application.yml 配置里数据库连接部分是最容易出错的地方server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/farmhelp?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.farmhelp.entity configuration: map-underscore-to-camel-case: truedriver-class-name 在 MySQL 5.7 和 8.0 下写法不一样。5.7 用com.mysql.jdbc.Driver8.0 用com.mysql.cj.jdbc.Driver。url 里的serverTimezoneAsia/Shanghai必须加不然连 MySQL 8.0 会报时区错误。useSSLfalse是因为本机开发环境一般没有配置 SSL 证书不关掉会连不上。map-underscore-to-camel-case: true这个配置很关键它能自动把数据库的create_time映射成实体的createTime不用每个字段都写 TableField。3.2 用户认证与权限控制实现系统里用户、农户、管理员三种角色接口必须区分权限。最简单的做法是 JWT 拦截器。JWT 生成的 token 放在请求头 Authorization 里每次请求经过拦截器校验再把用户信息放入 ThreadLocal 供业务代码取用。JWT 相关代码不复杂核心是生成和解析。我用的是 jjwt 库0.9.1 版本对应 Java 8 比较稳定。工具类里包含生成 token、解析 token、判断过期三个方法public class JwtUtils { // 密钥实际开发从配置中心读取 private static final String SECRET farmhelp-secret-key-2024; // 过期时间单位毫秒 private static final long EXPIRE 7 * 24 * 60 * 60 * 1000; public static String generateToken(Long userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }拦截器的核心逻辑是从请求头取 token解析失败返回 401解析成功放行。但要注意放行名单的问题登录接口、注册接口、商品列表接口、商品详情接口这些是公开接口用户未登录也能访问。我维护了一个数组把不需要认证的路径放进去拦截器里先判断当前请求路径是否匹配匹配就直接放行。权限控制只用拦截器还不够因为拦截器只能判断“是否登录”判断不了“是否是管理员”。比如后台管理接口只能由管理员调用农户只能操作自己的商品。我是这样处理的在后端接口方法上自定义一个RequireRole(admin)注解拦截器解析 token 里的角色再结合方法的注解判断是否有权访问。这个思路简单效果也不错比引入 Spring Security 全家桶轻量很多。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String value() default ; }使用方式RequireRole(admin) GetMapping(/admin/activity/list) public Result getActivityList() { ... }拦截器里通过 HandlerMethod 拿到方法上的注解对比当前用户角色不一致就返回 403。3.3 核心业务接口实现这个系统的核心业务是农产品购买流程从浏览商品到提交订单再到农户处理订单。我按照这个链路一个一个说。商品列表接口是第一步。前端传分页参数和筛选条件后端调用 Mapper 查询记录同时还要查询总数这样才能给前端返回 total 做分页展示。返回的数据我用一个 PageResult 对象包装包含 list、total、pageNum、pageSize 四个字段。这里有一个设计细节分页查询最好在 service 层做两次查询一次查列表一次查 count不要用 MyBatis 的 PageHelper 插件。PageHelper 用起来方便但容易在复杂 SQL 下统计出错误数量而且多人协作时谁忘了配置类型拦截器就乱了。自己写 limit 也不复杂可控性更好。商品详情接口要注意商品表里的 farmer_id 关联了用户表但用户表只存了用户名和电话没有农户介绍。我是在设计时干脆在 product 表冗余了一个 farmer_name 字段这样详情接口直接查商品表就能展示不用再 join 用户表。产品上架时由前端填好后端存进去。订单提交是重点。流程是前端把商品 id、购买数量、收货地址传到后端后端先查商品是否存在、是否上架、库存是否充足然后计算总价生成订单。这里要特别强调库存扣减的顺序和方式。我在 product 表里设了一个 stock 字段扣库存时用了乐观锁的思路SQL 语句是update product set stock stock - #{count} where id #{productId} and stock #{count}通过stock #{count}条件如果库存不足这条 SQL 更新的行数为 0代码就能判断出库存不足返回给前端。这种方式比先查库存再更新的方式更安全可以避免两个请求同时读到相同库存导致超卖。订单状态我用一个 int 字段 status 表示0待付款、1待发货、2已发货、3已完成、4已取消。前端和接口里都写清楚状态对应的值不要把状态数字直接展示给用户要映射成文案。这个映射逻辑放在前端 utils 里写一个函数就行。助农活动模块相对简单管理端可以发布活动用户端可以查看列表和详情。我加了一个报名功能也就是活动报名表 activity_apply字段包括用户ID、活动ID、报名时间。这样后台可以看到每场活动的报名人数比单纯一个活动表更有实际意义。4. 前端 Vue3 项目开发与前后端联调4.1 Vue3 项目初始化与工程配置前端我用 Vite 创建项目执行npm create vitelatest farmhelp-web -- --template vue就行。创建完成后进入目录安装依赖。有一点要提前说Vite 要求 Node.js 版本在 14.18 以上推荐用 Node 16 或 18不然启动会报错。项目里我配置了路径别名把src指向这样组件里引用文件可以写/api/product.js而不是一长串相对路径。vite.config.js 里这样配置import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } }, server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })server.proxy 是开发环境的跨域解决方案。前端运行在 5173 端口后端运行在 8080 端口如果前端直接请求http://localhost:8080/api/product/list浏览器会因为跨域限制拦截响应。通过 proxy前端代码里请求/api/product/listVite 会把请求转发到http://localhost:8080这样浏览器看到的请求是同源的就避开了跨域。生产环境我部署时会用 Nginx 反向代理后面再讲。Vue3 应用还需要路由和状态管理。路由我用 vue-router 4状态管理用 Pinia。Pinia 是 Vue3 官方推荐的状态管理库比 Vuex 更简洁一个 defineStore 就能定义一个 store。这个项目里我主要用 Pinia 存用户登录状态和购物车数据。4.2 Axios 请求封装与前后端联调细节前端所有请求都通过 axios 发送但我不会在每个页面里直接调用 axios而是封装一层 request 工具。核心作用是统一设置请求头、处理响应状态码、统一错误提示。request.js 核心代码import axios from axios import { ElMessage } from element-plus 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 token } return config }) // 响应拦截器统一处理后端返回结果 request.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } else if (res.code 401) { ElMessage.error(登录已过期请重新登录) router.push(/login) return Promise.reject(new Error(未授权)) } else { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default request这里把 baseURL 设成/api配合 Vite 的 proxy 正好。后端接口 Controller 的 RequestMapping 我也统一加了个/api前缀这样生产和开发环境都不需要用完整的 URL只写相对路径就行。接口 api 层的封装比如 product.jsimport request from /utils/request export function getProductList(params) { return request({ url: /product/list, method: get, params }) } export function getProductDetail(id) { return request({ url: /product/${id}, method: get }) }这样页面里调用getProductList(params)返回的是一个 Promise.then 里拿到的就是后端返回的数据部分非常干净。4.3 核心页面实现与组件复用商城页面按功能拆成四个部分商品列表、商品详情、购物车、订单管理和后台管理。前端页面多但真正核心的只有商品列表和商品详情两个购物车和订单可以复用列表页的组件思路。商品列表页我用了 Element Plus 的 el-card 和 el-pagination 组件。筛选区是分类下拉框、价格输入框、关键词输入框加上一个搜索按钮。每次点击搜索或者切换分页都调用fetchList()方法从接口拿最新数据。搜索条件用 reactive 对象存起来const queryParams reactive({ categoryId: null, minPrice: null, maxPrice: null, keyword: , pageNum: 1, pageSize: 12 })这里要特别注意分页组件触发 pageNum 改变时queryParams 对象要更新后再请求不能在请求里直接写死 pageNum。我踩过坑el-pagination 的 current-page 和页面里的 queryParams.pageNum 没绑定好导致点击第二页后数据还是第一页。正确做法是给 el-pagination 加v-model:current-pagequeryParams.pageNum并监听 current-change 事件调用 fetchList。商品详情页有一个关键逻辑加入购物车时要把商品 id、数量、单价传给购物车 store。我在 Pinia 里定义了一个 cart storeexport const useCartStore defineStore(cart, { state: () ({ items: [] }), actions: { addItem(product, count) { const existing this.items.find(item item.productId product.id) if (existing) { existing.count count } else { this.items.push({ productId: product.id, name: product.name, price: product.price, image: product.image, count }) } }, removeItem(productId) { this.items this.items.filter(item item.productId ! productId) } } })购物车数据刷新页面会丢失这个我接受因为目前项目不要求持久化购物车。如果要改进可以把 items 序列化到 localStorage下次进入页面时重新加载。后台管理页面是另一个前端子模块用 Vue Router 的嵌套路由实现侧边栏布局。左边菜单是商品管理、订单管理、活动管理、用户管理右边是对应的页面内容。这里的页面基本都是 el-table 加操作按钮整体比商城前端简单但要注意管理员接口的权限控制前端只是隐藏按钮后端还必须校验角色否则有人绕过页面直接调接口就能删除商品。5. 部署上线与常见问题排查5.1 打包部署流程前后端分离项目的部署比传统单体项目稍微多一点步骤但思路很清晰。前端打包成静态文件后端打成 Jar 包再用 Nginx 把两者结合起来。前端打包执行npm run build生成 dist 目录。这里面有几个坑Vite 默认的资源路径是绝对路径/assets/xxx如果你的项目要部署在子目录下就要在 vite.config.js 里设置base: ./改成相对路径否则资源加载 404。另外打包前要把 request.js 里的 baseURL 确认好开发环境是/api生产环境如果 Nginx 也配置了/api代理转发那就不需要改代码。后端打包执行mvn clean package -DskipTests生成 jar 文件。直接运行java -jar farmhelp-0.0.1-SNAPSHOT.jar生产环境建议写一个启动脚本加上 JVM 参数比如设置内存上下限java -Xms256m -Xmx512m -jar farmhelp.jar --spring.profiles.activeprodNginx 配置是实现前后端整合的关键。我的做法是前端 dist 文件放到 Nginx 的 html 目录/路径指向前端/api路径反向代理到后端 8080 端口。配置片段server { listen 80; server_name yourdomain.com; location / { root html/farmhelp/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html这行很关键它是 Vue Router 的 history 模式必备配置。如果不加访问/product/1这类路由时Nginx 找不到对应的物理文件会返回 404。加上这行后所有路径都会尝试交给 index.html由前端路由自己处理。如果你不想折腾这行配置可以在前端路由里改成 hash 模式URL 上会多一个#但部署省事很多。数据库方面生产环境初始化我推荐这样做在本地建好所有表、写入初始管理员账号然后用 mysqldump 导出 SQL 文件再到服务器上执行导入。不要只建表不导数据要保证 code 里初始化的 zip 文件直接可用。5.2 常见问题排查速查我在实际部署和联调过程中遇到过不少问题整理成一张表都是高频雷区。现象原因解决方案前端请求接口报 404后端接口路径没有/api前缀或者 Nginx 代理配置不匹配确认 Controller 的 RequestMapping 与前端 baseURL 一致前端请求接口报 504 或超时后端服务未启动或者端口被占用检查 8080 端口是否被监听netstat -ano数据库连接报 Public Key Retrieval is not allowedMySQL 8.0 的 caching_sha2_password 认证方式导致JDBC URL 加allowPublicKeyRetrievaltrue插入中文数据变成 ?数据库表字符集不是 utf8mb4建库时指定DEFAULT CHARSETutf8mb4连接参数加characterEncodingutf8MyBatis 报 Invalid bound statementmapper 接口和 XML 文件不匹配或者路径配置错误检查 XML namespace 是否为接口全限定名mapper-locations 是否匹配 classpath后端返回 JSON 中字段为 null实体属性和数据表字段名不对应开启map-underscore-to-camel-case或使用 TableField 映射Vue3 项目 npm run dev 启动失败Node 版本过低或者依赖安装不完整升级 Node 到 16删除 node_modules 后重新 npm install登录后访问管理接口返回 403后端权限校验拦截器未通过角色不对检查用户表 role 字段管理账号是否确实是 3打包后前端页面白屏静态资源路径错误vite.config.js 设置base: ./检查 dist 目录 index.html 中 script src 路径这十个问题覆盖了从环境、编码、路径、权限到数据库的常见坑可以做一个排查手册用。5.3 关于缓存与账号安全的两个小提示第一次上线时我忽略了缓存结果吃了大亏。后端项目更新后前端页面显示的仍是旧数据怎么刷新都没用。原因很简单SpringBoot 默认不会主动清理查询缓存而 MyBatis 的一级缓存是 SqlSession 级别的一个请求结束就清空不会出问题。但如果用了 MyBatis 的二级缓存或者在前端加了 Service Worker 缓存就会出现数据不一致。建议是生产环境不要开 MyBatis 二级缓存保持“每次查询都走数据库”的朴素策略等业务真到了需要缓存的规模再去考虑 Redis。账号安全这块密码不要明文存储。我用的是 Spring Security 自带的 BCryptPasswordEncoder保存用户时加密登录时用 matches 校验。有些开源项目为了省事直接存 MD5这其实是给黑客送漏洞防君子不防小人。BCrypt 每次加密结果都不同即使两个用户密码相同密文也不同安全性远高于 MD5。登录接口加一个简单的速率限制比如同一 IP 一分钟内连续失败 5 次就暂时锁定也能防止暴力破解。一点自己的体会做完这个项目我最想强调的不是某个技术点而是“前后端分离”这四个字的真正边界。很多新手容易把后端接口写得像前端页面或者在 Vue 组件里直接拼 SQL这些都是灾难。后端只做好数据校验、权限控制和业务处理前端只负责交互和展示接口约定好统一返回格式{ code, msg, data }这个系统就成功了一大半。如果你打算在这个项目基础上继续扩展可以考虑把订单队列改成 RabbitMQ 异步处理把商品图片换成对象存储把前后端都加一层 Dockerfile 方便自动化部署。方向很多但每一步都要建立在当前架构清晰的基础上。希望这篇内容能帮你少踩几个坑。