ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于SpringBoot的绿色行动平台开发:从MyBatis到Redis与MinIO的工程实践

基于SpringBoot的绿色行动平台开发:从MyBatis到Redis与MinIO的工程实践 1. 项目概述与设计思路1.1 绿色行动平台到底要解决什么问题做这个springboot绿色行动平台之前我先把需求方的话翻来覆去看了好几遍大概捋清楚了这不是一个普普通通的CRUD管理系统而是一个围绕“绿色行为”展开的闭环业务系统。它的核心场景是用户在小程序或Web端完成某些环保行为比如垃圾分类打卡、步行骑行记录、二手物品回收预约、低碳知识答题系统把这些行为量化成“绿色积分”积分再兑换成实物奖励、优惠券或者公益捐赠额度。这类系统的难点不在某个单点功能而在整条链路的状态流转用户行为产生记录记录经过审核有的是自动审核有的是人工抽检审核通过后积分入账积分又要支持冻结、消费、过期清理最后还要对账。最麻烦的是这些动作分散在不同的模块里如果一开始没设计好表结构和事务边界后期改起来想哭。我选springboot作为底座核心原因有两条第一springboot的自动配置和starter机制能把开发节奏提得很快这对“系统设计实现”这类偏毕设或者中小型项目来说意味着可以用最少的配置把项目跑起来精力可以集中在业务逻辑上第二spring生态整合能力很强不管是MyBatis、Redis、MinIO对象存储还是后续要接的RabbitMQ、Elasticsearch都有非常成熟的集成方案。对于一个需要同时处理用户体系、积分体系、行为审核、文件上传的综合性平台来说springboot是最稳的底盘选择。这个项目的目标用户其实是两类人一类是平台运营方他们需要看到行为数据、审核记录、积分发放情况另一类是普通用户他们关注的是如何参与活动、如何查询积分、如何使用积分。这就决定了系统必须做角色权限分离不能像很多课设那样所有功能揉在一个Controller里。1.2 系统全景图功能模块怎么划分按照业务属性我把系统拆成六个核心模块模块核心职责关键实体用户模块注册、登录、个人信息、身份角色普通用户/管理员/审核员user 表行为记录模块记录用户提交的绿色行为支持图片、位置、文字描述behavior_record 表审核模块管理员或自动规则对行为记录进行审核决定是否通过audit_record 表积分模块根据行为类型和审核结果计算积分处理积分的冻结、发放、消费、过期points_account、points_log 表奖励兑换模块商品管理、兑换订单、发货状态reward、exchange_order 表数据统计模块展示绿色行为总量、减碳量估算、用户参与排行基于已有表的统计查询这六个模块不是六个独立的孤岛而是有明确的数据流向。用户先提交行为记录行为记录表带上用户ID、行为类型、描述、图片地址、状态字段待审核/已通过/已驳回。审核通过后系统根据行为类型映射表里的积分规则给用户积分账户增加可用积分同时写一条积分流水。用户拿积分去兑换商品时先做积分冻结订单完成后扣减冻结积分超时未发货则解冻退回。这个设计最有价值的地方在于它把“行为”和“积分”解耦了。行为记录是事实积分是结果两边只靠一张状态字段和一张流水表做关联。这么设计的好处是未来如果新增行为类型只需要加一条行为类型配置不用动积分模块的表结构反过来如果积分规则要调整比如某种行为从10分涨到15分也只需要在配置层改历史数据不受影响。2. 核心技术选型与理由2.1 SpringBoot MyBatis为什么不是JPA也不是MyBatis-Plus选型对比这件事网上一搜一大把我来拿实际开发视角说点不一样的。SpringBoot是项目的容器和调度中心它帮我省了太多的框架配置工作比如Tomcat内嵌、SpringMVC自动配置、数据源自动装配这在以前用SSM框架的时候都得手动敲XML配置。如果你是用SpringBoot 2.7.x版本注意不要一上来就引入SpringBoot 3.x的依赖因为3.x最低要求JDK 17而很多毕设和中小项目环境还是JDK 8。数据持久层我选的是MyBatis原生而不是JPA或MyBatis-Plus有人可能觉得MyBatis-Plus更省事、自带分页插件和Wrapper构造器。但我的观点是对于这个系统原生MyBatis反而更可控。原因有几个第一MyBatis的XML文件能把复杂SQL和Java代码完全分开。绿色行动平台里有大量多表联查统计比如统计每个用户的总积分、某类行为的参与次数、排行榜排序这类SQL放在注解里会非常难看写在XML里可以单独调试通过小于号等特殊符号处理也不会出错。第二SQL手写意味着我对数据库的执行计划有完全的控制权。比如积分流水表会随着用户活跃量增长很快如果没有合适的索引一个简单的SUM查询可能全表扫描。手写SQL我可以在MyBatis XML里随手加force index提示或者调整SQL的JOIN顺序这在ORM自动生成的SQL里处理起来要绕很多弯子。第三项目中涉及批量插入行为记录、批量更新审核状态这类操作原生MyBatis有batchExecutor支持可以在一个Executor里缓存多条SQL然后统一flush性能比逐条insert高非常多。至于JPA对简单CRUD确实快但一旦涉及到复杂的多表聚合它要么需要写JPQL要么需要原生SQL混用反而两头乱。2.2 MinIO对象存储图片和文件往哪儿放绿色行动平台里最容易被忽略但又必须提前想清楚的就是文件存储。用户提交垃圾分类照片、回收物品照片管理员也可能上传活动海报这些都是二进制文件不能直接扔进MySQL里存BLOB否则数据库会快速膨胀备份和查询都会变慢。我的处理方式是引入MinIO这是一个开源的对象存储服务接口兼容S3协议可以部署在本地服务器也可以部署到Linux上通过Docker一键拉起。在SpringBoot里集成MinIO非常简单只需要引入minio的Java SDK然后配置endpoint、accessKey、secretKey通过一个配置类把MinioClient注入到Spring容器里就行。我当时在网上的热词里也看到“minio加入到springboot”这个搜索点确实这是很多人第一次接触对象存储时都绕不开的问题。我贴一下核心的配置方式方便大家直接抄minio: endpoint: http://localhost:9000 access-key: greenplatform secret-key: greenplatform123 bucket-name: green-action对应的Java配置类Configuration public class MinioConfig { Value(${minio.endpoint}) private String endpoint; Value(${minio.access-key}) private String accessKey; Value(${minio.secret-key}) private String secretKey; Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }上传文件的核心逻辑一般是接收到MultipartFile生成一个唯一的对象名用UUID加时间戳调用minioClient.putObject上传到指定的bucket然后把返回的对象访问地址存到行为记录表里。这里有一个开发时容易踩的坑如果你把MinIO部署在同一台机器上并且是通过Nginx做了反向代理那么MinIO返回的URL可能是内网地址小程序端访问不到。这个问题的解法是在返回给前端的文件URL处做字符串替换把endpoint替换成外网可访问的域名。2.3 Redis缓存为什么积分账户一定要做缓存积分账户的读写是整系统里并发压力最大的点。用户完成一个环保行为后会非常频繁地查询积分余额如果每次都去数据库里查MySQL的压力会很大。而且积分变动是需要原子操作的两个人同时增加积分如果并发没有处理好很容易出现丢失更新。我在项目里把Redis用在了两个核心场景第一个场景是积分余额的缓存。每个用户有一个points_account表字段包括total_points、frozen_points、available_points。查询的时候优先从Redis读key设计为points:user:{userId}value用JSON存储积分账户对象。积分变动的时候先更新MySQL再删除或更新Redis缓存。这里要注意的是不能用先删除缓存再更新数据库的策略因为一旦数据库更新失败缓存里就没了数据用户看到的就是积分少了这是很严重的线上事故。合适的做法是先更新数据库成功后再删除缓存下一次查询时重新加载数据库中的最新值。第二个场景是防止重复提交。用户可能在短时间内连续点击多次“提交行为记录”按钮导致同一条行为被录入多次生成多条待审核记录。用Redis的SETNX命令做幂等控制key是submit:behavior:{userId}:{date}比如限制一个用户一天只能对同一类行为提交一次这个逻辑可以做成注解方式在Controller接口上加自定义注解通过AOP统一处理非常干净。用Redis还有了个额外的好处排行榜功能实现起来很简单。积分排行直接用Redis的ZSET数据结构每个成员是userIdscore是总积分查排行时一行命令就出来了还不用写SQL。2.4 接口设计规范统一返回体与异常处理这块属于“看起来不起眼但体验天差地别”的部分。我见过太多项目每个Controller返回的JSON结构都不一样有的是直接返回对象有的在出错时返回null前端对接的时候要写好几套逻辑。我在项目里定义了统一的返回体Result结构是public class ResultT { private Integer code; private String message; private T data; }所有Controller的返回值都包装成Result成功时code为200业务失败时code用自定义状态码比如40001表示积分不足40002表示行为记录不存在。这样前端只需要在axios拦截器里统一处理Result根据code跳转页面或者弹出提示不用每个接口单独判断。异常处理方面我用RestControllerAdvice配合ExceptionHandler做全局异常捕获业务异常统一抛出BizException框架异常如SQL异常、空指针异常捕获后统一返回“系统繁忙请稍后重试”。这里要注意的是千万不要把异常堆栈直接返回给前端你看着方便但会把数据库表结构、内部逻辑全部暴露出去安全上是个大硬伤。3. 数据库设计与核心表结构3.1 行为记录表状态机设计的核心业务系统的表结构从来不是一蹴而就的我在设计behavior_record表时核心思考是这张表能不能支撑起整个行为从发生到积分到账的完整生命周期我最终设计的表结构长这样CREATE TABLE behavior_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT 用户ID, behavior_type VARCHAR(32) NOT NULL COMMENT 行为类型recycle/trash_sort/ride/quiz, title VARCHAR(128) NOT NULL COMMENT 行为标题, description TEXT COMMENT 行为描述, images VARCHAR(1024) COMMENT 图片地址多张用逗号分隔, location VARCHAR(255) COMMENT 提交时的地理位置, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待审核 1-已通过 2-已驳回, audit_remark VARCHAR(255) COMMENT 审核备注, submit_date DATE NOT NULL COMMENT 提交日期用于每日重复校验, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_date (user_id, submit_date), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT绿色行为记录表;这里有两个我特别想强调的设计细节。第一个是多图存储。我看到有不少人用单独一张behavior_image表来存图片然后通过行为记录ID关联。这种设计在规范化上是没问题的但对于这个业务场景来说一张行为记录通常只有一两张图专门建一张子表反而增加查询复杂度。我直接用VARCHAR(1024)存逗号分隔的多个图片URL前端展示时split一下就行。如果未来图片数量多了再考虑改成JSON格式或者拆表多写一个兼容策略就行。第二个是submit_date字段。这是一开始很容易忽略的字段直到做重复校验时才发现。用户提交记录后后端需要判断这个用户今天是否已经提交过同类型的行为如果只比较create_time就要做日期范围查询索引利用率不高。单独设置了submit_date后唯一索引、查询条件都变得非常清晰。状态字段status直接对应审核流程。状态机的流转是待审核-已通过/已驳回。已通过后触发积分发放已驳回后用户可以再次提交新记录之前那条记录保持已驳回状态做审计留痕用。这里不能把驳回的记录直接删掉否则积分审核动作发生的时间点和原因就没法追溯了。3.2 积分流水表每一分钱都要能追溯积分流水表是整个系统最不能含糊的表因为它承担着对账和纠纷处理的功能。用户发现自己的积分不对运营方第一件事就是查积分流水看看每一笔出入账是来自哪个行为、哪个订单。我的设计如下CREATE TABLE points_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT 用户ID, change_type VARCHAR(32) NOT NULL COMMENT 类型earn/spend/freeze/unfreeze/expire, change_amount INT NOT NULL COMMENT 变动数值正数为增加负数为减少, before_balance INT NOT NULL COMMENT 变动前可用积分, after_balance INT NOT NULL COMMENT 变动后可用积分, ref_type VARCHAR(32) COMMENT 关联类型behavior/exchange/system, ref_id BIGINT COMMENT 关联ID行为记录ID/兑换订单ID, remark VARCHAR(255) COMMENT 说明, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT积分流水表;这个表里最重要的一点是同时冗余了before_balance和after_balance。很多初学者写流水只记一个变动数值看起来省事一旦要对账、要判断用户当时账户状态就发现信息不够用。记录变动前后的余额之后即使某条流水因为异常被覆盖了也可以从历史流水推演出准确轨迹。我在这里多说一句积分流水的操作和积分账户的更新必须放在同一个数据库事务里。我的实现方式是在Service层写一个积分变动方法用Transactional注解包裹先查用户积分账户并加行锁select for update然后更新积分余额再插入积分流水三者要么全成功要么全失败。千万不要出现积分余额更新了但流水没插入的情况那会直接导致用户投诉。3.3 行为类型与积分规则配置表这个表是我后补的但补完之后发现它让系统灵活了不止一个等级。如果一开始就把各种行为的积分写死在Java代码的if-else里那每调整一次积分规则都要改代码、重新打包、重新发版运维成本和风险都很高。我把行为类型和积分规则设计成了一张配置表CREATE TABLE behavior_type_config ( id BIGINT AUTO_INCREMENT PRIMARY KEY, behavior_type VARCHAR(32) NOT NULL COMMENT 行为类型编码, behavior_name VARCHAR(64) NOT NULL COMMENT 行为名称, points INT NOT NULL COMMENT 基础积分, daily_limit INT DEFAULT 1 COMMENT 每日可获积分次数上限, need_audit TINYINT DEFAULT 1 COMMENT 是否需要人工审核1-是 0-否, status TINYINT DEFAULT 1 COMMENT 1-启用 0-停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT行为类型积分规则配置表;审核模块在收到一条行为记录后会根据behavior_type去查这张配置表拿到points和need_audit字段。如果need_audit是0则直接自动审核通过积分立刻入账如果是1则进入人工审核队列。这样就把“自动审核”和“人工审核”两种模式统一在了一个流程里前端不需要感知差异。日常运营中运营人员直接在后台管理页面维护这张表就能做到新行为类型上线、积分规则调整、旧类型下线。说实话系统上线之后90%的配置修改都发生在这张表上这比去改Java代码要安全得多。4. 核心功能实现从行为提交到积分到账4.1 行为提交接口事务边界与幂等控制行为提交是整个链路的第一步也是并发压力和脏数据风险最高的一个接口。用户提交一条行为记录带着userId、behaviorType、title、description、images、location等参数过来。我先校验参数再校验行为类型是否存在、是否在启用状态然后校验当天重复性最后落库。关于幂等控制我上面提到了用Redis的SETNX这里补一下伪代码让实操更清楚public ResultLong submitBehavior(BehaviorSubmitDTO dto) { String lockKey submit:behavior: dto.getUserId() : dto.getBehaviorType() : LocalDate.now(); Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofSeconds(30)); if (Boolean.FALSE.equals(locked)) { return Result.error(40003, 今天已经提交过该类型行为请勿重复提交); } try { // 业务校验 落库 behaviorRecordService.create(dto); return Result.success(recordId); } finally { redisTemplate.delete(lockKey); } }注意这里的lockKey最后一定要加LocalDate.now()把维度收到“用户行为类型日期”这个粒度。锁的过期时间设成30秒正常情况下接口耗时不会超过这个时间即使出现了异常锁也会自动释放不会影响用户第二天继续提交。再强调一下事务边界。行为记录的插入是一个独立事务积分发放是另一个独立事务。两者之间的关联由“行为记录状态”来承接。为什么要这么做因为行为记录一旦插入成功这条事实就不可丢弃了即使后续流程因为某种原因失败我们也保留了原始记录可以通过定时任务重新触发积分发放。如果把两个操作放在同一个事务里某个环节失败了用户的提交行为就不存在了这是不对的。用户提交行为的动作已经发生就不能回滚掉。4.2 审核流程实现人工审核的排队机制审核模块的核心是待审核队列。运营人员在后台看到的列表本质是behavior_record表中status0的记录按提交时间倒序排列。我加了一个查询条件通过behavior_type去关联behavior_type_config表优先展示need_audit1的记录自动审核的记录可以直接跳过。这里有一个性能上的考虑审核列表页的SQL是一个典型的分页查询需要join用户表和配置表同时按照status过滤并排序。我给这张表建了复合索引(idx_user_date, idx_status)并在XML中用了 标签动态拼接查询条件避免用户ID为空时把整表数据捞出来。人工审核通过后要调用的核心方法是积分发放。这里把代码拆得清楚一点Transactional(rollbackFor Exception.class) public void approveRecord(Long recordId, Long auditorId) { // 1. 查行为记录校验状态是待审核 BehaviorRecord record behaviorRecordMapper.selectById(recordId); if (record null || record.getStatus() ! 0) { throw new BizException(记录不存在或已处理); } // 2. 查行为类型配置拿积分规则 BehaviorTypeConfig config behaviorTypeConfigMapper.selectByType(record.getBehaviorType()); // 3. 更新行为记录状态 - 已通过 record.setStatus(1); record.setAuditorId(auditorId); behaviorRecordMapper.updateById(record); // 4. 调积分服务发放积分 pointsService.changePoints( PointsChangeRequest.builder() .userId(record.getUserId()) .changeType(earn) .changeAmount(config.getPoints()) .refType(behavior) .refId(recordId) .remark(绿色行为 config.getBehaviorName()) .build() ); }为什么在同一个Service方法里用同一个事务因为审核这个操作的口径就是“要么审核通过且积分到账要么整个失败回滚用户依然是待审核状态”。这和提交行为时的两个独立事务策略不同原因在于提交行为是用户侧动作审核是管理侧动作两者的业务语义不一致事务边界自然不同。驳回操作就更简单了只需要更新状态为2附上auditRemark不涉及积分操作。我在实现审核时还加了一个小功能审核员只能看到与自己负责的行为类型匹配的记录。比如负责垃圾分类审核的人看不到骑行记录这是通过审核员配置表里的behavior_type权限控制的虽然这个功能在毕设中不是必须的但做出来后会明显提升系统的专业度。4.3 积分兑换流程冻结、扣减、超时解冻兑换模块的流程是用户选择商品 - 校验积分是否足够 - 冻结积分 - 生成订单 - 管理员发货 - 扣减冻结积分。这里面最容易被忽视的是“超时未发货自动解冻”。我在exchange_order表里增加了两个字段frozen_expire_time DATETIME COMMENT 冻结过期时间, status VARCHAR(16) COMMENT PENDING-待发货 SHIPPED-已发货 CANCELLED-已取消用户下单时冻结积分的同时设置frozen_expire_time为当前时间加48小时。定时任务每30分钟扫描一次把超过expire_time还没发货的订单状态改为已取消同时把冻结积分解冻回可用积分。这一步如果不做用户凑了很久的积分莫名其妙被冻住了体验非常差。冻结积分和扣减积分的SQL操作用的是Redis缓存加MySQL行锁双保险核心SQL是UPDATE points_account SET frozen_points frozen_points #{amount}, available_points available_points - #{amount}, update_time NOW() WHERE user_id #{userId} AND available_points #{amount}执行后要检查受影响行数如果受影响行数为0说明可用积分不足直接返回“积分不足”错误。注意SQL里硬条件available_points amount这是防止在极端并发下出现负数积分的最后一道防线。5. 项目构建与常见问题排查5.1 Maven项目构建方法与版本配置在网上搜“springboot maven项目构建方法”的人非常多很多初学者卡在第一步。我简要梳理一下我的构建流程。第一步是创建SpringBoot项目我习惯直接用IDEA的Spring Initializr选择Java 8、SpringBoot 2.7.14版本。不用最新版的原因前面说过了JDK 8兼容性最好也有很多插件对高版本SpringBoot支持不好。第二步是在pom.xml中引入必要的依赖。核心依赖除了spring-boot-starter-web、spring-boot-starter-data-redis、mybatis-spring-boot-starter之外还有minio、lombok、hutool工具包。hutool是个好东西里面有很多现成的工具方法比如生成UUID、日期处理、Bean转换能省下很多重复代码。第三步是配置application.yml。这里我把数据源、Redis、MinIO、MyBatis的配置分层放好区分了dev和prod环境。MyBatis配置里特别注意mapper-locations要指向classpath:mapper/*.xml否则MyBatis扫描不到XML文件运行时会报Invalid bound statement。第四步是编译打包。在项目根目录执行mvn clean package -DskipTests把skipTests加上是为了跳过测试省一些时间。如果一切正常target目录下会生成一个green-action.jar的可执行jar包。部署时用java -jar green-action.jar --spring.profiles.activeprod指定生产环境。有一个我踩过的坑分享给读者如果你的项目里同时引入了spring-boot-starter-data-redis和redis.clients.jedis两个依赖在SpringBoot 2.7下会出现Redis连接池冲突。解决方法是把jedis依赖的scope设置为runtime或者直接去掉。5.2 Vue打包放进SpringBoot前后端一体部署很多毕设会把前端用Vue写后端用SpringBoot写然后希望最终部署成一个包方便演示和交付。“vue打包放进springboot中”这个需求简单说就是把Vue打包后的dist目录复制到SpringBoot的src/main/resources/static目录下。具体步骤是前端项目根目录执行npm run build生成dist文件夹。把dist文件夹里的所有内容复制到后端项目的src/main/resources/static目录。重新mvn package打包启动后访问http://localhost:8080就能看到前端页面。前端项目里配置的后端API地址建议用相对路径比如/api/xxx这样在前后端一体部署时就不存在跨域问题。这套方案的好处是演示和部署只需要跑一个jar包不需要额外配置Nginx也不存在跨域CORS的问题。但要注意如果之后把前后端拆开部署一定记得重新配置API代理地址否则前端请求会404。5.3 我踩过的5个高频问题第一个是数据库字段关键字冲突。我在设计积分表时用了level字段结果MySQL的level虽然不是严格意义上的关键字但在某些版本下会报语法错误。后来统一把所有字段检查了一遍把名字是关键字或者疑似关键字的全部改掉比如status、order、desc这种。这里一个实用技巧是建表后用explain select * from table limit 1命令测试一遍能过就说明没有明显语法问题。第二个是LocalDateTime序列化问题。SpringBoot默认的Jackson序列化LocalDateTime会输出一串数字前端格式化起来很头痛。我在配置类里加了全局的Jackson配置统一把LocalDateTime格式化为yyyy-MM-dd HH:mm:ssLocalDate格式化为yyyy-MM-dd。这个配置加完后全接口的日期格式都一致了再也不用为单个字段写注解。第三个是multipart文件上传大小限制。SpringBoot默认上传大小为1MB用户上传几张照片很容易就超了。我修改了配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB第四个是MinIO上传后前端访问不了。原因是对外URL和MinIO内网endpoint不一致。我在MinioUtil里增加了一个公开访问域名配置在返回URL时做替换这个方案比在MinIO服务端配置bucket policy更灵活。第五个是Nginx反向代理WebSocket或SSE时连接断开。如果后面打算做实时消息通知比如审核结果通知在Nginx配置里一定要设置proxy_read_timeout和proxy_buffering off否则长连接一会儿就被切断了。5.4 表结构和索引设计的几点心得数据库表设计我最后再啰嗦几句。绿色行动平台这类系统表不会太多二三十张左右但有几类表需要特别留意。第一类是日增量大的表比如behavior_record、points_log这两张表一定要在create_time上加索引或者像我在第3节做的那样加(user_id, submit_date)复合索引。否则用户在半年后查自己的历史记录一张几百万行的表来一次全表扫描单条接口就直接几秒钟。第二类是状态频繁变化的表比如exchange_order状态字段上必须加索引运营后台要按照状态筛选如果没有索引查询会随着订单量增长越来越慢。第三类是配置类表比如behavior_type_config数据量永远不会太大索引要不要都行更重要的是在编码层加一个Caffeine本地缓存在启动时加载一次后续从内存读性能接近零开销。我把这套索引设计原则总结成一句话主键永远用自增ID时间字段永远考虑范围查询状态字段永远要考虑筛选外键关联字段永远要有索引。6. 经验总结与进一步扩展6.1 做这类系统设计时我认为最重要的设计决策绿色行动平台这个项目做完之后我最大的体会是系统的复杂度和难度不在代码量而在状态设计和事务边界。我最庆幸的两个决定第一个是把行为记录和积分变动拆成两个领域模型中间通过状态和行为类型配置进行衔接这让审核逻辑的扩展变得非常干净第二个是引入了行为类型配置表把积分规则从代码中抽出来运营侧可以自主调整规则而不需要动不动就发版本。如果一开始把这些逻辑揉在一起比如在行为记录Controller里直接操作积分账户后期新增“公益活动报名”这个行为类型时就要动Service层代码还要考虑是否要人工审核、积分翻倍活动怎么做改起来风险极大。6.2 这个系统后续还能怎么扩展系统基础框架搭建完之后有三个我认为最值得做的扩展方向第一个是消息通知模块。审核通过、积分到账、兑换订单发货这些都是用户非常关心的动态可以用WebSocket或极光推送把实时通知补上。用户提交一条行为记录几分钟后在手机上收到“审核通过积分已到账”的推送整个体验会提升很多。第二个是数据可视化大屏。绿色行动平台运营方天然关注减碳量数据可以在系统中引入ECharts对接MySQL里的统计数据做一个可视化大屏展示今日行为量、累计积分发放量、各行为类型占比这些指标。这类功能在一些校园场景或社区宣传场景下非常加分。第三个是积分兑商品之外的激励体系。当前积分只能兑换实物或优惠券可以扩展出成就系统、排行榜、连续打卡奖励机制。连续打卡7天额外奖励积分的这个逻辑需要在行为记录表的基础上增加一个连续天数的计算服务实现起来也不复杂但对用户留存有明显帮助。6.3 对正在做类似项目的朋友几个实用建议最后分享几个偏工程实践层面的建议不是教科书里能学到的。第一写代码前先把数据流图画清楚。你不用画得多专业一张纸上把“用户提交行为 - 审核 - 积分变动 - 兑换 - 发货”这条链路标出来标注每个环节涉及的表和状态比直接上手写代码效率高十倍。很多后期改得想哭的BUG根子就在于一开始链路没理清。第二设计接口时一定要想好“幂等”。不管是行为提交还是积分变动都要考虑用户重复点击、网络重试的情况。Redis锁或者数据库唯一索引至少要选一种。第三日志要打得详细尤其是积分变动和审核操作。我在审核通过和积分发放的方法里都保留了userId、recordId、configId、points这几个关键信息配合日志链路追踪工具排查线上问题的时候能节省大量时间。我做完这个项目后又把表结构重新翻看了几遍梳理出了几条可以沉淀的通用套路这套思路将来做别的管理系统也都能复用。希望这次的分享能给正在做springboot相关项目的朋友一些参考。
返回列表