
1. 选这个题目的真实逻辑与整体设计1.1 为什么是“商城积分系统”——毕业设计选题的底层考量每年到毕业季都有大量学生来问Java选题的事。我见过太多人选了“图书管理系统”“宿舍管理系统”“课程管理系统”这类题代码量不大、业务逻辑简单看着是完成了但一到答辩就被导师问住你这个系统的技术难点在哪为什么用这张表不用那张表有什么并发场景——答不上来。商城积分系统这个题目最大的价值在于它把“电商核心链路”切了一个足够有深度的剖面。积分系统本身不是一个大而全的商城但它的业务核心却完整覆盖了用户体系、商品体系、订单体系、营销规则体系、流水对账体系。你做一个积分系统等于把Spring Boot实战中最常被面试官拷问的几个点全部串起来了用户鉴权、事务管理、并发控制、缓存设计、接口幂等、分布式锁、定时任务。更实际的一点是这个题目做起来不会失控。商城完整项目动辄几十张表积分系统把范围聚焦在“赚积分”“花积分”“对账”这三件事上业务闭环清晰每张表都有明确存在的理由。答辩的时候你不需要背模板因为每一步设计都是你自己推导出来的深度自然就出来了。从我带过的项目来看积分系统这个题目的完成度上下限都很大敷衍做法是写一套简单的增删改查接口把积分加减做成一条update语句认真做法是做出积分余额、积分流水、签到连续规则、兑换并发控制、订单超时回滚、幂等防重。这篇文章就是按“认真做法”的标准来拆解整个项目从数据库设计到关键代码到你会踩的坑一次讲透。1.2 技术栈选型Spring Boot为核心的前后端分离方案这个项目的技术选型我直接说结论后端Spring Boot MyBatis-Plus MySQL Redis前端Vue 3 Element Plus鉴权用Sa-Token或者JWT定时任务用Spring原生Scheduled就够了。很多人纠结要不要上Spring Cloud、要不要用RabbitMQ、要不要用Seata分布式事务。这里必须先泼一盆冷水毕业设计的核心目标是证明你掌握了工程化的Java开发能力不是证明你背了多少分布式中间件的概念。单机部署的Spring Boot项目照样可以把并发、缓存、事务这些“硬功夫”体现得明明白白。你要在答辩时让人信服的是你对某一个技术点的深度理解而不是技术栈的数量。Spring Boot版本的选择我建议用稳定版本不要追新。写这篇文章时Spring Boot 3.x已经普及但如果你用JDK 8就老老实实用2.7.x版本用JDK 17才考虑3.x。热搜词里有一条是“springboot版本太高”导致的各种问题十有八九是版本和JDK、依赖插件不匹配。后面我会专门把这类问题汇总成一个坑位清单。Redis在这个项目里的地位很微妙。它不是一个“为了用而用”的组件而是积分系统的刚需——积分的实时余额展示、签到防重复、兑换商品时的库存预扣、积分流水号生成全都依赖Redis。但你要注意面试官会问的问题Redis里的积分余额和数据库不一致怎么办答案不是用分布式事务而是用“Redis做缓存DB做最终数据源定期对账”的经典架构。鉴权我用Sa-Token因为它的API对新手更友好登录、踢人下线、权限校验的代码量比手写JWT拦截器少很多而且它天然集成了Redis会话管理。如果你之前只接触过Spring Security从零上手Sa-Token大约只需要半天对毕业设计的时间线非常友好。2. 积分怎么会“爆雷”业务建模与表结构设计2.1 积分从哪来到哪去先把业务规则想清楚再写代码写积分系统之前最忌讳的事情是直接建表。你先要回答一个问题积分在这个系统里的生命周期到底是什么我习惯把积分拆成两条链路赚积分链路用户注册、每日签到、连续签到额外奖励、购物下单获得积分、参与活动获得积分、管理员手动调整积分。这里面每条规则都可以独立开关、独立配置数量上限。一定要考虑“每天最多获取多少积分”这类限制不然用户用脚本刷签到你的系统分分钟被薅秃。花积分链路积分兑换商品消耗积分、积分抵扣现金下单时按比例抵扣、积分兑换优惠券、积分抽奖。花积分时最核心的规则是“积分一旦扣减必须立刻产生一条不可篡改的流水记录”这是后续所有对账和售后的基础。你可以把积分理解成银行的存款账户用户看到的是余额底层是每一笔存取明细。绝不能只维护一个“用户积分数”字段那样一旦出bug数据错了都查不到原因。积分流水就是积分系统的“账本”余额可以不对流水不能丢。从业务流程上我把整个系统划分为五个模块用户与鉴权模块登录注册、个人信息、积分余额查询积分资产模块积分账户、积分流水、冻结/解冻赚积分模块签到、任务、购物奖励、后台调整花积分模块积分商城、商品兑换、订单管理运营配置模块积分规则配置、商品上下架、公告管理这个划分方法在答辩时可以直接当系统架构图来讲——每个模块的职责、模块间的关系、数据流向都在你脑子里比背PPT强一百倍。2.2 数据库设计六张核心表与字段规划的完整思路数据库是整个项目的地基。我见过太多人把表设计得像Excel表字段堆一大堆结果后期改需求时痛苦不堪。积分系统的表我建议按以下结构来用户表sys_user用户ID、用户名、密码BCrypt加密、手机号、状态、创建时间。积分余额不要放在用户表因为积分是高频变更字段放一起会拖累用户表的查询性能。积分账户表points_account账户ID、用户ID、可用积分、累计获得积分、累计消耗积分、版本号乐观锁用、更新时间。这张表一个用户最多一条记录对应“银行存款账户”的概念。积分流水表points_record流水ID、用户ID、积分变动值正数增加、负数扣减、变动类型签到、兑换、购物奖励、人工调整、关联订单号、变动后余额、创建时间。这张表是只追加的物理不删除查询量大后要按时间做分表或归档。签到表points_sign_in签到ID、用户ID、签到日期、连续签到天数、获取积分、签到时间。专门独立一张表是为连续签到规则服务的判断“昨天签没签”时直接查这张表。商品表points_goods商品ID、商品名称、图片、所需积分、库存、限兑数量、上下架状态、版本号。积分商城的商品和普通商城商品不一样它没有真实价格只有”积分价“所以字段上会更轻量。兑换订单表points_order订单ID、用户ID、商品ID、消耗积分、商品快照名称/图片、状态待发货、已发货、已完成、已取消、创建时间、发货时间。为什么单独建账户表而不是把积分余额直接放用户表原因有两个。第一是性能用户表会被登录、个人信息等高频查询访问而积分余额的修改极其频繁如果同一条记录上有频繁的写操作数据库的行锁竞争会影响整个用户模块。第二是逻辑清晰积分账户和用户资料是两个领域模型分开后你甚至可以把积分独立成一个微服务——虽然毕业设计不这么做但这个设计思路可以讲给导师听。字段类型也值得说几句。积分值用BIGINT不要用INT因为你不确定未来运营活动会不会让积分总量超过21亿INT上限提前用大字段免去后期迁移的痛苦。金额相关的字段用DECIMAL(10,2)不要用FLOAT或DOUBLE数据库的浮点类型在精度上会出问题这是老生常谈但还是有人踩。2.3 积分流水的唯一性为什么不靠自增ID积分流水表的主键不要用自增ID。原因是积分流水在插入时需要唯一业务编号方便做幂等和排查问题。我的做法是用“日期 用户ID 随机数”生成一个业务流水号或者直接用Redis的INCR生成趋势递增序列。在Java代码里我会写一个专门生成流水号的组件Component public class PointsSerialGenerator { Resource private StringRedisTemplate redisTemplate; private static final String SERIAL_PREFIX points:serial:; public String generate(Long userId) { String date LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); Long seq redisTemplate.opsForValue().increment(SERIAL_PREFIX date); // 防止无限增长24小时后key过期 redisTemplate.expire(SERIAL_PREFIX date, 1, TimeUnit.DAYS); return date String.format(%06d, userId) String.format(%08d, seq); } }这里有个很实际的经验Redis的INCR命令在并发下能保证唯一且有序比数据库自增更适合做流水号。而且这个流水号里天然含有日期信息排查问题时一看就知道是哪天的数据。在数据库层面对这个字段加唯一索引还能防止接口重复提交导致重复入账。3. 核心功能落地赚积分、花积分、防作弊3.1 签到送积分从接口设计到连续签到实现签到是积分系统里最简单也最容易写错的功能。表面看就是一张签到表插入一条记录但用户可能“一天签多次”“补签”“连签判断错误”每个细节都能写出一堆bug。我的签到接口设计如下PostMapping(/sign/in) public ResultString signIn(RequestParam Long userId) { // 1. 从Redis中判断今天是否已签到 String signKey sign: userId : LocalDate.now(); Boolean hasSigned redisTemplate.hasKey(signKey); if (Boolean.TRUE.equals(hasSigned)) { return Result.error(今天已经签到过了); } // 2. 查询昨天是否签到判断连续天数 LocalDate yesterday LocalDate.now().minusDays(1); Integer continuousDays pointsSignInMapper.getContinuousDays(userId, yesterday); int todayContinuous (continuousDays null ? 0 : continuousDays) 1; // 3. 计算本次签到所得积分连续签到的积分递增规则 int awardPoints calculateAwardPoints(todayContinuous); // 4. Redis先置标记防止并发重复签到 redisTemplate.opsForValue().set(signKey, 1, 24, TimeUnit.HOURS); // 5. 写入签到记录 增加账户积分 写流水事务 pointsSignInService.doSignIn(userId, todayContinuous, awardPoints); return Result.success(签到成功获得 awardPoints 积分); }这里有几个细节要特别注意。关于并发控制第1步到第4步之间有一个时间窗口两个请求可能同时通过第1步的判断。解决的办法就是第4步用Redis的setIfAbsentSETNX只有在key不存在时才能设置成功。正确写法是Boolean success redisTemplate.opsForValue().setIfAbsent(signKey, 1, 24, TimeUnit.HOURS); if (Boolean.FALSE.equals(success)) { return Result.error(今天已经签到过了); }关于连续签到积分规则不要写死为固定值。运营规则说变就变比如“连续签到7天额外奖励100积分”这个配置应该放在规则配置表或者Nacos配置中心程序只负责读取。我的做法是写一个PointsRuleService把所有积分规则封装成可配置项。关于数据库唯一约束即使Redis前缀判断有漏洞数据库也有保底方案——签到表加唯一索引(user_id, sign_date)重复插入直接报错避免脏数据。这是“双保险”思想缓存层拦截并发数据库层做最终兜底。3.2 商品兑换库存扣减与积分扣减的一致性方案积分兑换商品是整个项目里最容易出并发问题的地方。假设某商品库存只剩1件同时有10个用户发起兑换请求如果代码写成“先查库存库存大于0再扣减”就会超卖——多个请求同时读到库存为1同时通过校验然后全部扣减成功。标准的解决方案有两种乐观锁和Redis预扣库存。毕业设计阶段我推荐把两种都实现答辩时有东西可讲。乐观锁方案在商品表加一个version字段扣库存时使用带条件的更新int rows pointsGoodsMapper.updateStockByVersion(goodsId, 1, version); if (rows 0) { throw new BusinessException(商品库存不足或已更新请重试); }对应的SQL是UPDATE points_goods SET stock stock - 1, version version 1 WHERE id #{goodsId} AND stock 1 AND version #{version}这条SQL的巧妙之处在于stock 1条件从数据库层面保证了不会扣成负数version #{version}条件保证了并发更新时只有一个请求能成功。两个条件合起来超卖问题被“数据库行锁”天然解决了。但这还不够积分扣减和库存扣减必须放在同一个事务里Transactional(rollbackFor Exception.class) public void exchange(Long userId, Long goodsId) { // 1. 查商品信息 PointsGoods goods pointsGoodsMapper.selectById(goodsId); if (goods null || goods.getStatus() ! 1) { throw new BusinessException(商品不存在或已下架); } // 2. 查用户积分余额 PointsAccount account pointsAccountMapper.selectByUserIdForUpdate(userId); if (account.getAvailablePoints() goods.getRequiredPoints()) { throw new BusinessException(积分不足); } // 3. 乐观锁扣库存 int stockRows pointsGoodsMapper.updateStockByVersion( goodsId, goods.getVersion()); if (stockRows 0) { throw new BusinessException(手慢了商品已被抢完); } // 4. 账户积分扣减乐观锁 int accountRows pointsAccountMapper.deductPoints(userId, goods.getRequiredPoints()); if (accountRows 0) { throw new BusinessException(积分不足或账户异常); } // 5. 生成兑换订单 PointsOrder order new PointsOrder(); // ... 设置订单字段 pointsOrderMapper.insert(order); // 6. 记录积分流水 pointsRecordMapper.insert(buildRecord(userId, -goods.getRequiredPoints(), order.getId())); }这里重点说明step 4的deductPointsUPDATE points_account SET available_points available_points - #{points}, total_consumed total_consumed #{points}, version version 1 WHERE user_id #{userId} AND available_points #{points}用available_points #{points}条件防止积分扣成负数。为什么要在查询用户积分时用selectByUserIdForUpdate因为select ... for update会对这条账户记录加行锁防止两个兑换请求同时对同一账户扣减。虽然前面有乐观锁但这里是双保险——一个锁DB行一个锁账户行。Redis预扣库存方案为了提升性能还会用Redis先扣一遍Long remain redisTemplate.opsForValue().decrement(goodsStockKey goodsId); if (remain null || remain 0) { // 回补库存 redisTemplate.opsForValue().increment(goodsStockKey goodsId); throw new BusinessException(库存不足); }这个方案响应快但Redis和DB的数据一致性需要额外处理。毕业设计如果只做一个方案我建议做乐观锁方案能讲清楚原理、实现简单、答辩不会翻车。Redis预扣可以作为“性能优化扩展点”写在论文里表明你懂这个方案但不必完全实现。3.3 接口幂等积分系统最容易被忽略的致命问题积分系统的所有写接口都天然面临“重复提交”的风险。用户手抖点了两次签到按钮、前端超时重试、消息队列重复消费、管理后台重复导入——同一个积分操作如果被执行两次就是资金事故。最常见的幂等方案是“前置检查 唯一约束”。以兑换商品为例前端在发起兑换时生成一个requestIdUUID后端把requestId作为订单表的唯一键。这样即使前端重试了十次数据库的唯一索引也会拦截重复插入。PostMapping(/exchange) public ResultString exchange(RequestBody ExchangeRequest request) { // 先查这个请求是否已处理过 PointsOrder existOrder pointsOrderMapper.selectByRequestId(request.getRequestId()); if (existOrder ! null) { return Result.success(兑换成功, existOrder.getId()); } try { pointsExchangeService.exchange(request.getUserId(), request.getGoodsId(), request.getRequestId()); return Result.success(兑换成功); } catch (DuplicateKeyException e) { // 并发重复提交捕获唯一键冲突 return Result.success(兑换成功); } }积分扣减流水表同样加上request_id唯一索引从源头杜绝重复入账。这套方案在答辩时能直接打动导师你不仅考虑了正常流程还考虑了异常流程。3.4 积分过期与定时任务别忘了这些“运营隐藏功能”很多学生做到积分兑换完成就认为项目做完了但实际上真正的商城积分系统还有几个容易被忽略的隐藏功能它们非常能体现设计深度积分过期积分不像钱很多平台规定积分有效期一年过期自动清零。实现方式是定时任务扫描账户表把一年前获得但未使用的积分标记为过期并生成“积分过期”流水。订单超时关闭积分兑换订单在“待发货”状态停留超过N天后系统自动关闭或提醒管理员发货。用Spring的Scheduled定时任务就能实现。签到提醒每晚8点给当天未签到的用户推送提醒这需要一张用户签到状态表配合定时任务扫描。Component public class PointsExpireTask { Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void expirePoints() { LocalDateTime expireTime LocalDateTime.now().minusYears(1); ListLong userIds pointsRecordMapper.selectUserIdsByExpireTime(expireTime); for (Long userId : userIds) { Long expirePoints pointsRecordMapper.sumPointsBeforeTime(userId, expireTime); if (expirePoints ! null expirePoints 0) { // 创建过期流水 扣减账户积分 pointsAccountService.deductPointsWithRecord(userId, expirePoints, 积分过期); } } } }定时任务用Scheduled就够了不要再引入Quartz或XXL-Job。毕业设计项目体量下Spring自带的Scheduled开箱即用而且面试时被问到也容易解释。如果你非要在毕业设计里展示“调度中心”那是另一个量级的复杂度了。4. 实现过程中必踩的坑与排查实录4.1 Spring Boot版本太高引发的连锁反应热搜词里“springboot版本太高”这个问题是这两年学生项目里出现频率最高的情况。很多人新建项目时习惯性地选最新版本然后遇到一堆奇怪问题。我列一个版本适配速查表方便你照着选JDK版本Spring Boot推荐版本MyBatis-Plus推荐版本说明JDK 82.7.183.5.3.2最稳组合资料最多遇到问题好搜JDK 112.7.183.5.3.2同上兼容性没问题JDK 173.0.x - 3.1.x3.5.3.2以上注意javax包名变jakartaJDK 213.2.x3.5.5新特性多但资料相对少Spring Boot 3.x迁移到jakarta.*命名空间后很多旧教程里的import javax.servlet.*直接编译报错。你在搜索问题时一定要先确认搜到的答案是否和你的Spring Boot大版本匹配不然会浪费时间。另一个高频报错是“java: warning: 源发行版 17 需要目标发行版 17”或者“java: error: 无效的源发行版”。这个问题的本质是IDEA里的Project SDK、Project language level和Maven的compiler插件版本不一致。解法是在pom.xml里显式指定编译版本properties java.version8/java.version maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties同时要保证IDEA的Settings - Build Tools - Maven - Importer里选择了JDK 8。这个问题不复杂但每年有大量学生卡在这里。4.2 Lombok不生效与“You arent using a compiler supported by Lombok”这个报错在下载了新版Lombok配旧版IDEA时特别常见java: You arent using a compiler supported by lombok, so lombok will not work.原因基本只有一个IDEA内置的编译器版本太老而Maven里引入的Lombok版本需要更新的编译器。解法有两条路径第一条把Lombok固定在一个稳定版本比如1.18.30然后升级IDEA到较新版本或升级IDEA的Java编译器。第二条如果不想升级IDEA就在pom.xml里把Lombok版本降下去比如1.18.24。我的建议是能用Lombok就用但也要有“去掉Lombok也能编译”的自觉。毕业设计项目里用Lombok能省掉大量getter/setter代码让实体类干净很多但如果环境一直报错别死磕直接手动生成getter/setter或使用IDEA的Delombok功能把Lombok注解转成代码。项目能跑通比什么都重要。4.3 循环依赖与Spring Boot自动装配原理Autowired循环依赖这个问题在把“用户服务”和“积分服务”互相注入时非常容易发生。你设计Service的时候可能不经意间就写了UserService依赖PointsService、PointsService又依赖UserService。Spring Boot 2.6之后默认禁止循环依赖启动直接报错。最推荐的处理方式不是开启spring.main.allow-circular-referencestrue而是重构代码把相互调用的逻辑抽到第三个Service里或者使用Lazy延迟注入。举个例子如果UserService要调PointsService获取积分余额同时PointsService要调UserService获取用户信息你其实应该抽一个PointsQueryService专门做跨模块查询避免两个Service直接互相引用。这个设计思想在答辩时很有讲头你从报错中理解了Spring的Bean创建机制并且用了合理的架构手段解决。顺带说一下“Spring Boot自动装配原理”——这是热搜词里出现频率极高的面试题也是导师最爱问的概念。你要讲清楚Spring Boot通过EnableAutoConfiguration配合spring.factories文件3.0后是/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports加载所有自动配置类通过ConditionalOnClass、ConditionalOnMissingBean等条件注解决定是否生效。比如你的项目引入了Redis依赖RedisAutoConfiguration就会被加载并自动创建RedisTemplate和StringRedisTemplate。答辩时如果能现场举一个自己项目里的例子比如“我就是通过查看RedisAutoConfiguration源码发现它默认的序列化器是JDK序列化所以自己手动把Key和Value的序列化方式改成了String和Jackson”这比背定义强十倍。4.4 OutOfMemoryError: Insufficient Memory与JVM配置热搜词里的“java: outofmemoryerror: insufficient memory”我在学生项目里也见过很多次。这种问题分两种场景第一种是编译期报错。启动Spring Boot项目时IDEA分配的内存不够报Could not reserve enough space for object heap或者编译进程直接闪退。解决方法是给IDEA调大虚拟机内存Help - Change Memory Settings把堆内存设在1024MB以上同时在Help - Edit Custom VM Options里检查-Xmx参数是否过小。第二种是运行期OutOfMemoryError: Java heap space。这通常是你的代码内存泄漏比如循环里不断向List添加数据、未关闭的文件流或者数据库连接未释放、查询一次性加载了全表数据。排查步骤是先用jmap -heap pid看堆内存的使用情况和GC配置再用jvisualvm或者JProfiler监控堆上的对象增长重点排查大量实体对象查询、缓存未设置过期、定时任务并发执行等情况我遇到最多的一个场景是Excel导出功能一次性查全量用户数据并写入工作簿几万条数据直接OOM。解决方案是分页查询、分批写入或者使用SXSSFWorkbook流式导出。4.5 Spring Boot项目打包部署与Docker Desktop毕设答辩前把项目打包部署是必须的。你不可能在答辩现场用IDEA跑代码万一环境出问题就尴尬了。最稳妥的方式是打成可执行Jar包放在服务器上用java -jar启动。打包命令特别简单mvn clean package -DskipTests然后运行java -jar points-system-1.0.0.jar --spring.profiles.activeprod但很多学生卡在“本地能跑打包后不能跑”这个坎上常见原因有三类Jar包没包含配置文件检查src/main/resources下的application-prod.yml是否被正常编译进Jar包可用jar tf xx.jar查看。数据库连接地址不对打包后的环境连不到本地MySQL检查application-prod.yml里的数据库URL是否指向实际部署的地址。静态资源路径问题前端Vue项目如果打成静态资源放进src/main/resources/static下要注意路径前缀和前端路由的history模式匹配问题。如果用了history模式后端要配置转发规则否则刷新页面会404。Docker Desktop打包是另一个加分项。写一个简单的DockerfileFROM openjdk:8-jdk-alpine VOLUME /tmp COPY points-system-1.0.0.jar app.jar ENTRYPOINT [java,-jar,/app.jar]构建并启动docker build -t points-system:1.0.0 . docker run -d -p 8080:8080 --name points-system points-system:1.0.0需要注意镜像时区问题和JVM参数调优。openjdk:8-jdk-alpine默认时区是UTC如果你的签到功能按服务器当前日期判断会导致“今天签到”变成“昨天签到”的笑话。解决方法是启动时带上时区参数docker run -d -p 8080:8080 -e TZAsia/Shanghai --name points-system points-system:1.0.04.6 常见问题速查表我把毕设项目中最高频的问题整理成一张速查表方便你在开发时对照排查现象根本原因解决方案启动报Port 8080 was already in use端口被占用换端口或在application.yml中改server.port也可用netstat -ano查占用进程Invalid bound statement (not found)Mapper.xml没扫到检查MapperScan配置和mybatis-plus.mapper-locations路径刷新页面404前端history路由问题后端增加forward规则转发到index.htmlTable doesnt exist数据库脚本没执行用Navicat执行points.sql检查库名是否和配置一致Data too long for column字段长度不够调整数据库字段长度一般VARCHAR(255)起步JSON序列化异常LocalDateTime转换问题引入jackson-datatype-jsr310配置spring.jackson.date-format中文乱码编码不一致确保数据库、连接URL加characterEncodingutf8、IDEA编码都是UTF-8Whitelabel Error Page路由或异常未处理使用RestControllerAdvice统一异常处理返回JSON格式的错误信息5. 答辩准备的技巧与项目扩展方向5.1 从“能跑”到“能讲”答辩时的三个高频拷问项目做完了只是第一步答辩时导师问的问题才是真正的考验。我总结了积分系统被问到的频率最高的三个问题并提供参考答案问题一为什么积分余额和积分流水要分开两张表这是典型的基础问题回答逻辑积分余额是“状态”积分流水是“事件”。状态会变、可以重建而事件是事实、不可修改。分开存储的优点是第一高频扣减只更新账户表流水表只追加避免行锁竞争第二余额出问题时可以用流水表重建具备数据修复能力第三后续要统计“昨天新增了多少积分”“本月消耗了多少积分”直接查流水表就行不需要扫描账户表。问题二Redis在项目里解决了什么问题如果Redis挂了怎么办回答逻辑Redis在项目里有三个核心用途——缓存、分布式锁或SETNX防重、计数器。如果Redis挂了项目不会完全瘫痪因为数据库是最终数据源。用户登录、签到等操作可以降级为直接查数据库只是性能下降。真正的风险点在于“Redis预扣库存”这类依赖Redis的操作所以我的设计里把Redis预扣和DB扣减解耦即使Redis宕机DB的乐观锁仍然能保证数据正确。问题三积分系统的数据一致性是怎么保障的回答逻辑整个系统的数据一致性在不同层面用了不同手段。在并发控制层面账户扣减使用了CAS式更新available_points points条件在库存控制层面使用了版本号乐观锁在防重用层面使用了request_id唯一索引在事务层面所有跨表操作都放在Transactional中任何一个步骤失败整体回滚。5.2 项目还能往哪些方向扩展如果说完了这些还觉得项目展示不够“丰满”可以聊扩展方向。我建议从两个大方向讲第一个是消息队列方向。可以把“下单后发送通知”“积分变动后生成对账单”这类异步操作抽出来用RabbitMQ或RocketMQ解耦。比如用户兑换商品后系统需要发短信通知、给管理员生成发货任务、记录埋点日志这些都不需要同步完成可以发一条消息到队列里由消费者异步处理。第二个是数据分析方向。积分是用户运营的重要抓手可以增加积分趋势统计、用户分层高活跃用户、沉默用户、流失用户、积分获取渠道分析等报表功能。这个方向的实现虽然不难但能体现你对业务价值的理解导师听到“用户运营”“RFM模型”“用户生命周期”这些词会明显觉得你的项目有商业视野。第三个是安全加固方向。比如接口防刷对签到和抽奖接口做限流、积分异常检测短时间内积分变动频率超高自动触发人工审核、操作日志审计。这个方向的实现也不算太难加一个AOP切面就能给所有写操作记审计日志。从我的经验看答辩时最忌的是项目功能一堆但说不清楚为什么这么设计。你只要把本文讲的这些设计原因都理解了、用自己的话能讲出来这个项目就完全够格了。5.3 最后的经验之谈做这个项目最值得投入时间的地方做完整套系统后如果说要给你一个优先级建议我的排序是数据库设计 积分扣减并发控制 流水记录完整性 幂等处理 前端页面美观度。很多学生花了大把时间在调前端样式、换UI组件库、设计花哨的动效上结果最核心的并发控制逻辑反而写得马马虎虎。你要清楚这是Java毕业设计导师更看重的是后端逻辑的严谨性和工程素养。前端只要干净整洁、交互清晰就足够了。我最后想说的是与其把时间花在纠结“选什么题目好”“要不要用最新技术”上不如先把积分系统的核心链路老老实实写一遍。这个项目做完你对Spring Boot的理解、对数据库设计的理解、对线上问题排查的能力都会比刷一百道面试题更扎实。因为面试题背完会忘代码写出来的每一行都是你自己的东西。