☰
校园便利平台系统开发实战:SpringBoot+Vue3前后端分离全流程解析
2026/10/15 4:45:39 网站建设 项目流程

校园里的快递代取、二手教材流转、临时拼单,这些需求一直存在,但真正做成系统的人不多。我在做这个“校园便利平台系统”时,最初的目标很朴素:把信息发布、交易撮合、订单跟进这三件事在一个前后端分离的Web系统里跑通。技术栈用的是 Java SpringBoot + Vue3 + MyBatis + MySQL,这也是目前中小型Web项目里非常常见的一套组合。这篇文章从需求拆解讲起,一直到数据库建模、后端接口、前端页面、部署上线,把整个过程中的关键决策和踩坑点都交代清楚。如果你正在做课程设计、毕业设计,或者想找一个能完整运行的SpringBoot+Vue3案例来学框架,这份拆解应该能帮你少走不少弯路。

1. 需求拆解:一个校园便利平台的核心闭环是什么

1.1 目标用户和角色边界

做项目第一步不是写代码,而是搞清楚谁会用它。校园便利平台的目标用户很明确:在校学生。场景也相对固定:有人快递到了没空取,有人二手教材想转手,有人想找人拼单买水果,还有人需要跑腿代办。这些东西如果靠微信群接龙,信息很快会被刷掉,也没有状态跟踪,所以需要一个集中式平台来承接。

角色方面,我一开始想得很复杂,又是商家又是骑手又是运营。最后砍成两个角色:普通用户和管理员。普通用户既能发布商品或服务,也能浏览、下单、接单;管理员负责审核商品、管理用户和查看基础统计。权限控制在RBAC模型里就是两张表的事,没必要上Spring Security那套重型方案,用拦截器加自定义注解就够了。角色边界越清晰,后面写权限判断就越省心。

1.2 功能边界的取舍:先跑通三个主流程

很多学生项目失败,不是因为功能太少,而是因为功能太多。我第一版列了购物车、收藏、即时聊天、积分商城、拼团、朋友圈分享,最后全部砍掉,只保留三条核心链路:

  • 注册登录链路:注册、登录、退出、个人资料查看修改。
  • 交易链路:发布商品/服务、浏览详情、搜索分页、下单、接单、完成、取消。
  • 管理链路:用户禁用、商品下架、订单状态查看。

为什么砍掉即时聊天?因为第一版要实现会多出一个WebSocket模块和消息推送体系,成本不小。实际业务里用户对“看对方联系方式然后线下沟通”这件事接受度非常高,所以我只在订单详情里放了匿名化后的微信号或手机号占位字段,让双方线下联系。这个取舍让整个项目少了一半工作量。

支付模块也是同理。学生之间的交易大多是几块钱到几十块钱,校内场景天然信任成本低,没必要对接支付宝、微信支付,涉及商户资质和合规流程,个人项目根本跑不下来。我做成“线上确认状态、线下完成交付”的模式。这个决策既是技术取舍,也是为了不给自己挖合规的坑。

1.3 非功能需求提前定清楚

非功能需求不写下来,到后期一定会打架。我给自己定了三条硬指标:

  • 数据安全:密码必须加盐哈希,绝对不存明文;所有SQL用预编译占位符,防SQL注入;接口返回不允许暴露密码字段。
  • 性能底线:虽然校园项目并发量不会太高,但列表页必须分页,核心查询字段必须走索引,接口响应时间控制在100ms以内。
  • 移动端优先:学生大部分时间用手机访问,前端页面不能只适配桌面浏览器,响应式布局必须做。

这些指标看起来不高,但足够约束技术选型和实现质量了。

2. 技术选型:SpringBoot+Vue3+MyBatis+MySQL为什么够用

2.1 后端为什么用SpringBoot而不是手工搭SSM

SpringBoot的“自动配置”特性,本质上就是把SSM时代大量重复的配置工作交给约定来完成。以前搭SSM要写一堆Spring配置文件、MyBatis配置文件、web.xml,光是环境就能折腾一两天。SpringBoot直接引入starter依赖,加上一个注解,Tomcat内嵌启动,开发效率完全不是一个量级。

版本选择上,我用的是Spring Boot 2.7.18 + JDK 8。如果你本机装了JDK 17,也可以选Spring Boot 3.x,注意MyBatis要用配套的mybatis-spring-boot-starter版本。这里建议不要追求最新,选择生态成熟的版本,遇到问题能搜到的解决方案最多。

2.2 持久层为什么用MyBatis而不是JPA或MyBatis-Plus

JPA和Hibernate在中小型项目里确实配置简单,但有一个问题:一旦涉及多表联查、动态排序、复杂条件拼接,要么写JPQL,要么用Specification,学习成本反而上来了。MyBatis的思路更直白——SQL就是一等公民,你要什么数据,自己写SQL。这个平台虽然数据模型不复杂,但列表页的筛选条件很多,比如按关键词、按分类、按状态、按时间排序,用MyBatis的动态SQL处理起来非常顺手。

至于MyBatis-Plus,它是MyBatis的增强工具,提供了现成的CRUD方法和分页插件,开发速度确实更快。但我这次故意用原生MyBatis加XML映射文件,因为我想把SQL的真实执行路径掌握清楚,而不是完全依赖自动生成的方法。对想打好基础的人来说,先手写一遍再上增强工具,理解会深得多。

2.3 前端为什么用Vue3而不是Vue2

Vue3最大的变化是组合式API(Composition API)。Vue2时代我们写业务逻辑会分散在data、methods、computed、watch里,一个页面功能多点就会变得难维护。Vue3的组合式API允许你把某个功能的变量和函数聚在一起,比如把“订单状态刷新”相关的代码放一块,思路更清晰。

另一个考量是生态。Vue Router 4、Pinia这些配套工具已经把组合式API的玩法固化下来了。加上Vite做构建工具,开发时热更新速度快,编译后的静态文件也够小。对前后端分离项目来说,前端最终产物就是一堆静态文件,后端完全不用关心前端源码,这也是我一直推荐Vue3的原因。

前端UI库我用的是Element Plus,表格、表单、弹窗、分页组件都比较齐全,适合快速搭管理后台。页面样式再配合Flex布局和媒体查询做一下移动端适配,一套代码两端都能看。

2.4 MySQL在这个规模下的定位

MySQL 8.0免费、通用、生态成熟,InnoDB引擎支持事务,这对订单场景是硬性要求。校园便利平台的并发量不会特别大,MySQL完全扛得住,而且市面上关于它的资料最多,遇到问题排查起来最轻松。

关键点是数据库连接串和字符集配置。连接串里要加上serverTimezone=Asia/Shanghai和useSSL=false,字符集用utf8mb4而不是utf8。utf8mb4是MySQL在5.5.3之后引入的完整UTF-8实现,能正确存储emoji和生僻字,学生发布商品描述里出现表情符号时不会乱码。这一点是我实际踩过的坑,后面会专门说。

3. 数据库建模:表结构、索引与订单状态设计

3.1 用户表:不要把业务字段塞进去

用户表是最基础的,但也最容易犯错误。很多人喜欢在用户表里加积分、余额、收货地址等多个字段,结果业务没做起来,表结构先臃肿了。我的设计是只保留最核心的信息:用户名、密码哈希、昵称、角色、手机号、头像、创建时间。

CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT 'BCrypt哈希值', `nickname` varchar(50) DEFAULT '' COMMENT '昵称', `role` tinyint NOT NULL DEFAULT 0 COMMENT '0普通用户 1管理员', `phone` varchar(20) DEFAULT '' COMMENT '手机号', `avatar` varchar(255) DEFAULT '' COMMENT '头像地址', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1正常 0禁用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

密码字段长度留到100,是因为BCrypt哈希结果通常在60个字符左右,但不同实现可能略有差异,留足余量没错。角色字段用tinyint存,比起字符串更省空间,查出来转成枚举也方便。用户状态字段是我后补的,因为管理员要能封禁违规用户,没有状态字段就只能删数据,不记录任何痕迹,这显然不行。

3.2 商品表:状态字段是交易链路的中枢

商品表承载的是交易信息。核心字段是发布者ID、标题、描述、价格、图片、分类、状态。

CREATE TABLE `goods` ( `id` bigint NOT NULL AUTO_INCREMENT, `seller_id` bigint NOT NULL COMMENT '发布者ID', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `title` varchar(100) NOT NULL COMMENT '标题', `description` text COMMENT '描述', `price` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '价格', `image_url` varchar(255) DEFAULT '' COMMENT '主图地址', `status` tinyint NOT NULL DEFAULT 0 COMMENT '0上架 1下架 2已售 3待审核', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_seller_status` (`seller_id`, `status`), KEY `idx_category_status` (`category_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

价格用decimal而不是double,因为浮点数在比较和累加时会产生精度问题,钱相关字段必须用定点数类型。状态字段我拆了4个值,其中“待审核”是给管理员审核用的。这里有个设计心得:不要试图用多个布尔字段表示状态,比如“is_sold”“is_offline”“is_checked”,三个布尔一组合就乱套了。用一个整数状态字段,配合代码里的状态机,比一堆布尔值可靠得多。

3.3 订单表:唯一订单号和状态流转

订单表是平台里最核心的表。字段包括订单号、商品ID、买家ID、卖家ID、金额、状态、创建时间、更新时间。

CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号', `goods_id` bigint NOT NULL COMMENT '商品ID', `buyer_id` bigint NOT NULL COMMENT '买家ID', `seller_id` bigint NOT NULL COMMENT '卖家ID', `amount` decimal(10,2) NOT NULL COMMENT '成交金额', `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待接单 1进行中 2已完成 3已取消', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_buyer_status` (`buyer_id`, `status`), KEY `idx_seller_status` (`seller_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

订单号为什么不直接用自增主键?因为自增ID在电商场景里容易被别人遍历猜测,比如你订单号是10086,别人就能猜到10085订单可能存在。我在后端用“时间戳+随机数”生成订单号,格式类似20250401120000123456,保证唯一且不容易被枚举。物理外键我也没有加,订单和商品、用户之间只保留逻辑关联。理由很简单:物理外键在项目迭代时改表结构会很痛苦,而且InnoDB在插入和删除时会多一层外键检查,性能没优势。数据一致性通过应用层事务来保证。

3.4 索引设计的两个原则

索引不是越多越好,每一个索引都会拖慢写入速度。我只给高频查询路径建了索引。两个原则:一是联合索引要遵循最左前缀原则,比如我需要查“某个卖家正在上架的商品”,就建了(seller_id, status)联合索引,而不是分开建两个单列索引;二是检索量大但区分度低的字段不要单独建索引,比如status字段只有4个值,单列索引帮助不大,一般配合其他字段做联合索引。

列表页的分页查询走的是create_time倒序,所以create_time字段本身没有单独建索引。对于这种体量小、数据量在几万行以内的平台,不建也没太大区别。等数据量真到几十万,再考虑加索引或者上搜索引擎都来得及。

4. 后端核心实现:鉴权、统一返回与订单状态机

4.1 项目结构和登录鉴权

后端我按Controller-Service-Mapper三层组织代码,包结构如下:

com.example.campus ├── common // 统一返回、异常、常量 ├── config // 跨域配置、拦截器注册 ├── controller // 接口层 ├── entity // 实体类 ├── mapper // MyBatis Mapper接口 ├── service // 业务逻辑层 ├── utils // JWT、密码加密等工具

Login接口逻辑:先根据用户名查出用户,用BCrypt校验密码,验证通过后签发一个JWT,返回给前端。前端后续请求都带上这个token,由一个拦截器统一校验。

@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { // 放行登录、注册、商品列表等公开接口 if (request.getRequestURI().contains("/auth/")) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { Claims claims = JwtUtils.parseToken(token.substring(7)); if (claims != null) { // 将用户ID和角色放入ThreadLocal,供后续业务使用 UserContext.set(claims); return true; } } response.setStatus(401); response.setContentType("application/json"); response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}"); return false; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束后清除ThreadLocal,防止用户串号 UserContext.clear(); } }

这里有个经验要点:ThreadLocal存用户信息,用完一定要在afterCompletion里清理。不然Tomcat的线程池会复用线程,上一次请求的用户信息可能被下一个请求读到,造成严重的用户串号问题。这个坑我印象很深,排查了一个下午才发现是ThreadLocal没清理。

密码加密用BCrypt而不是MD5。MD5加盐虽然也有一定安全性,但BCrypt是专门为密码存储设计的慢哈希算法,可以调整计算成本,暴力破解难度大得多。Spring Security里自带的BCryptPasswordEncoder可以直接引入使用,不需要把整个Spring Security框架拉进来。

4.2 统一返回结构和全局异常处理

前后端分离项目,前端最怕后端接口返回格式不统一。我定义了一个Result类:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.setCode(code); r.setMessage(message); return r; } }

配合@RestControllerAdvice做全局异常处理,业务代码里不用到处写try-catch。Service层抛出自定义异常,全局处理器统一捕获转换成Result返回给前端。这样接口状态一目了然,前端axios拦截器也能统一处理错误码。

4.3 商品分页查询:MyBatis动态SQL实战

商品列表查询是一个典型的多条件动态SQL场景,用户可能只传关键词,也可能同时传分类和状态。用MyBatis的XML映射文件处理非常合适。

<select id="listGoods" resultType="com.example.campus.entity.Goods"> select id, seller_id, title, description, price, image_url, status, create_time from goods <where> <if test="keyword != null and keyword != ''"> and title like concat('%', #{keyword}, '%') </if> <if test="categoryId != null"> and category_id = #{categoryId} </if> <if test="status != null"> and status = #{status} </if> </where> order by create_time desc limit #{offset}, #{pageSize} </select>

这里有两个细节值得说。第一,模糊查询用concat('%', #{keyword}, '%')而不是直接在Java层拼好通配符再传进来,是为了避免当keyword包含百分号或下划线时产生意外匹配。第二,排序字段不要用动态传入列名的方式拼接,否则容易出现SQL注入风险,列名如果必须动态,应该用白名单方式校验。

分页我直接用limit实现,没有引入PageHelper插件。原因很简单:项目查询量不复杂,手写limit反而更直观。如果以后查询复杂度上来了,再引入分页插件也是顺手的事。

4.4 下单逻辑与事务边界

下单是整个系统里最需要保证数据一致性的操作。逻辑上分三步:检查商品状态必须是上架中、把商品状态改为已售、插入订单记录。这三步要么全部成功,要么全部失败,必须放在同一个事务里。

事务只靠@Transactional注解还不够,并发场景下会出问题。试想两个人同时看到一件商品并下单,两个事务都通过“商品状态检查”,然后都执行更新,就会出现一单商品被卖出两次的情况。解决办法是在更新SQL里把状态条件带上,做一次条件更新:

int updated = goodsMapper.updateStatusToSold(goodsId);

对应SQL:

update goods set status = 2 where id = #{goodsId} and status = 0

如果updated返回0,说明商品状态已经不是上架中,直接抛异常让事务回滚。这种方式本质上是乐观锁的思路,用状态字段做版本判断,对于校园平台这种低并发场景完全够用,不需要引入真正的版本号机制。

订单状态流转同理,我写了一个状态机工具:

public class OrderStatusMachine { public static boolean canTransfer(Integer oldStatus, String action) { switch (action) { case "accept": return oldStatus == 0; // 待接单 -> 进行中 case "finish": return oldStatus == 1; // 进行中 -> 已完成 case "cancel": return oldStatus == 0 || oldStatus == 1; // 待接单/进行中 -> 已取消 default: return false; } } }

所有订单状态变更都走这个状态机,绝不直接改status字段。这样即使以后业务流程扩展,状态流转规则也集中在一处维护,不会出现订单从“已完成”又跳回“待接单”这种逻辑错误。

5. 前端Vue3对接:从页面结构到接口联调

5.1 工程初始化和目录结构

前端用Vite创建Vue3项目,执行npm create vue@latest命令后,按需要选择Router、Pinia、ESLint插件。项目结构我尽量拆分清楚:

src ├── api // 接口请求模块,一个资源一个文件 ├── assets ├── components // 通用组件 ├── router // 路由配置 ├── stores // Pinia状态管理 ├── utils // axios封装、工具函数 └── views // 页面组件

路由分两块:用户端和管理端。用户端有首页、商品详情、发布、订单列表、个人中心;管理端有商品审核、用户管理、订单管理。我用了路由懒加载,每个页面单独打包,首屏只加载首页需要的JS,体验会好一些。

5.2 axios封装、token处理与开发期接口转发

axios封装是前后端分离项目的关键一环。统一处理三件事:请求头自动带上token、响应拦截器处理业务错误码、401跳转登录页。

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( res => { const { code, message, data } = res.data if (code === 200) { return data } ElMessage.error(message || '请求失败') return Promise.reject(new Error(message || '请求失败')) }, err => { if (err.response && err.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error('网络异常,请稍后重试') return Promise.reject(err) } ) export default request

这里baseURL设成/api,开发时候通过Vite的server配置把请求转发到后端8080端口,不需要后端做跨域处理也能联调:

server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }

等到生产环境,用Nginx做同样的事情:前端请求/api开头,Nginx反向转到后端Java服务。这个转发逻辑前端代码一行都不用改。

5.3 状态管理和核心页面逻辑

Pinia里面我只存了两个核心状态:用户信息和未读通知数量。用户信息会在登录成功后写入Pinia,同时持久化到localStorage。刷新页面时重新从localStorage恢复,避免丢失登录态。

import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}') }), actions: { setLoginData(token, userInfo) { this.token = token this.userInfo = userInfo localStorage.setItem('token', token) localStorage.setItem('userInfo', JSON.stringify(userInfo)) }, logout() { this.token = '' this.userInfo = {} localStorage.removeItem('token') localStorage.removeItem('userInfo') } } })

商品列表页用了Vue3的<script setup>写法,逻辑集中在setup里:

<script setup> import { ref, onMounted } from 'vue' import { getGoodsList } from '@/api/goods' const goodsList = ref([]) const loading = ref(false) const loadList = async () => { loading.value = true try { const data = await getGoodsList({ page: 1, pageSize: 10, status: 0 }) goodsList.value = data.records } finally { loading.value = false } } onMounted(loadList) </script>

这里想特别说一个心得:接口返回数据不要直接塞进响应式对象里当嵌套结构用,容易引起视图不更新。我习惯先赋值给一个普通变量,确认数据结构没问题,再整体赋值给ref变量。因为Vue3的响应式代理只对赋值出去的顶层属性做依赖收集,嵌套层级改动时容易看不出问题。

5.4 打包构建与上线形态

前端最终打包命令就是npm run build,产出到dist目录。这个目录里全是静态资源,后端完全不需要管。生产环境我把它托管到Web服务器下,用户访问的是Web服务器的80或443端口,Web服务器再把/api开头的请求转发给后端8080端口。这种形态下,前端和后端各自独立演进,可以分别发布,这才是前后端分离的最终意义。

6. 实测中容易踩的坑和最终的部署建议

6.1 开发期跨域问题

前后端分离第一道坎就是跨域。开发期和后端联调时,浏览器会拦截跨域请求。我见过很多新手一看到跨域报错,就在前端把axios的baseURL直接改成后端地址,然后再去后端写CORS配置,来回折腾半天。

最舒服的解决方案就是上面说的Vite转发。原理是让前端开发和后端服务跑在同一套端口语义下,请求先到Vite开发服务器,Vite再把请求发给后端。这完全避开了浏览器的跨域限制,前后端开发时都不用管CORS,等到部署时由Nginx统一处理。只有一种情况需要在后端写CORS配置,那就是你硬要前后端域名不一致,然后让浏览器直接跨域调用,这时候后端就要用@CrossOrigin或者CorsFilter放开策略。

建议是:开发期Vite转发,生产期Nginx转发,代码层面不写任何跨域逻辑。这套方案最干净。

6.2 数据库连接串的时区和字符集问题

我第一次启动项目,发现时间字段差了8个小时,前端显示的下单时间和数据库里的对不上。原因是MySQL连接串没指定时区,驱动默认用了服务器本地时区概念。正确写法:

spring: datasource: url: jdbc:mysql://localhost:3306/campus_platform?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password

如果忘了useSSL=false,高版本MySQL驱动还会报SSL连接警告,虽然不影响运行,但控制台密密麻麻的黄字非常烦人。字符集方面,数据库建库时指定utf8mb4,连接串再指定characterEncoding=utf8,双保险,中文和emoji都不会乱。

6.3 图片上传和静态资源映射

商品发布一般要传图片。完整搭建对象存储不现实,我用的是本地磁盘存储方案:后端接收multipart文件,保存到服务器上一个固定目录,然后把这个目录做静态资源映射。

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadDir + "/"); } }

上传成功后,数据库image_url字段存的就是/upload/20250401/xxx.jpg这种相对路径。前端拼接接口域名后就能直接访问。注意生产环境不要把上传目录放在项目打包目录里,否则重新部署jar包时图片会全部丢失。我的是放在服务器一个独立目录,比如/data/campus_upload,部署脚本负责备分这个目录。

6.4 部署形式和避坑清单

部署有两种常用方式。最简单的是单机部署:后端打包成jar包,java -jar campus.jar运行在8080端口;前端dist目录扔到Web服务器静态目录;再用一份Nginx配置完成静态资源服务和/api转发。

如果想更好维护一点,可以Docker Compose把后端、前端、MySQL容器化编排。但说实话,校园项目没必要一开始就上容器,单机部署足够,等有真实用户量再平滑迁移也不迟。

最后整理一份避坑清单,都是我实际开发里遇到过的问题:

问题现象根本原因解决方式
时间差8小时MySQL连接串缺时区加serverTimezone=Asia/Shanghai
emoji存进去变问号字符集不是utf8mb4库表连接串统一utf8mb4
列表页翻页重复数据分页参数传递顺序问题前端参数名与后端Page对象对齐
用户串号ThreadLocal未清理afterCompletion里clear
商品超卖状态更新没带条件update goods set status=2 where status=0
上传图片重启丢失文件存到了jar包内路径改用独立外部存储目录

这些坑的共性问题,其实是“靠经验而非靠直觉”。每一项我在项目过程中都真实遇到过。尤其是商品超卖那个问题,如果不做条件更新,并发稍微上来一点数据就会出现脏状态,测试阶段很难查出来。

最后再分享一个心得体会。做这种完整项目,最大的收获不是代码写了多少行,而是建立了一种“全局视角”:知道数据库表结构怎么设计会影响接口实现,知道接口设计会反过来约束前端页面,知道部署形态会影响配置文件怎么写。校园便利平台虽然业务不复杂,但技术链路是完整的,把它跑通一遍,SpringBoot、Vue3、MyBatis、MySQL这一套组合的真实协作方式就全都吃透了。如果你也准备写类似的系统,建议先把核心闭环做扎实,再慢慢加花活。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询