尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

SpringBoot+Vue商城系统实战:从数据库建模到订单库存与部署全复盘 这个项目算是我这两年做过最完整的一个个人研发项目了。名字叫米家商城其实是从零搭出来的前后端分离商城系统——前台用户商城加后台管理系统一套全通后端SpringBoot MyBatis MySQL前端Vue全家桶。源码量不算小业务复杂度也够看从数据库建模到订单流转再到部署上线我完整走了两遍也重构了两轮踩的坑非常多。最近一直有人问商城项目到底该怎么做我干脆写一篇详细复盘把这套系统的设计思路、核心实现和我实际踩过的坑都摊开说一说。这篇文章适合几类人看准备用SpringBootVue做课程设计或毕业设计的学生刚工作不久想找个完整业务系统练手的朋友以及打算把老单体商城项目重构一遍的开发者。我会尽量解释每个关键选择背后的“为什么”因为商城项目最大的工作量从来不在写接口而是在业务设计和边界处理上。先给你交个底整套系统跑通不难跑稳才是真正考验功力的地方。1. 先说结论为什么2025年还选SpringBootVue写商城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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB 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) ) ENGINEInnoDB COMMENT订单主表;订单号设计同样值得说。订单号要唯一、不要太长、还不能让人一眼看出当天单量。我用的是“时间串yyyyMMddHHmmss 用户ID后四位 随机数”拼接后大概18位例如2025060814203500124837。这个方案不需要额外依赖分布式ID服务并发下重复概率极低对当前量级完全够用。如果以后量起来再切换成雪花算法也不迟。2.3 数据库索引与订单号的生成规则索引这块我的原则很简单没有索引就是全表扫描乱建索引就是浪费磁盘。商城系统优先建索引的字段有三类外键关联字段user_id、category_id、product_id高频查询字段order_no、username、phone状态筛选字段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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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.StdOutImplmap-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的“双保险”方案。流程是用户登录成功后端生成JWT Token同时把Token和用户ID写入Redis设置2小时过期前端把Token存到localStorage每个请求头带Authorization: Bearer token后端拦截器校验Token有效性解析出用户ID后放到ThreadLocal里方便后续Service直接取每次有效请求到达就刷新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跑JVMMySQL和Redis也在同一台机器上。这个方案对个人项目和商城的初期流量完全够用等流量真上来了再考虑拆库拆服务。部署的步骤流程安装MySQL导入建表SQL。注意Linux下MySQL默认表名大小写敏感建表时统一小写安装Redis设置密码并修改bind地址禁止公网直接访问安装Nginx把前端构建产物放到/usr/share/nginx/html/mall/目录配置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_log1 slow_query_log_file/var/log/mysql/slow.log long_query_time1上线初期我用这个日志抓到一条商品列表多表关联SQL跑了1.8秒定位后发现少建了一个索引补上之后直接降到几十毫秒。这类问题不靠日志定位靠直觉几乎发现不了。6.3 上线后的常规巡检清单上线后我给自己定了一个30分钟巡检清单连续做了一周看后端日志有没有ERROR级异常看服务器内存、CPU、磁盘使用率完整走一遍首页、列表、详情、购物车、下单流程看MySQL慢查询日志记录新增慢SQL检查Redis内存占用是否增长过快这套巡检把隐藏问题基本都暴露出来了。最典型的一个是Redis内存增长过快最后定位到Token长期有效的用户会话积压太多后来加了一个定期清理过期Token的定时任务才解决。商品图片的加载速度对前端体验影响很大。我后来把图片从本地目录迁到了Minio对象存储加了一层Nginx代理来访问桶里的静态资源。Minio本身不复杂唯一的坑是如果配了公开读权限的桶需要单独设置匿名访问策略否则图片URL会返回无权限错误。这个坑我在网上也看到很多人问。MySQL连接SSL的问题也会在部署时冒出来。某些环境下连接串里带了useSSLtrue如果没有配置证书就会报SSL connection error。本地测试可以直接useSSLfalse生产环境有条件就配上正式证书没有条件的话明确禁用SSL也比模棱两可的报错强。7. 复盘总结这个项目还能怎么改最后聊点复盘和后续改造方向。这套米家商城从零开发大概用了两周多业余时间从需求梳理、UI草稿、后端实现到部署上线完整走了一遍。整个流程下来我最大的体会是一套系统跑通不难跑稳很难。你独立做过一次全栈项目后会发现技术点其实就那么几个真正拉开差距的是边界情况的处理、多人协作时的规范以及上线后的持续观察。如果现在重新做一遍有几件事我一定会放到最前面接口文档先行。第一次开发时我是凭感觉写接口后期前端返工无数次改字段改到崩溃。第二个版本先设计接口文档再动手编码效率提升非常明显。引入Mock数据。商品、订单的测试数据在前端联调时非常好用没有Mock数据页面做出来根本看不出效果也不能暴露字段缺失。日志规范要提前定。我在项目里把关键接口的入参、耗时都打了日志后面排查线上问题省了太多时间。早期日志太简陋线上报错只能逐行打补丁。后续改造方向我心里排了三个优先级一是秒杀业务可以引入消息队列做削峰填谷二是下单支付链路逐步拆成独立服务把稳定性和扩展性拉开三是商品详情页做页面静态化或者服务端渲染进一步提升首屏速度。这套系统整体来说非常适合作全栈入门到进阶的实战项目只要把数据库设计、订单状态机、权限体系、缓存优化这几块吃透你基本就具备独立开发一个业务系统的能力了。我后面还会继续重构这个商城有新踩的坑也会继续记录分享。如果这篇文章里的某个问题正好点醒了你那这几千行代码就没白写。
返回列表