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

资讯详情

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

Java电商后台管理系统实战:Spring Boot+MyBatis Plus+Redis全解析

Java电商后台管理系统实战:Spring Boot+MyBatis Plus+Redis全解析 简介这是一份面向Java后端开发学习者的电商后台管理系统完整源码包旨在模拟京东、淘宝等大型电商平台的后台管理功能帮助读者理解商品管理、订单处理、用户管理、权限控制与数据检索等核心模块的实际落地。资源共185个文件含71个Java源文件、68个class编译文件、34个XML配置、10个YML配置、Git忽略文件等压缩包整体约365KBJava代码承载业务逻辑与数据交互XML与YML分别负责Spring/MyBatis框架和Spring Boot应用配置工程结构完整。从预览内容可见项目覆盖Elasticsearch检索测试、商品与分类服务、品牌服务、后台用户管理及服务实现类便于对照调试。目前已吸引306人学习下载适合具备一定Java基础、希望系统了解电商后台架构的开发者借助源码可掌握服务分层、持久层操作和检索集成的常见写法也可作为课程设计或二次开发的起点。1. 一套能跑通、能答辩的Java电商后台管理系统它到底解决了什么为了课程设计或毕业设计去找Java电商后台管理系统设计源码的人多数会撞上同一个场面源码下好了启动就报错改了数据库密码又遇到Redis连不上最后连页面长什么样都没看到。这套基于Java语言的电商后台管理系统设计源码其实就是一个前后端分离的单体后台Spring Boot提供RESTful接口MyBatis Plus操作MySQLRedis做缓存前端用Vue管后台页面。登录鉴权、商品管理、购物车、订单流程都覆盖到了适合拿来做课程设计起点也适合跑通后对着回答面试里的Java八股问题。我按自己调这类项目的顺序把环境搭建、核心模块、常见坑和验证方式一次讲透。2. 技术选型与项目骨架Spring Boot MyBatis Plus MySQL怎么选、怎么搭这一章解决的是“技术栈为什么这么定”和“项目怎么从零搭起来”两个问题。很多人拿到源码第一反应是赶紧点开页面结果代码还没跑起来就被一堆依赖搞得没耐心。我习惯先花半小时把骨架讲清楚后面动手调试时才知道报错出在哪一层。2.1 选型理由三条边界划清技术栈电商后台管理系统这个领域技术栈选择其实很收敛。我见过有人把Spring Cloud全家桶塞进来最后连启动都要调半天。我的判断标准只有三条第一前后端必须分离不然Vue页面和Thymeleaf混在一起新手维护起来非常痛苦第二SQL必须可见可调不然出了问题没法跟老师或面试官解释第三本地要能一条命令启动频繁重启不心疼。顺着这三条选型就顺理成章。Spring Boot在SSM基础上把Bean配置、内嵌Tomcat、自动装配都收进了约定里跑一个Java后台再也不需要配一堆XML。MyBatis Plus则在MyBatis之上补齐了通用CRUD和分页插件你不用为每个表写insert、update的XML但SQL又能随时打印出来看不会像Hibernate那样像一个黑匣子。下面这张表是我在给同学讲选型时常用的对比对比项SSH / SSM 老项目Spring Boot MyBatis Plus配置复杂度XML多环境敏感约定大于配置启动快SQL可见性MyBatis XML可见XML与注解都支持日志直出分页实现手写limit/PageHelper分页插件一行搞定适合场景老系统维护课程设计、新后台、面试练手对Java基础还不太扎实的同学MyBatis Plus还有一个隐性好处实体类上标注TableName、TableId之后BaseMapper自带的单表方法已经覆盖80%的操作你可以把精力放在事务、状态机、缓存这些真正值钱的地方。这也正好呼应了Java面向对象里封装复用的思想答辩时能讲出道理。2.2 用Maven搭出最小可运行骨架pom.xml与application.yml怎么配先说环境。JDK 8或11、Maven 3.6、MySQL 5.7或8.0、Redis 5这四样缺一不可。Java环境变量配置是Java基础里的老生常谈但几乎每个人都栽过一次装完记得在命令行依次执行java -version、mvn -v、mysql --version确认有输出再继续。我接手过的源码里三分之一跑不起来就是环境变量或JDK版本不对而不是代码本身有问题。pom.xml是整个Maven项目的依赖清单核心依赖就这几个parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.14/version relativePath/ /parent properties java.version1.8/java.version mybatis-plus.version3.5.3.1/mybatis-plus.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesstarter-web负责接口和Tomcatmybatis-plus-boot-starter是数据访问层核心mysql驱动只在运行时需要starter-data-redis管缓存jjwt负责生成登录令牌lombok用注解消除实体类的getter/setter样板代码。版本号不必死守按你本地依赖拉到的版本微调即可但Spring Boot 2.x和MyBatis Plus 3.x是搭配好的组合别混到3.0以下的老版本。接着写application.yml这是项目启动时最关键的配置文件server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mall_admin?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 database: 0 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这段配置里最容易翻车的是serverTimezoneAsia/Shanghai漏掉它MySQL 8下启动就会报时区错误。第二个容易忽略的是redis.database: 0如果你项目里缓存命名空间多建议db0放业务缓存、db1放验证码这样redis-cli里flushdb不至于把全站缓存一次清光。MyBatis Plus的逻辑删除配置意味着删除操作会变成update set deleted1而不是硬删做电商后台时订单、商品这类数据必须保留痕迹。2.3 启动类与统一返回体让前后端从第一天就对齐骨架的最后两块是启动类和统一返回体。启动类很简单import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication MapperScan(com.mall.admin.mapper) public class MallAdminApplication { public static void main(String[] args) { SpringApplication.run(MallAdminApplication.class, args); } }SpringBootApplication是Spring Boot的入口组合注解包含自动配置和组件扫描。MapperScan把com.mall.admin.mapper包下所有Mapper接口扫描成Bean省去在每个接口上写Mapper。如果你漏了这行启动时不报错但一调用接口就会提示找不到Mapper Bean这类错误最容易让人绕远路。统一返回体是我特别强调的一点。前后端分离项目里接口必须约定统一结构否则有的接口返回Map、有的返回String前端axios拦截器没法写import lombok.Data; Data public class ResultT { private Integer code; private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMsg(ok); r.setData(data); return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.setCode(500); r.setMsg(msg); return r; } }约定code200成功、401未登录、500业务失败前端只看code字段非200就弹出msg。后端所有Controller统一返回ResultT接口文档都好写很多。这个习惯不是我发明的只要你以后进任何Java团队几乎都会看到类似的包装类早点养成不吃亏。3. 核心业务模块商品、订单、权限与缓存从表结构到接口一次打通骨架搭好之后真正的业务模块才是这套源码的肉。我按电商后台最典型的主链路来讲建表、商品查询、下单、权限控制、Redis缓存。顺着这条线走完你对整个源码的理解就成型了。3.1 数据库表设计五张核心表先讲清楚再动手电商后台再怎么复杂落到表上就是用户、分类、商品、购物车、订单这几类。我建议至少建立下面六张核心表表名作用关键字段ums_user后台与前台用户id, username, password, nickname, statuspms_category商品分类id, parent_id, name, sortpms_product商品主表id, category_id, name, price, stock, pic, status, versionoms_cart购物车id, user_id, product_id, quantity, checkedoms_order订单主表id, order_sn, user_id, total_amount, status, create_timeoms_order_item订单明细id, order_id, product_id, product_name, price, quantity表命名用模块前缀是我一直推荐的ums用户模块、pms商品模块、oms订单模块。这样不仅看着整齐面试时也能说清楚业务分层。订单状态字段用tinyint0待支付、1已支付、2已发货、3已完成、4已取消在代码里用常量类定义千万别在Service里直接写魔法数字0、1、4不然三个月后你自己都看不懂。金额字段必须用DECIMAL(10,2)这是纪律。用float或double存钱Java经典的计算误差会在订单总额上翻车这是Java基础里最常见的坑。库存字段stock要配一个version字段后面扣库存要靠乐观锁没有版本号就只能锁表并发稍微上来就出事。外键我建议不加。很多课程设计硬性要求外键但真实电商系统里外键会让删除变得困难还会影响写入性能。表之间的关系用逻辑字段维护事务保证一致性已经够用。如果你答辩被问到“没有外键怎么保证数据完整性”答案就是数据库事务应用层校验这比外键更贴近生产环境。3.2 商品管理从Mapper到Controller把分页查询一次讲透商品管理是后台最简单的CRUD但也最能看出一个人代码功底。实体类这样设计import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; Data TableName(pms_product) public class Product { TableId(type IdType.AUTO) private Long id; private Long categoryId; private String name; private BigDecimal price; private Integer stock; private String pic; private Integer status; Version private Integer version; TableLogic private Integer deleted; }TableName指定表名TableId(type IdType.AUTO)表示数据库自增主键Version让MyBatis Plus在update时自动带上版本号条件TableLogic对应yml里的逻辑删除字段。注意name字段不要起名叫name以外的同义词Spring Boot里不涉及关键字问题但desc、rank这类词尽量避开MySQL 8下有些是保留字。Mapper接口不需要手写SQLpublic interface ProductMapper extends BaseMapperProduct { }BaseMapper自带selectById、selectPage、insert、updateById等方法单表查询完全够用。真正的业务逻辑放在Service层import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.mall.admin.mapper.ProductMapper; import com.mall.admin.entity.Product; import org.springframework.stereotype.Service; import javax.annotation.Resource; Service public class ProductService { Resource private ProductMapper productMapper; public PageProduct pageProducts(String keyword, Long categoryId, int pageNum, int pageSize) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); // 第一个参数是condition条件为false时自动跳过该查询条件 wrapper.like(keyword ! null, Product::getName, keyword) .eq(categoryId ! null, Product::getCategoryId, categoryId) .orderByDesc(Product::getId); return productMapper.selectPage(new Page(pageNum, pageSize), wrapper); } }like方法的第一个参数是booleankeyword为null时这行条件就不会拼进SQL这比手动if拼接干净得多。selectPage的Page对象里已经封装好了records、total、current等字段前端分页组件直接能用。这里必须提醒如果你没注册分页插件selectPage只是内存假分页查全表再截取总数和页码全不对这个坑我在第4章详细展开。Controller层保持薄只做参数接收和返回import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; RestController RequestMapping(/api/product) public class ProductController { Resource private ProductService productService; GetMapping(/page) public ResultPageProduct page( RequestParam(required false) String keyword, RequestParam(required false) Long categoryId, RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize) { return Result.ok(productService.pageProducts(keyword, categoryId, pageNum, pageSize)); } }pageNum从1开始pageSize默认10前端el-table或element分页器直接对接。首页的商品列表接口要放行后台的商品新增、修改接口则需要登录鉴权这个后面在拦截器里统一处理。3.3 订单流程与状态机下单、支付回调、取消订单怎么设计订单模块是整个后台最复杂的部分它的难点不在CRUD而在状态流转。一个订单不能从待支付直接跳到已完成也不能从已发货跳回待支付。用状态机约束流转比在代码里散落大量if (status 1 ...)要可靠得多。状态机核心逻辑就是一张流转表0待支付只能到1已支付或4已取消1已支付只能到2已发货这是电商后台面试里必问的题。下单方法是最需要小心的代码涉及事务、并发、库存三个难点import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.stream.Collectors; Transactional public Long createOrder(Long userId, ListCartItem items) { // 1. 计算总价校验商品状态 BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { Product product productMapper.selectById(item.getProductId()); if (product null || product.getStatus() ! 1) { throw new RuntimeException(商品不可购买 item.getProductId()); } total total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 2. 插入订单主表 Order order new Order(); order.setUserId(userId); order.setOrderSn(ORD System.currentTimeMillis()); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 3. 插入明细扣减库存 for (CartItem item : items) { int rows productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(库存不足 item.getProductId()); } OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); } // 4. 清空对应购物车 cartMapper.deleteByUserIdAndProductIds(userId, items.stream().map(CartItem::getProductId).collect(Collectors.toList())); return order.getId(); }Transactional保证这四个步骤要么全成要么全败任何一步抛出RuntimeException都会回滚。这里有个容易忽略的细节事务注解在同类内部方法调用时会失效比如在同一个类里用this.createOrder()调Spring的代理对象没走进去事务控制就没了。所以一定要由Controller注入Service再通过注入的这个Service调方法。库存扣减的SQL是防超卖的关键Update(UPDATE pms_product SET stock stock - #{quantity}, version version 1 WHERE id #{productId} AND stock #{quantity} AND deleted 0) int deductStock(Param(productId) Long productId, Param(quantity) Integer quantity);核心是WHERE stock #{quantity}这个条件。在高并发下两个请求同时读到库存100都执行update数据库的行锁会让后到的那个更新在stock quantity判断时返回0从而失败。如果你先select查库存再if判断再执行update并发请求会把库存打成负数这是超卖问题最常见的根因。这个知识点在Java面试里对应“怎么保证数据一致性”能讲清楚这个案例比背十道八股文都有用。支付回调和取消订单是另外两个必须写状态判断的入口。支付回调不能只信同步请求里的参数要在回调里校验签名和金额然后把状态从0改成1取消订单前必须判断当前状态是0否则用户点了取消却货已发出会造成资损。这些边界场景我不会在源码里写得特别豪华但每个都会补上if (order.getStatus() ! 0)这样的判断答辩时能讲出来就是加分项。3.4 权限控制与接口鉴权JWT 拦截器守住后台接口后台管理系统和前台最大的区别是后台的接口必须鉴权不能有人拿到地址就随便改商品、删订单。现在前后端分离项目普遍用JWT不用Session原因是Session依赖Cookie域分布式部署时还要做会话共享而JWT本身就是无状态的令牌后端只要验签就能拿到用户ID和用户名。JWT工具类核心就两个方法生成和解析import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; public class JwtUtil { private static final String SECRET mall-admin-secret-key-please-change; private static final long EXPIRE 24 * 60 * 60 * 1000L; public static String createToken(Long userId, String username) { return Jwts.builder() .claim(userId, userId) .claim(username, username) .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(); } }SECRET是签名密钥放代码里只适合课程设计演示真实项目要放到配置文件或配置中心。EXPIRE设为24小时后台管理场景够了如果做C端App建议改成7天并加自动续期逻辑。登录接口验证用户名密码通过后调用createToken返回给前端前端把token存localStorage每次请求在Header里带Authorization: Bearer token。拦截器负责统一校验token这是Spring MVC里做鉴权的标准姿势public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }解析成功就把userId塞进request属性后续Controller可以从request.getAttribute取到当前操作人。失败统一返回401前端拦截到401就跳回登录页。注册拦截器时要注意白名单Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/user/register, /api/product/page); } }商品分页接口放行是因为前台浏览不需要登录但后台新增、修改、删除商品必须登录。这里顺带回应一个Java面试高频问题拦截器Interceptor和过滤器Filter的区别。Filter在Servlet容器层先执行Interceptor在Spring MVC层能通过HandlerMethod知道请求具体要调到哪个Controller方法所以做细粒度权限控制更精确。你要是能把这句话讲出来面试官就知道你不是只会用注解。3.5 Redis缓存商品信息穿透、击穿、雪崩的兜底写法商品详情是电商后台访问量最高的接口不缓存就是每次刷新都打一次MySQL。但Redis缓存不是简单set进去就完事面试里最常考的穿透、击穿、雪崩三个问题电商后台里全都能遇到。给一个最小完整的兜底写法public Product getProductDetailCached(Long productId) { String key product:detail: productId; String json stringRedisTemplate.opsForValue().get(key); if (json ! null) { if (EMPTY.equals(json)) { return null; // 缓存空值拦截不存在商品的穿透请求 } return JSON.parseObject(json, Product.class); } // 互斥锁热点key过期瞬间只让一个线程去查数据库 String lockKey product:lock: productId; if (Boolean.FALSE.equals(stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 3, TimeUnit.SECONDS))) { Thread.sleep(50); return getProductDetailCached(productId); } try { Product product productMapper.selectById(productId); if (product null) { stringRedisTemplate.opsForValue().set(key, EMPTY, 300, TimeUnit.SECONDS); } else { // 随机过期时间避免大量key在同一秒失效 long expire 600 (int) (Math.random() * 300); stringRedisTemplate.opsForValue().set( key, JSON.toJSONString(product), expire, TimeUnit.SECONDS); } return product; } finally { stringRedisTemplate.delete(lockKey); } }这段代码三个意图很清晰缓存空值解决穿透因为恶意请求会拿大量不存在的ID打数据库数据库查不到缓存也不会有每次都穿透到MySQLsetIfAbsent互斥锁解决击穿因为热点商品key过期一瞬间会有几百个请求同时去查库拿不到锁的线程先睡50毫秒再递归只有第一个线程真正查库随机过期时间解决雪崩因为所有商品key如果固定600秒过期整点容易成片失效数据库压力会瞬间拉满。与数据库的一致性怎么保证我一般用“先更新数据库再删除缓存”的策略Transactional public void updateProduct(Product product) { productMapper.updateById(product); // 更新成功后删除缓存下次读取时再回填 stringRedisTemplate.delete(product:detail: product.getId()); }为什么不先删缓存再更新数据库因为如果你删完缓存、数据库更新失败后面的请求会把旧数据重新写回缓存价格改了等于没改。先更新数据库再删缓存即使删缓存失败最多是缓存里还是旧数据但下一次更新或定时任务能兜底。更严谨的做法是延迟双删删完缓存等100毫秒再删一次防止并发读写把旧值又插回去。这块内容对应Java面试里“怎么保证数据一致性”的经典问题把这两个策略讲明白已经足够撑起半个小时的追问。4. 排查篇这套电商后台管理系统源码里的常见坑现象、原因与解决办法跑这套源码最花时间的往往不在功能而在环境与版本之间的暗自较劲。下面五个坑是我调这类项目时反复撞见的每一条按“现象→原因→解决”来讲你照着排除能省下大半天。4.1 启动时报错“Access denied for user root”或数据库连接失败现象Maven编译通过启动Spring Boot后控制台刷出一串红色日志提示Access denied for user root或者报Communications link failure。原因多数不是代码问题而是application.yml里的数据库密码和本地MySQL不一致或者MySQL 8换了驱动类你还用老的com.mysql.jdbc.Driver也可能是时区参数没带导致连接被拒绝。解决先用命令行验证MySQL能登录mysql -uroot -p输入同样密码能进说明密码没问题。再用show variables like port确认端口是不是3306。MySQL 8必须用com.mysql.cj.jdbc.Driverurl里带serverTimezoneAsia/Shanghai否则会报时区无法识别。还有一种情况是密码里有特殊字符比如、#yml里需要加引号包起来。提示以后凡是启动报“connection”相关错误第一步永远先确认不是环境问题再去看代码。用telnet 127.0.0.1 3306测一下端口通不通比反复重启项目高效得多。4.2 分页查询“每页能返回数据但总数永远是0或不对”现象调用商品分页接口pageNum1, pageSize10records有数据但total显示0或者换页时数据重复第二页还是第一页的内容。原因MyBatis Plus的selectPage必须配合分页插件PaginationInnerInterceptor否则Page对象是内存假分页先查全表再在Java里截取total自然不准。这是MyBatis Plus最常见的翻车点十个人里至少有六个会踩。解决单独建一个MyBatis Plus配置类注册分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); // 超过最大页数直接报错避免静默回退到第一页 pagination.setOverflow(false); // 限制单页最大条数防止有人传pageSize100000 pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }setOverflow(false)表示页码越界时报错而不是自动跳回第一页这样能暴露前端组件的越界问题。setMaxLimit(100)是给接口加保险避免有人恶意传超大pageSize把数据库拖垮。注意这个配置类必须能被Spring扫描到放在启动类同包或子包下。4.3 日期字段在JSON里变成一串数字或者前端传日期报解析失败现象接口返回的createTime变成1714552200000这种毫秒数或者前端传2024-05-01 12:30:00Spring报JSON parse error: Cannot deserialize value of type java.util.Date。原因Jackson默认把java.util.Date序列化成时间戳而LocalDateTime它默认没有内置反序列化器两边格式没对齐。这是Spring Boot项目里非常典型的“本地好好的联调就崩”问题。解决在application.yml里统一配置日期格式并让实体字段处理两个方向JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;JsonFormat在序列化和反序列化时都生效timezone必须写成GMT8否则半夜时间会差8小时。同时确认pom里用的是spring-boot-starter-web而不是单独引了一个旧版jacksonSpring Boot已经自动管理jackson-datatype-jsr310不需要自己再重复加依赖加了版本冲突反而解析失败。4.4 修改商品后Redis里还是旧价格前台展示不对现象后台把商品价格从100改成80接口返回成功但商品详情页还是显示100。刷新几次偶尔变80偶尔又变回100。原因缓存更新顺序写反了。常见错误是先删除缓存再更新数据库更新数据库失败后缓存已经没了后面的请求把旧数据重新填回缓存另一种错误是更新数据库之后直接把当前对象写入缓存但当前对象是更新前查出来的旧数据。解决先更新数据库再删除缓存让下次读取时重新回填Transactional public void updateProduct(Product product) { productMapper.updateById(product); stringRedisTemplate.delete(product:detail: product.getId()); }顺序一定是先库后缓存不能颠倒。为了应对极端情况下的并发读写可以在删除缓存后再等100毫秒删除一次这就是常说的延迟双删。课程设计阶段做到先更新再删除已经及格把延迟双删的面试口径记住了也可以。4.5 项目本地能跑部署到服务器后接口404且页面打不开现象本地IDE启动访问http://localhost:8080/一切正常打成jar包放到服务器页面能打开但登录接口404或者接口通了页面白屏。原因两种情况要分开看。接口404通常是请求根本没打到后端服务Nginx把/api转发配置写错了页面白屏则大概率是前端打包后的静态文件和后端接口域名不一致或者跨域没配。解决先在后端服务器上确认服务状态curl http://127.0.0.1:8080/api/product/page -X GET能返回JSON说明后端没问题问题在Nginx。检查转发配置location /api/ { proxy_pass http://127.0.0.1:8080; }这里有一个非常隐蔽的坑location /api/末尾有斜杠proxy_pass http://127.0.0.1:8080末尾没有斜杠路径会原样传递后端能收到/api/product/page。如果proxy_pass结尾也加了斜杠Nginx会把/api/前缀吞掉转发过去变成/product/page接口直接404。前后端分离部署时CORS也可以在后端统一配置CorsFilter放行前端域名省得每次打包都改前端。5. 把源码变成面试亮点验证核心流程、压测下单接口、配置幂等键5.1 用一份合理的假数据验证核心流程跑通源码后第一件事不是截图交差而是灌一份像样的假数据。我习惯准备2个用户、3个分类、20个商品然后按这条链路手动走一遍登录拿token分页查商品加购物车下单模拟支付回调取消已付款订单期望被拒绝。这六步走完后台的登录、商品、购物车、订单四个模块基本就没大问题了。不要只点一个接口就认为项目通了前后端联调的问题比后端自测多一倍。5.2 压测下单接口先找瓶颈再谈优化用JMeter或ab工具建一个50线程并发下单的压测你会发现错误率集中在库存扣减上。原因是UPDATE pms_product SET stock stock - 1 WHERE stock 1这条语句会锁住商品行所有并发请求排队执行。优化思路是把库存预热到Redis里用Redis的原子递减做预扣再异步同步到MySQL这样吞吐量能提升一个量级。这个优化很值得写进Java面试“怎么保证数据一致性”的回答里既有代码又有数据比空谈理论扎实得多。5.3 一个加分做法为下单接口加幂等键连续点击“提交订单”按钮可能产生两笔一模一样的订单。服务端加一个幂等键重复请求直接返回失败String idempotentKey request.getHeader(Idempotent-Key); if (!stringRedisTemplate.opsForValue() .setIfAbsent(order:repeat: idempotentKey, 1, 1, TimeUnit.MINUTES)) { throw new RuntimeException(请勿重复提交); }setIfAbsent用Redis原子性保证同一时间只有一个请求能设置成功第二个请求进来会失败直接拦截。三行代码就把重复下单的问题解决了这笔账面试官很买账。我带课程设计时最常说的一句话是源码能跑起来只是起点能把它讲清楚才叫真正做完。这套基于Java语言的电商后台管理系统设计源码跑通不难难的是把表结构、订单状态、缓存一致性这些决定系统质量的地方真正吃透。调试时把日志打开把每个异常当成一次学习机会你收获的会远远超过作业本身。希望帮到你。本文还有配套的精品资源点击获取
返回列表