news 2026/10/2 15:02:13

SpringBoot+Vue商城系统实战:从数据库建模到订单库存与部署全复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue商城系统实战:从数据库建模到订单库存与部署全复盘

这个项目算是我这两年做过最完整的一个个人研发项目了。名字叫米家商城,其实是从零搭出来的前后端分离商城系统——前台用户商城加后台管理系统一套全通,后端SpringBoot + MyBatis + MySQL,前端Vue全家桶。源码量不算小,业务复杂度也够看,从数据库建模到订单流转再到部署上线,我完整走了两遍,也重构了两轮,踩的坑非常多。最近一直有人问商城项目到底该怎么做,我干脆写一篇详细复盘,把这套系统的设计思路、核心实现和我实际踩过的坑都摊开说一说。

这篇文章适合几类人看:准备用SpringBoot+Vue做课程设计或毕业设计的学生,刚工作不久想找个完整业务系统练手的朋友,以及打算把老单体商城项目重构一遍的开发者。我会尽量解释每个关键选择背后的“为什么”,因为商城项目最大的工作量从来不在写接口,而是在业务设计和边界处理上。先给你交个底:整套系统跑通不难,跑稳才是真正考验功力的地方。

1. 先说结论:为什么2025年还选SpringBoot+Vue写商城

1.1 这套技术栈的真实处境

每次一聊新项目,总有人说“现在谁还用SpringBoot,早该上微服务云原生了吧”。这话有道理,但放到商城这种典型的全栈业务系统上,多少有点用牛刀杀鸡的意思。SpringBoot到今天依然是Java后端开发的绝对主力,生态成熟、资料好找、排错成本低,Vue就更不用提,前端三大框架里学习曲线最平缓、中文资料最多的选择,没有之一。

我这套系统当时的选型是:

  • 后端:SpringBoot 2.7.18 + MyBatis + MySQL 8.0 + Redis
  • 前端:Vue 3 + Vite + Pinia + Element Plus,前台商城页面自己封装组件
  • 存储:初期把商品图片放本地目录,后来迁到Minio对象存储

要说明一下,选SpringBoot 2.7而不是3.x,不是因为3不好,是当时项目里的部分依赖和3.x的Jakarta迁移还没对齐。2025年做全新项目其实建议直接上SpringBoot 3.2以上的稳定版,但从老项目改造过来,继续用2.7也没问题。版本高低真的不重要,重要的是你对框架本质的理解。

1.2 选型的边界条件与取舍逻辑

我做技术选型一定会先问三个问题:项目周期长不长?部署环境什么规模?团队协作水平如何?

这套商城就是一个典型的中小型单体业务系统,预估并发几十到几百,数据量初期也就几万条订单。这个场景下,单体应用就是性价比最高的方案。不要一上来就拆微服务、上消息队列,那是把简单问题复杂化。很多看起来“落后”的方案,恰恰是最可靠、最容易维护的。

MyBatis在SQL可控性上的优势,做商城项目时体会特别深。商城里的复杂查询非常多——多表联查、统计报表、分页排序,MyBatis的XML让你能精确控制每一条SQL,比JPA自动生成SQL好调优得多。代价是样板代码多,所以我项目里用了MyBatis-Generator生成基础的单表CRUD,在这个基础上手写复杂查询,效率和可控性都挺平衡。

2. 数据库模型是商城的骨架:我的建表思路

2.1 用户、商品、分类三大基础域的字段设计

商城系统最核心的基础数据是三块:用户、商品、分类。这三个模块的表设计做好了,后面扩展业务会非常顺畅,反过来则到处打补丁。

用户表,我建议至少保留这些字段:

CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL', `gender` TINYINT DEFAULT 0 COMMENT '性别 0未知 1男 2女', `status` TINYINT DEFAULT 1 COMMENT '状态 1正常 0禁用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), KEY `idx_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

这里有个小坑:密码字段长度别舍不得给。BCrypt加密后的字符串固定是60个字符,网上有些教程图省事写32位,等你接真实注册流程时数据被截断,登录永远失败,排查半天都找不出问题。我第一版就吃过这个亏。

商品表是重头戏,字段最多。我的商品表核心字段大概包括:id、name、sub_title、category_id、main_img、detail_img_json、price、stock、sales_count、status。价格字段必须用DECIMAL(10,2),这条已经是老生常谈,但每代人都会有人踩:FLOAT和DOUBLE在MySQL里存十进制小数会引入精度误差,订单对账的时候差一分钱都让人头大,用DECIMAL就没有这个问题。

分类表控制在两级以内就够了。如果要做三级分类,用parent_id递归关联。真实电商里二级分类能覆盖90%以上的场景,过度设计分层反而让前端选择器变得复杂。

商品和分类这两个表是典型的“代码生成器直出”场景。用MyBatis-Generator生成实体、Mapper接口和XML后,我只需要手动补上逻辑删除字段、通用状态字段和创建更新时间默认值,剩下的精力全部投入到复杂查询SQL上。

2.2 购物车、订单、订单项的事务关联设计

商城的数据关系里,购物车和订单最容易设计翻车。

购物车我做成一张关联表:

CREATE TABLE `cart` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL COMMENT '用户ID', `product_id` BIGINT NOT NULL COMMENT '商品ID', `quantity` INT DEFAULT 1 COMMENT '数量', `selected` TINYINT DEFAULT 1 COMMENT '是否选中', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_product` (`user_id`, `product_id`) ) ENGINE=InnoDB COMMENT='购物车表';

注意uk_user_product这个唯一键。它的作用是保证同一个用户对同一件商品只存在一条购物车记录,加购的时候走INSERT ... ON DUPLICATE KEY UPDATE quantity = quantity + 1这种更新逻辑,而不是不断插入新行。这个设计能让购物车的查询、合并、统计逻辑简单非常多,也避免数据膨胀。

订单模块我拆成了两张表:orders订单主表和order_item订单项表。为什么要拆?因为一个订单可以包含多个商品,如果把商品列表塞进订单表某一个字段,后面前台查订单列表就得解析JSON,后台统计销售额也得拆来拆去,纯属给自己找罪受。订单项表的另一个关键作用是记录下单时刻的商品快照——商品名称、单价、图片都复制到订单项里,这样以后就算商品下架改名,历史订单照样能正确展示。

订单主表的核心字段:

CREATE TABLE `orders` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单号', `user_id` BIGINT NOT NULL, `total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单总金额', `pay_amount` DECIMAL(10,2) DEFAULT 0 COMMENT '实付金额', `pay_type` TINYINT DEFAULT 1 COMMENT '支付方式 1微信 2支付宝', `status` TINYINT DEFAULT 0 COMMENT '订单状态 0待支付 1已支付 2已发货 3已签收 4已取消', `consignee` VARCHAR(50) DEFAULT NULL COMMENT '收货人', `consignee_phone` VARCHAR(20) DEFAULT NULL, `consignee_address` VARCHAR(255) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `pay_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB COMMENT='订单主表';

订单号设计同样值得说。订单号要唯一、不要太长、还不能让人一眼看出当天单量。我用的是“时间串(yyyyMMddHHmmss)+ 用户ID后四位 + 随机数”,拼接后大概18位,例如2025060814203500124837。这个方案不需要额外依赖分布式ID服务,并发下重复概率极低,对当前量级完全够用。如果以后量起来,再切换成雪花算法也不迟。

2.3 数据库索引与订单号的生成规则

索引这块我的原则很简单:没有索引就是全表扫描,乱建索引就是浪费磁盘。商城系统优先建索引的字段有三类:

  1. 外键关联字段:user_id、category_id、product_id
  2. 高频查询字段:order_no、username、phone
  3. 状态筛选字段:status(后台经常按状态查订单列表)

容易忽略的是联合索引。比如后台最常见的“按订单状态+时间范围查订单”查询,如果只有status单列索引,MySQL还是要做回表和文件排序。我在orders表里加了一个idx_status_time (status, create_time)联合索引,查询速度提升非常明显。如果你做订单管理后台总觉得列表页慢,去查一下有没有这个索引。

建表时还有几个基础规范:字符集一律用utf8mb4,别再用老旧的utf8。实际上MySQL的utf8只是一个别名,最大只支持3字节,存emoji和生僻字会直接报错。utf8mb4才是完整版。另外表名和字段名统一小写下划线风格,避免在Linux和Windows之间切换部署时踩到大小写敏感问题。

3. 后端SpringBoot工程搭建要点

3.1 项目结构划分与Maven依赖

后端结构我按经典的“控制层-服务层-数据访问层-实体”四层组织,同时把Controller拆成admin和client两个子包:

com.xiaomi.mall ├── controller │ ├── admin # 后台管理接口 │ └── client # 前台商城接口 ├── service │ └── impl ├── mapper ├── entity ├── dto # 入参对象 ├── vo # 返回给前端的对象 ├── common # 公共常量、统一结果、异常 ├── config # 配置类 ├── interceptor # 拦截器 ├── utils # 工具类 └── MallApplication.java

这么分的好处是看一眼包名就知道接口是给前台用户用的还是后台管理员用的。同时入参和出参不直接放实体类,而是用DTO和VO隔离。很多小项目嫌类太多不这么干,后面前端改一个字段名,后端就要连带改实体,或者某个接口不小心把实体整个JSON序列化返回给了前端,密码字段也跟着裸奔出去。我早期项目就出过这种事,登录接口直接返回了整个用户实体,虽然密码是加密后的,但这种接口设计在真实项目里等于开了一扇危险的门。

Maven依赖方面,核心就这几个:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

这里必须强调一个坑:mybatis-spring-boot-starter的版本要和SpringBoot版本严格匹配。我有一次在SpringBoot 3.1上配了旧版MyBatis starter,启动直接报ClassNotFoundException: javax.sql.DataSource。原因就是SpringBoot 3.x把javax迁移到了jakarta命名空间,旧集成包不兼容。不想在这个问题上浪费时间,就用SpringBoot 2.7 + MyBatis 2.x的组合,或者去MyBatis官方查最新的适配版本。版本兼容表比任何经验都靠谱。

3.2 MyBatis配置与XML映射的最佳实践

MyBatis的配置不需要太复杂,但有几个关键项别漏:

mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.xiaomi.mall.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

map-underscore-to-camel-case: true是个救命配置,打开后数据库的user_name字段能自动映射到JavaBean的userName属性,省掉一大半手写resultMap的工作。如果不打开,每个实体类字段都要靠<resultMap>一个个硬怼,那是纯粹的体力活。

log-impl: StdOutImpl用于开发环境打印SQL。上线前记得改成Slf4jImpl或者干脆关掉,不然每条SQL都会打出来,日志文件膨胀速度会让你怀疑人生。

我写了一段时间XML之后,发现一个规律:能用Java代码处理的条件判断就不要全堆在XML里,XML只做核心SQL拼接。MyBatis的动态SQL标签<if>、<where>、<foreach>非常强大,但一旦把复杂业务条件全写进去,可读性和可维护性都会急剧下降。过度抽象的动态SQL会让接手维护的人看不懂,我见过一个订单查询XML堆了二十多个<if>,后面修bug改了一整天。

顺便聊一下MyBatis缓存。一级缓存是SqlSession级,默认开启,但一次请求里如果先查询再更新,缓存里的旧数据会坑你一把。二级缓存默认关闭,商城这类系统不建议依赖它做分布式缓存,因为MyBatis原生二级缓存只在一个JVM实例内生效,多实例部署就会出数据一致性问题。真正的缓存方案应该放在Redis,业务层自己控制查询顺序和过期策略。

3.3 登录鉴权与用户会话管理

用户登录这块,我用的是JWT加Redis的“双保险”方案。流程是:

  1. 用户登录成功,后端生成JWT Token,同时把Token和用户ID写入Redis,设置2小时过期
  2. 前端把Token存到localStorage,每个请求头带Authorization: Bearer <token>
  3. 后端拦截器校验Token有效性,解析出用户ID后放到ThreadLocal里,方便后续Service直接取
  4. 每次有效请求到达就刷新Redis里这个Token的过期时间,实现滑动续期

拦截器核心逻辑大致这样:

@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate redisTemplate; @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); } if (!jwtUtil.verify(token)) { throw new BusinessException(401, "未登录或登录已过期"); } Long userId = jwtUtil.getUserId(token); redisTemplate.expire("login:token:" + token, 2, TimeUnit.HOURS); UserContext.set(userId); return true; } }

后台管理员鉴权跟用户端类似,只是要额外校验一次角色字段。我第一版没有做角色拆分的RBAC,所有后台接口共用一个管理员账号,后来表结构里加了role字段和菜单表才算完善。其实个人项目或小团队后台,简单的RBAC完全够用,不用一上来就上Spring Security全家桶。

很多人问我为什么不用Spring Security。我的经验是:Spring Security的过滤器链确实强大灵活,但对这个体量的商城,学习成本和配置复杂度都太高了。JWT加一个拦截器就能覆盖会话控制80%的需求,剩下20%跟具体业务耦合很紧,框架帮不上什么忙。当然,如果是公司级统一认证需求,那就老老实实研究Spring Security。

4. 前台商城与后台管理两端Vue实现

4.1 前台商城的核心页面与组件划分

Vue前端我拆成两个独立工程:mall-web前台用户商城和mall-admin后台管理系统。分开打包、分开部署,互不干扰,想改其中一个不会影响另一个。

前台商城页面不多,但每个页面都有业务分量:

  • 首页:热门商品推荐、分类导航
  • 商品列表页:分类筛选、价格排序、分页
  • 商品详情页:主图轮播、规格选择、加入购物车
  • 购物车页:勾选商品、改数量、实时计算总价
  • 下单页:选择地址、确认金额、提交订单
  • 订单列表页:状态筛选、取消订单、确认收货
  • 个人中心页:用户信息、收货地址管理

判断一个商城前端写得好不好,不要只看页面多不多,要看商品数据流和购物车状态是否清晰。我在前台用了Pinia做全局状态管理,购物车数据会同步到localStorage,用户刷新页面购物车不会丢。Vue3项目一定要用Pinia而不是Vuex,因为Pinia语法简洁、TypeScript支持好、官方也在主推。

路由设计上,前台商城用普通静态路由就够了,但后台管理系统需要动态路由。所谓动态路由,就是根据登录用户角色,在路由守卫里通过Vue Router的addRoute方法动态注册菜单页面。我用在后台菜单上,不同角色登录后看到的菜单项不一样。核心代码:

// router/index.ts router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login' }) } else { next() } }) // 后台登录后动态注册 const menus = await getMenuByRole() menus.forEach(menu => { router.addRoute({ path: menu.path, component: () => import(`@/views/${menu.component}`) }) })

前端还有一个细节特别容易忽略:购物车加购按钮的防连点。用户连续点击加购,如果不做节流,一个商品可能被重复提交多次。我解决的办法是点击后让按钮进入loading状态,等接口返回再恢复。这种小坑虽然不起眼,但直接影响用户体验和库存准确性。

4.2 后台管理系统的权限与数据看板

后台管理系统用的是Element Plus,页面模块包括:数据看板、商品管理、分类管理、订单管理、用户管理、系统设置。

数据看板是我后补的模块,也是使用频率最高的。看板顶部的几个核心指标,其实就是几个SQL聚合查出来:总用户数、今日订单量、今日销售额、待发货数量。比如今日销售额:

SELECT COALESCE(SUM(pay_amount), 0) FROM orders WHERE status IN (1,2,3) AND pay_time >= CURDATE();

后台和前台的接口在同一个后端服务里,因此接口设计从一开始就要区分好前缀。我用/api/admin/**和/api/client/**两个路径前缀,拦截器按前缀做权限控制。如果前期没做这个区分,后台管理员和普通用户共用同一套接口,后面每次加鉴权逻辑都是灾难。

4.3 API对接与前端路由拦截要点

前后端联调是整体项目里最耗时最容易扯皮的部分。前端和后端各写各的,字段名一旦对不上就开始互相甩锅。我的解决方式是:先定接口文档,跑通主流程,再写页面。

接口返回统一格式:

{ "code": 200, "message": "success", "data": {} }

前端axios封装里统一处理后端返回码,code != 200时全局弹错误提示,每个页面不需要重复写错误分支。同时axios响应拦截器拿到401状态码时做一次全局登录跳转。这套约定落地后,前后端联调效率提升非常明显。

分页结构也统一成:

{ "list": [], "total": 100, "pageNum": 1, "pageSize": 10 }

前端封装一个通用的分页组合式函数,每个列表页只需要调这个函数,分页参数组装和翻页逻辑不用再重复写。这类复用工作短期内看有点繁琐,但为后端新增页面能省下大量重复代码。

5. 订单状态机与并发扣库存:最容易出错的业务闭环

5.1 订单状态流转与超时处理

整个商城项目里,订单模块是难度天花板。原因很直白:订单状态多,状态之间流转有约束,还牵扯支付、库存、发货的连锁操作,任何一环漏了就数据不一致。

我定义的状态是:

状态值含义对应操作
0待支付创建订单后
1已支付支付回调成功
2已发货后台管理员发货
3已签收用户确认收货
4已取消用户取消或超时关闭

状态流转必须先在脑子里理清楚,代码才有据可依。比如只有“待支付”状态才能取消,只有“待支付”才能支付成功,只有“已支付”才能发货。我在代码里用一个状态机枚举类来收口:

public enum OrderStatus { PENDING(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), RECEIVED(3, "已签收"), CANCELLED(4, "已取消"); private final int value; private final String desc; }

更新订单状态时,我坚持用一条带状态校验的update语句:

int rows = orderMapper.updateStatus(orderId, fromStatus, toStatus); if (rows == 0) { throw new BusinessException("订单状态异常,请刷新后重试"); }

“返回影响行数是否为0”是整个并发安全的关键。假如用户同时点了取消和支付,没有状态校验的话,极易出现一个操作扣库存、另一个操作又回补库存的双重操作。这个原子条件更新能保证数据库层面上只有一条分支执行成功。

超时未支付订单的处理,我用定时任务每分钟扫描一次“创建时间超过30分钟且状态为待支付”的订单,批量关闭并回补库存。也可以用延迟消息中间件,但对这个场景来说,定时任务简单且足够。扫描的SQL务必带上create_time索引,否则数据量大了会被慢查询拖死。对了,回补库存不能直接SET stock = stock + qty就完事,这条SQL本身没问题,但一定要和扣库存一样考虑并发安全,用条件更新的方式补,不然会覆盖别人同时下单产生的库存变化。

5.2 并发扣库存的几种方案对比

库存扣减是商城高并发的经典考题。我实测下来,最优方案不是用什么复杂的中间件,而是把扣库存变成一个原子SQL加条件判断。

最安全的写法:

UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}

这行SQL让数据库自己判断:当前库存不够,影响行数为0;更新成功,说明库存扣减成功。业务层根据影响行数决定继续走下单还是返回库存不足。这个方法不需要加锁,不需要分布式锁,在绝大多数商城并发场景下性能和安全都达标了。

相比之下,“先select库存再update”的写法从根上就有漏洞。select和update是两个操作,中间有并发间隙,库存可能已经被别人改掉。哪怕你给它套上synchronized,在单机项目里还能用,一旦要横向扩容就失效了。分布式锁倒是个选择,但锁开销大,粒度还难把控。真要用,推荐Redisson的Lock,粒度要精细到某个商品的库存上,别把整个下单流程锁住,那会让吞吐量直线下降。

5.3 支付回调的幂等处理经验

支付回调是商城项目里另一个大坑。第三方支付平台对同一笔订单回调,可能是多线程并发调多次,也可能是网络超时后自动重试多次,所以回调处理逻辑必须满足幂等。

我的做法是:收到支付回调时,先去数据库查当前订单状态,如果已经不是“待支付”,说明这笔订单之前已经处理过了,直接返回成功给支付平台,不再更新任何数据。这样既不会重复改状态,也不会重复加积分,更不会重复触发发货逻辑。

支付回调里切记不要信任前端传过来的金额。每次都要从数据库查订单原价,和回调报文里的实付金额做比对,不一致直接拒绝。我刚接支付时没做这层校验,测试环境回调金额和订单金额不一致,结果订单被标记成已支付,数据对不上,折腾了大半天才定位到。金额校验这个习惯,一定要刻进做支付功能时的DNA里。

幂等的另一个关键点是:保证整个业务处理的入口是幂等的,出错了可以重放,重放不会产生额外副作用。如果你用了消息队列做支付后的异步处理,也要在消费者里做防重和去重,否则消息重投时会出大事。

6. 从本地到上线:部署过程与性能细节

6.1 打包与部署(前后端分离部署)

部署方案我用了最简单的单机多服务模式:一台云服务器,前端静态资源交给Nginx,后端SpringBoot跑JVM,MySQL和Redis也在同一台机器上。这个方案对个人项目和商城的初期流量完全够用,等流量真上来了再考虑拆库拆服务。

部署的步骤流程:

  1. 安装MySQL,导入建表SQL。注意Linux下MySQL默认表名大小写敏感,建表时统一小写
  2. 安装Redis,设置密码并修改bind地址,禁止公网直接访问
  3. 安装Nginx,把前端构建产物放到/usr/share/nginx/html/mall/目录
  4. 配置Nginx反向代理,/api前缀转发到后端8080端口

Nginx关键配置:

server { listen 80; server_name your.domain.com; location / { root /usr/share/nginx/html/mall; 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配置。Vue是单页应用,前端路由在服务器上没有真实文件对应,所以所有路径都要回退到index.html。漏掉这行配置,刷新非根路径页面就直接404了,这个问题几乎百分之百会遇到。

后端打包用Maven的package命令生成jar包。部署脚本我写了一个简单的deploy.sh,流程是停旧进程、备份文件、替换jar、启动服务。脚本很短,但能省掉每天重复敲命令的时间。条件允许的话,用systemd管理进程可以做到开机自启和崩溃自动拉起,比纯粹nohup java -jar要稳妥得多。

6.2 数据库连接池、缓存与慢SQL优化

系统上线之后,第一件要检查的不是业务功能,而是数据库连接池配置。

我用的是SpringBoot默认的HikariCP,生产环境参数不能直接裸奔:

spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000

这些参数没有标准答案,跟机器配置和业务量挂钩。我的经验是池子不要开太大,每个连接都要占用JVM和数据库两端的内存。一台4核8G的服务器,20个连接已经完全够用。池子太小会导致请求排队,池子太大则会拖垮数据库。

热点数据我直接在商品详情接口上做了一层Redis缓存。key是product:detail:{id},第一次查询数据库后把详情序列化存入Redis,后续查询直接回缓存,设置5分钟过期。这样商品被频繁访问时,数据库压力能降低一个数量级。

慢SQL排查开了MySQL慢查询日志:

slow_query_log=1 slow_query_log_file=/var/log/mysql/slow.log long_query_time=1

上线初期我用这个日志抓到一条商品列表多表关联SQL跑了1.8秒,定位后发现少建了一个索引,补上之后直接降到几十毫秒。这类问题不靠日志定位,靠直觉几乎发现不了。

6.3 上线后的常规巡检清单

上线后我给自己定了一个30分钟巡检清单,连续做了一周:

  • 看后端日志有没有ERROR级异常
  • 看服务器内存、CPU、磁盘使用率
  • 完整走一遍首页、列表、详情、购物车、下单流程
  • 看MySQL慢查询日志,记录新增慢SQL
  • 检查Redis内存占用是否增长过快

这套巡检把隐藏问题基本都暴露出来了。最典型的一个是Redis内存增长过快,最后定位到Token长期有效的用户会话积压太多,后来加了一个定期清理过期Token的定时任务才解决。

商品图片的加载速度对前端体验影响很大。我后来把图片从本地目录迁到了Minio对象存储,加了一层Nginx代理来访问桶里的静态资源。Minio本身不复杂,唯一的坑是如果配了公开读权限的桶,需要单独设置匿名访问策略,否则图片URL会返回无权限错误。这个坑我在网上也看到很多人问。

MySQL连接SSL的问题也会在部署时冒出来。某些环境下连接串里带了useSSL=true,如果没有配置证书,就会报SSL connection error。本地测试可以直接useSSL=false,生产环境有条件就配上正式证书,没有条件的话明确禁用SSL也比模棱两可的报错强。

7. 复盘总结:这个项目还能怎么改

最后聊点复盘和后续改造方向。

这套米家商城从零开发,大概用了两周多业余时间,从需求梳理、UI草稿、后端实现到部署上线完整走了一遍。整个流程下来我最大的体会是:一套系统跑通不难,跑稳很难。你独立做过一次全栈项目后会发现,技术点其实就那么几个,真正拉开差距的是边界情况的处理、多人协作时的规范以及上线后的持续观察。

如果现在重新做一遍,有几件事我一定会放到最前面:

  1. 接口文档先行。第一次开发时我是凭感觉写接口,后期前端返工无数次,改字段改到崩溃。第二个版本先设计接口文档再动手编码,效率提升非常明显。
  2. 引入Mock数据。商品、订单的测试数据在前端联调时非常好用,没有Mock数据,页面做出来根本看不出效果,也不能暴露字段缺失。
  3. 日志规范要提前定。我在项目里把关键接口的入参、耗时都打了日志,后面排查线上问题省了太多时间。早期日志太简陋,线上报错只能逐行打补丁。

后续改造方向,我心里排了三个优先级:一是秒杀业务可以引入消息队列做削峰填谷;二是下单支付链路逐步拆成独立服务,把稳定性和扩展性拉开;三是商品详情页做页面静态化或者服务端渲染,进一步提升首屏速度。

这套系统整体来说非常适合作全栈入门到进阶的实战项目,只要把数据库设计、订单状态机、权限体系、缓存优化这几块吃透,你基本就具备独立开发一个业务系统的能力了。我后面还会继续重构这个商城,有新踩的坑也会继续记录分享。如果这篇文章里的某个问题正好点醒了你,那这几千行代码就没白写。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 15:02:08

输电线路红外过热检测数据集:VOC/YOLO双格式2253张实战解析

简介&#xff1a;面向电力设备巡检、计算机视觉目标检测研究者的输电线路红外过热检测数据集&#xff0c;针对输电线路运行中红外过热隐患难以快速定位的问题&#xff0c;提供精细标注数据&#xff0c;可直接用于目标检测模型的训练、验证与对比研究&#xff0c;也适合作为高校…

作者头像 李华
网站建设 2026/10/2 15:02:04

MindSpore字节码虚拟机:动态张量计算的实时编译内核

1. 这不是又一个“虚拟机JIT”的老故事&#xff0c;而是张量计算范式迁移的底层支点 你打开VSCode&#xff0c;选中MindSpore内核&#xff0c;写完一段 nn.Cell 定义&#xff0c;点击运行——几毫秒后&#xff0c;GPU上已开始执行融合后的算子流水线。你没手动写CUDA核&…

作者头像 李华
网站建设 2026/10/2 15:01:14

GitHub周榜精选:0920~0926效率工具与AI编程项目深度解析

1. 从“0920&#xff5e;0926”这个时间切片说起&#xff1a;为什么周榜比月榜更有参考价值 每周固定刷 GitHub Trending 的人应该都有个体会&#xff1a;月榜上的项目往往是“已经火了很久的老面孔”&#xff0c;而周榜才是真正能看出“这周圈子里在聊什么”的温度计。0920&am…

作者头像 李华
网站建设 2026/10/2 15:00:37

RollingGo酒店MCP工具扩容至7个:AI Agent酒店预订全流程能力解析

1. 这次更新到底改了什么&#xff1a;从4个工具到7个工具的跨越RollingGo酒店MCP这次把内置工具从原来的4个直接扩容到7个&#xff0c;补齐了酒旅全流程的能力闭环。如果你之前用过早期版本&#xff0c;应该记得那时候只能做基础的城市搜索、酒店列表拉取和房型查询&#xff0c…

作者头像 李华
网站建设 2026/10/2 15:00:28

项目经理能力强不强,遇事反应见真章

干这行十几年&#xff0c;我带过、合作过、也亲手送走过不少项目经理。慢慢我发现一个几乎不会出错的判断方法&#xff1a;别看谁把WBS、敏捷、干系人管理讲得头头是道&#xff0c;也别看他平时周报写得有多漂亮&#xff0c;就看他遇到突发状况时的第一反应。项目经理能力强不强…

作者头像 李华
网站建设 2026/10/2 14:59:51

8G显存16G内存跑本地大模型:Ollama+GGUF量化部署全攻略

8G显存、16G内存的电脑&#xff0c;到底能不能跑本地大模型&#xff1f;这句话我过去一年被问了不下百次。每次我都会先给结论&#xff1a;能跑&#xff0c;而且跑得比我预期好得多。这套配置如今是大模型本地部署最常见的平民门槛——往上比不过24G大显存机器&#xff0c;但往…

作者头像 李华