资讯动态

SpringBoot + Vue + Redis + Docker全栈项目实战:从注册登录到支付排行榜

发布时间:2026/10/21 22:06:05 来源:尧图企业网站定制
我最近把一个前后端分离的完整闭环项目从头到尾盘了一遍技术栈正好就是标题里那套SpringBoot Vue-Cli Redis Docker业务上串了邮箱注册登录、短信注册登录、Alipay支付、用户积分排行榜。这个项目编号叫 Project1_06属于那种“单看每个模块都不难但合在一起很容易翻车”的典型全栈练习项目。我把它跑通之后最大的感受是这种项目真正的难点不在某个框架的API而在Redis到底该存什么、支付回调怎么防守、积分和订单之间怎么保证一致、Docker部署时哪些细节不配好会折腾你一晚上。这篇文章我会按当时动手的顺序把这个项目的整体设计、双通道注册登录、排行榜ZSet设计、支付链路、Docker部署逐个拆开讲。你如果正准备做类似的全栈项目或者想看看一个真实项目里这些技术点是怎么组合的可以直接照着下面的思路抄作业。1. 项目全貌从业务链路到技术栈选型1.1 这个项目到底在做什么Project1_06 看起来是个以用户体系为中心的电商或社区类服务项目。它最核心的业务链路其实只有一条用户通过邮箱或手机号注册登录 → 选择商品或服务下单 → 通过支付宝完成支付 → 支付成功后获得积分 → 积分写入排行榜 → 用户能看到自己的名次和Top名次。这条链路涵盖了当前绝大多数互联网产品的通用玩法注册登录负责“拉人”支付负责“成交”积分排行榜负责“留存和促活”。单独看每一个环节都不算复杂但它们之间有强依赖关系——支付成功才能加积分加积分才能改变排名排名变化又会反过来刺激用户继续消费。这种业务闭环正是这类项目最大的价值。1.2 技术选型为什么偏偏是这四个组件我之前也见过不少同学问为什么这个项目选SpringBoot、Vue-Cli、Redis、Docker而不是别的。我的理解是这四个东西刚好把项目从开发到部署的每个阶段都覆盖了而且各自都承担着不可替代的角色。SpringBoot后端主框架。Java生态最成熟社区资料极多无论接支付宝SDK、短信服务商还是自己封装验证码工具类都能很快找到参考。另外SpringBoot的自动装配和starter机制让项目初始化成本非常低。Vue-Cli前端工程化脚手架。Vue-Cli带路由、状态管理、开发服务器、构建工具链配合Element Plus这类组件库做表单、弹窗、列表页非常快。和SpringBoot通过RESTful API通信各管各的。Redis这个项目里Redis不是可有可无的缓存而是核心基础设施。验证码需要临时存储和自动过期登录态需要共享并可以主动踢人排行榜需要有序集合和实时分数更新这三件事用MySQL去做都很别扭用Redis做则刚刚好。Docker项目跑完总要交付Docker可以保证本地开发和服务器运行环境一致。数据库、Redis、后端Jar包、前端静态资源全部编排进容器一条命令拉起整个环境省去一堆“在我电脑上明明可以跑”的争论。组件在这个项目里承担的角色如果缺了会怎样SpringBoot后端业务逻辑、API、第三方SDK集成开发工作量剧增生态支持变弱Vue-Cli前端页面、路由、状态管理、API对接前端没法工程化团队协作混乱Redis验证码、登录态、排行榜、限流验证码过期和排行榜实时性很难实现Docker统一线上环境简化部署运维部署易出环境差异维护成本高1.3 数据库设计与模块划分虽然是标题里的四件套但持久化还是离不开MySQL。我当时为了这个项目设计了一套最小化表结构不复杂但刚好能支撑整条业务链路user表用户主表字段包含id、用户名、邮箱、手机号、密码BCrypt加密、头像、创建时间。user_points_log表积分流水表字段包含id、用户id、积分变动值、变动原因、关联订单号、创建时间。这张表很重要后面讲数据一致性时还要提到。order_master表订单主表字段包含订单号、用户id、商品id、金额以分为单位、订单状态、创建时间。payment_info表支付流水表字段包含支付流水号、订单号、支付方式、支付宝交易号、支付金额、回调状态。模块倒不需要拆得太碎我当时是按controller/service层面分成六大块认证模块、用户模块、商品模块、订单模块、支付模块、排行模块。重点是前后端分离的目录规范后端一个project-api工程前端一套Vue-Cli工程互不污染。项目做到后面你会发现边界清晰比技术花哨重要得多。2. 双通道注册登录验证码体系与登录态的Redis化改造2.1 验证码这东西为什么要存Redis邮箱注册登录和短信注册登录本质上都是“身份凭证 验证码核验”的组合。手机号和邮箱只能证明“这个号码/这个地址能接收到消息”要确认这个号码/地址属于当前用户必须通过一次性验证码。验证码有几个天然的需求它必须在几分钟内失效它必须能防重放它必须能防爆破。这三个需求全部指向同一个方案——把验证码存进Redis并设置合理的过期时间。Redis的EXPIRE天然支持“几分钟后自动删除”比对数据库一个个字段去判断“验证码是否过期”要省事得多。我当时设计了这么一组Redis Key发送邮箱验证码auth:code:email:{email}TTL 5分钟发送短信验证码auth:code:sms:{phone}TTL 5分钟邮箱发送频率限制auth:limit:email:{email}TTL 60秒短信发送频率限制auth:limit:sms:{phone}TTL 60秒为什么邮箱和短信要分成两套前缀因为邮箱验证码和短信验证码的发送渠道、发送成本、通道故障都不完全相同分开存储方便做单独控制。比如短信有成本和第三方通道额度限制需要更严格的频控邮箱则相对宽松。2.2 验证码服务的统一抽象与代码落地邮箱和短信虽然发送渠道不同但核心流程高度一致生成验证码 → 设置Redis过期时间 → 发送给用户 → 用户提交后校验。所以我先写了一个统一的验证码Service把逻辑抽到一起。Service public class VerifyCodeService { Autowired private StringRedisTemplate stringRedisTemplate; // 发送验证码isEmail通道走邮箱否则走短信 public void sendCode(String target, boolean isEmail) { String limitKey auth:limit: (isEmail ? email: : sms:) target; Boolean canSend stringRedisTemplate.hasKey(limitKey); if (Boolean.TRUE.equals(canSend)) { throw new RuntimeException(发送频率过快请稍后再试); } String code String.valueOf(ThreadLocalRandom.current().nextInt(100000, 999999)); String codeKey auth:code: (isEmail ? email: : sms:) target; stringRedisTemplate.opsForValue().set(codeKey, code, 5, TimeUnit.MINUTES); stringRedisTemplate.opsForValue().set(limitKey, 1, 60, TimeUnit.SECONDS); if (isEmail) { mailSender.sendCodeMail(target, code); } else { smsSender.sendCodeSms(target, code); } } // 校验验证码成功则立即删除防止重放 public boolean verifyCode(String target, String code, boolean isEmail) { String codeKey auth:code: (isEmail ? email: : sms:) target; String cached stringRedisTemplate.opsForValue().get(codeKey); if (cached ! null cached.equals(code)) { stringRedisTemplate.delete(codeKey); return true; } return false; } }这里有个细节我刚做项目时经常漏掉验证码校验成功后一定要立刻删除Redis Key。如果不删同一串验证码在TTL内可以反复使用攻击者拦截到一次验证码之后可以无限利用这属于很致命的逻辑漏洞。所以校验接口必须设计成“一次性核验”用完之后直接删Key。短信发送部分我当时没有直接写死某一家服务商的SDK而是先定义一个发送接口然后做一个Aliyun实现和一个Mock实现。本地开发和测试环境用Mock直接把验证码打回前端Console线上切到真实短信服务。这样做的坏处是多写一个实现类好处是项目不需要依赖真实短信额度就能跑通整个流程。public interface SmsSender { void send(String phone, String code); } Service ConditionalOnProperty(name app.sms.mock, havingValue true) public class MockSmsSender implements SmsSender { Override public void send(String phone, String code) { log.info([mock sms] send code {} to phone {}, code, phone); } }2.3 登录态为什么不用Session而是用Redis Token注册登录做完了紧接着的问题是用户登录成功后后端怎么识别这个用户很多人第一反应是HttpSession。但前后端分离项目里前端用Vue-Cli跑开发服务器跨域请求天然存在Session要处理跨域Cookie、CSRF等问题非常麻烦。而且一旦后端要横向扩展成多个实例Session默认存在单机内存里集群之间就串了。所以我选了“Redis Token”方案。登录成功后生成一段随机Token把它作为Key存进RedisValue是userId、昵称等信息TTL设为7天。前端把Token放进请求头Authorization里后端用一个拦截器统一解析。Component public class AuthInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate stringRedisTemplate; private static final ListString WHITE_LIST Arrays.asList(/api/auth/login, /api/auth/register, /api/auth/sendCode); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (WHITE_LIST.contains(request.getRequestURI())) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { String tokenKey auth:token: token.substring(7); String userInfo stringRedisTemplate.opsForValue().get(tokenKey); if (StringUtils.hasText(userInfo)) { stringRedisTemplate.expire(tokenKey, 7, TimeUnit.DAYS); // 滑动续期 request.setAttribute(userId, userInfo); return true; } } response.setStatus(401); return false; } }这里有一个我后来觉得特别值回票价的细节只要登录态在Redis里后端随时可以把某个用户踢下线只要删除对应的Token Key就行。如果当时图省事儿用了无状态的JWT想踢人都要等Token自然过期根本做不到主动控制。这个项目的“活跃用户管理”“后台封号”等扩展功能都是依赖Redis Token才能实现的。2.4 双通道登录的边界情况与坑这一节我把自己踩过和见过别人踩的坑集中列一下。验证码长度短信验证码一般6位数字邮箱验证码可以6位也可以更多。位数太短容易被暴力猜位数太长用户容易输错6位是体验和安全的平衡点。频控一定要覆盖发送接口只对“校验接口”做限制是防不住恶意刷短信的。攻击者会直接循环请求发送接口把你的短信额度刷光所以发送前必须查频控Key有未过期的限流Key就拒绝发送。图形验证码防机器人真要是公网项目短信和邮件接口还应该加一层图形验证码或滑条验证码防止脚本直接调发送接口这一点可以放在Vue页面里做。邮箱格式和手机号格式校验前端做一遍轻校验后端必须再做一遍重校验。后端如果只依赖前端传参很容易被绕过导致垃圾数据入库。我当时就吃过这个亏本地调试时短信验证码发送接口没有做频控一个循环脚本把短信服务商的测试额度几分钟内全打完了。后来老老实实把频控逻辑补上重新写了一版限流工具类才安心继续往后做。3. 用户积分排行榜Redis ZSet的数据结构与排行榜取舍3.1 为什么不做SQL排行榜而选ZSet排行榜功能第一个想到的方案肯定是SQL那到底要不要用Redis ZSet我是先把SQL方案算了一笔账才做的决定。用MySQL做排行榜通常是一张积分流水表跑SELECT user_id, SUM(points) FROM user_points_log GROUP BY user_id ORDER BY SUM(points) DESC LIMIT 50。听起来很直接但用户量和流水量一上来这个聚合查询会遍历大量记录而且每产生一笔积分都要实时重算Top50数据库压力非常大。另外这个排行查询会被频繁点击缓存和实时性也很难两全。Redis里的ZSet是有序集合每个成员自带一个分数底层是跳表加哈希表结构插入、更新、范围查询的时间复杂度都是对数级别专门为这类“按分数排序的成员集合”场景设计。映射到我们的业务就是member存userIdscore存用户总积分ZINCRBY一条命令就能给某个用户加分ZREVRANGE一条命令就能拿Top50。实时性、性能、实现成本全都比SQL方案好。3.2 积分从哪来支付成功事件驱动的加分机制积分怎么进排行榜这个项目里积分来源很明确——支付成功。所以我在支付模块收到支付宝异步通知、把订单状态更新为已支付之后紧接着发布一个支付成功事件积分模块监听这个事件去加积分。用Spring的事件机制可以避免把积分逻辑塞进支付回调里造成代码耦合。核心的加积分伪代码如下Transactional public void addPoints(PaymentSuccessEvent event) { UserPointsLog log new UserPointsLog(); log.setUserId(event.getUserId()); log.setChangePoints(event.getPayAmountInYuan().intValue()); // 比如1元1积分 log.setOrderNo(event.getOrderNo()); pointsLogMapper.insert(log); // Redis排行榜加积分 String rankKey rank:user:score; stringRedisTemplate.opsForZSet().incrementScore(rankKey, event.getUserId().toString(), event.getPayAmountInYuan()); }注意这里有两个写入动作一个是MySQL积分流水一个是Redis排行榜加分。后面我会专门讲这两个的一致性处理这里先强调一点必须先把积分流水落库再更新Redis排行榜。流水是主数据排行榜是可重建的缓存顺序反了可能导致Redis加了分但流水丢了对账的时候很难排查。3.3 一个ZSet还是多个ZSet日榜、周榜、总榜的设计排行榜如果只做总榜一个ZSet就够。但真实项目通常还要日榜、周榜这类玩法。我在这个项目里做了总榜和当日榜两种设计方式非常直白总榜rank:user:score当日榜rank:user:score:20250601日榜Key带日期后缀每天零点之后新产生一个Key昨天的Key在业务低峰期让它自然过期。为了保证日榜的分数是当天数据加积分时必须根据当前时间拼出对应的日榜Key同时更新总榜Key。周榜用类似思路可以做要么每天凌晨用定时任务把过去7天的日榜合并成周榜要么实时用ZUNIONSTORE把多个日榜Key合并到一个临时周榜Key。我更推荐后者因为定时任务一多就很容易出调度问题ZUNIONSTORE是Redis原生命令一次性把7个日榜按SUM聚合代码也很简单。public void buildWeeklyRank() { String weekKey rank:user:score:weekly; String[] sourceKeys getLast7DaysKeys(); // 生成最近7天的key数组 DefaultTypedTupleObject[] empty new DefaultTypedTuple[0]; stringRedisTemplate.opsForZSet().unionAndStore( sourceKeys[0], Arrays.asList(Arrays.copyOfRange(sourceKeys, 1, sourceKeys.length)), weekKey ); stringRedisTemplate.expire(weekKey, 7, TimeUnit.DAYS); }这个方案唯一的注意点是日榜Key如果已经过期删除了合并出来的周榜就会缺数据。所以日榜TTL我一般设置成7天以上比周榜生命周期长给周榜合并留足缓冲时间。3.4 TopN查询、我的排名与分页ZSet的查询接口非常贴合排行榜需求我这里把三个常用操作对应一下榜单TopNZREVRANGE rank:user:score 0 9 WITHSCORES用户自己的名次ZREVRANK rank:user:score {userId}带分页的榜单纯数据ZREVRANGE rank:user:score (page-1)*size page*size-1 WITHSCORES在Java里用StringRedisTemplate实现TopN很简单public ListRankItem getTopRank(int limit) { SetZSetOperations.TypedTupleString result stringRedisTemplate.opsForZSet() .reverseRangeWithScores(rank:user:score, 0, limit - 1); ListRankItem items new ArrayList(); int rank 1; for (ZSetOperations.TypedTupleString tuple : result) { items.add(new RankItem(Long.valueOf(tuple.getValue()), tuple.getScore(), rank)); } return items; }这里有两个需要说明的点一是不同用户积分相同时Redis按member的字典序排序名次会显得“不自然”产品上一般接受这种并发排名规则不必做得太复杂二是reverseRangeWithScores返回的Score是Double如果你积分值特别大要注意精度问题不过普通积分场景完全够用。3.5 排行榜数据的持久化与Redis宕机恢复排行榜这种高实时性数据存在Redis里但它也是核心业务数据直接丢了用户会闹。我在项目里的做法是分层处理来源数据在MySQL每一笔积分流水都落库Redis排行榜顶多算“按流水聚合出来的缓存视图”。Redis开AOF持久化appendonly yesappendfsync everysec用每秒一次的频率刷盘。万一进程挂了最多丢1秒左右的数据可接受。定时对账任务每天凌晨跑一个任务扫描MySQL积分流水把总积分和Redis里的ZSet分数做比对不一致就按MySQL数据重刷排行榜。如果Redis彻底宕机重启后排行榜空了怎么办我补了一个管理接口从MySQL重新聚合所有用户的积分总和并重建ZSet相当于“缓存预热”。因为已经有了积分流水这个主数据排行榜的恢复成本其实很低这也是这种设计最大的底气。4. 支付链路订单状态机、回调验签与应用幂等4.1 支付模块梳理下单、提交支付、同步跳转和异步通知支付是整条链路里最不能出错的一环。Alipay支付接入的时候先要理清整个流程有几个节点用户下单后创建订单状态待支付后端调用支付宝的支付下单接口拿到一段自动提交的表单HTML返回给前端前端渲染之后用户扫码或跳转收银台完成付款。付款完成后支付宝会做两件事一是把用户在浏览器里重定向回return_url给用户看支付结果二是给后端接口notify_url发一个HTTP异步通知告诉后端“这单已经支付了”。我在这类项目里见过最多的误区是把return_url当成业务终结信号。其实同步跳转完全不可靠用户可能付完款直接关掉浏览器根本不会触发return_url就算触发了也完全可能是伪造的。真正可以相信并用来更新订单状态的只有异步通知。所以前端跳转回来只能用于“刷新页面给用户一个反馈”后端所有业务逻辑必须挂在异步通知里。4.2 回调处理的核心防守验签、验金额、验状态拿到异步通知后第一件事不是直接改订单状态而是做三重校验每一重漏掉都会出事故。第一重验签支付宝的通知参数里带一个sign字段是用商户私钥对请求参数签名生成的。后端必须用支付宝公钥对请求内容做验签确认这条通知确实来自支付宝服务器。验签通常是SDK的一行代码但这一行代码保证了“不可能伪造回调”。boolean check AlipaySignature.rsaCheckV1(requestParams, alipayConfig.getPublicKey(), AlipayConfig.CHARSET_UTF8, AlipayConfig.SIGN_TYPE_RSA2); if (!check) { log.warn(支付宝异步通知验签失败); return failure; }第二重验金额不能直接用订单表里的“订单应付金额”和回调里的金额做比较而是要读取回调参数里的total_amount等字段和订单库里记录的实际订单金额比对。为什么因为一个金额参数在传输过程中如果被篡改验签已经能挡住但这里还必须防的是接口调用方自己记错金额、或者订单本身发生过改价等场景。金额以支付宝回调为准同时必须严格相等哪怕是0.01元的差异都不能放行。第三重验订单状态订单状态必须是“待支付”才允许更新。如果订单已经处于“已支付”状态说明这条通知可能重复到达此时应当直接返回成功应答不能再次走一遍加积分逻辑。4.3 幂等设计与数据库并发下的积分重复问题支付回调天然存在重复通知机制。支付宝在没有收到明确“success”答复时会间隔一段时间主动重发通知最多重发大约8次时间跨度和每次都不同。如果后端一时处理慢、或者网络闪断同一个支付结果会被传两遍。解决办法就是在处理逻辑里加幂等控制。我当时的实现分两层第一层在数据库订单表里加一个唯一的支付流水号字段收到回调时先尝试UPDATE order_master SET statusPAID WHERE order_no? AND statusUNPAID受影响行数只有1才继续执行不为1说明已经处理过直接返回“success”。第二层给积分发放加一个去重标志按支付宝交易号做唯一索引。我建了一张支付单流水表trade_no唯一索引回调处理时先插入流水记录插入返回冲突说明这条交易已经处理过直接放弃积分发放。这两层加下来重复通知、并发线程同时到达的情况都能兜住。我实际测试过把回调接口并发打10次最终订单只被更新一次积分只发放一次排行榜分数也稳定不变没有出现多倍积分的情况。4.4 沙箱环境和公网回调调试的几点经验支付宝开放平台提供沙箱环境申请之后会拿到一套沙箱AppID、应用私钥、支付宝公钥所有接口调用都走沙箱网关支付体验和真实环境一致。做这个项目时强烈建议先沙箱调试不要一上来就用真实密钥出问题追究起来很麻烦而且沙箱里各种金额都可以随便测不用担心造成真实资金损失。调试过程中有几个让人头疼的常见问题我提前说一下回调地址必须是外网可访问的。支付宝服务器向notify_url发通知时它不可能访问到你的localhost。本地调试时需要让内网开发环境暴露到公网相关工具使用时注意不要乱传代码和明文密钥即可。服务端回调接口要容忍乱序。支付回调到达的时间和下单时间没有严格的先后关系不能假设“回调一定在下单之后到达”所以下单接口和回调接口之间不要依赖时序。返回字符串必须严格是“success”。很多新手写回调时返回了JSON支付宝识别不了会一直重发通知导致同一单被反复处理。虽然幂等能挡住重复处理但日志会非常难看还是要规范返回。支付这块做完我对“金额敏感型业务要保守”这句话有了特别深的理解。开发时可以追求效率但涉及钱、积分、订单状态宁可多写一层校验也别为了赶进度省略掉。5. Docker化部署一套Compose把后端、前端、Redis、MySQL全部收编5.1 容器化拆解四个容器怎么分工项目开发完了准备部署我用Docker把整个环境容器化。这台部署机的环境是全新的我一开始也犹豫过要不要直接在宿主机上装MySQL、Redis、Java、Node、Nginx想想还是放弃了。宿主机部署最大的问题是环境依赖一堆、迁移成本高、改配置反复折腾而且万一哪天要换服务器等于重来一遍。用Docker之后整个项目变成一堆镜像和一份编排文件换机器只要把镜像导入、把Compose文件拉起来五分钟就能跑起来。这个项目的容器拆法很简单容器镜像暴露端口作用mysqlmysql:8.03306持久化业务数据redisredis:7-alpine6379验证码、登录态、排行榜backend自建后端镜像8080SpringBoot应用frontend自建前端镜像80Nginx托管Vue构建产物四个容器放在同一个Docker网络里互相通过容器名访问不需要把MySQL、Redis的端口暴露到公网安全性和隔离性都更好。5.2 后端与前端Dockerfile多阶段构建是基础操作后端是Java项目打包方式是Maven构建出Jar包然后让一个仅带Java运行时的镜像来跑。最简单的Dockerfile是直接装一个Maven和JDK构建镜像再把Jar打进去但这样镜像体积巨大部署也慢。所以我用了多阶段构建第一阶段负责编译第二阶段只保留运行产物。# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:resolve COPY src ./src RUN mvn package -DskipTests # 运行阶段 FROM eclipse-temurin:17-jre-alpine WORKDIR /app RUN apk add --no-cache tzdata ENV TZAsia/Shanghai COPY --frombuilder /app/target/project1-06.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]这段Dockerfile踩过一个小坑值得说基础镜像如果用默认的时区容器里是UTC时间日志时间会比北京时间少8个小时而且LocalDate.now()这类取当天日期的代码会莫名其妙地“跑到昨天”。所以我直接拉了一个时区设置环境时间统一成北京时间。数据库连接串参数serverTimezoneAsia/Shanghai也一并配上整条链路的时间就对齐了。前端Dockerfile更典型用Node镜像把Vue项目构建成dist静态文件再交给Nginx镜像跑。构建完的静态文件用COPY --from拷贝到Nginx的站点目录下面即可。# 构建阶段 FROM node:16-alpine AS builder WORKDIR /app COPY package.json ./ RUN npm install COPY . . RUN npm run build # 运行阶段 FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80Nginx配置需要特别注意一个点前端单页面应用配合Vue Router使用history模式时路由路径不是真实文件路径刷新一个/dashboard页面会直接404。必须在Nginx里配try_files $uri $uri/ /index.html;把匹配不到文件的路由全部回退到入口HTML。不配这一行部署完刷新就白屏排查半天还以为是静态资源路径错了。5.3 用docker-compose编排整条链路我习惯把后端、前端、Redis、MySQL都写进同一个docker-compose.yml里这样一键就能拉起整个项目。Compose文件核心关注点是环境变量注入、容器依赖关系、数据卷挂载和网络互通。version: 3.8 services: mysql: image: mysql:8.0 container_name: project-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: project1_06 ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1] interval: 5s retries: 10 redis: image: redis:7-alpine container_name: project-redis ports: - 6379:6379 command: redis-server --appendonly yes volumes: - redis-data:/data backend: build: ./backend container_name: project-backend environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql DB_PORT: 3306 DB_NAME: project1_06 DB_USER: root DB_PASSWORD: root123 REDIS_HOST: redis REDIS_PORT: 6379 ALIPAY_GATEWAY_URL: https://openapi-sandbox.dl.alipaydev.com/gateway.do ports: - 8080:8080 depends_on: - mysql - redis frontend: build: ./frontend container_name: project-frontend ports: - 80:80 depends_on: - backend volumes: mysql-data: redis-data:写这段Compose的时候有一个特别常见的误区depends_on只是控制启动顺序它不等同于“依赖的服务已经就绪”。MySQL容器启动了不代表3306端口已经能接受连接SpringBoot启动时连不上MySQL依然会报错。为了解决这个问题我做了两手准备MySQL加了healthcheck健康检查SpringBoot的连接池配置了connection-timeout和自动重连让后端在依赖延迟几秒时不会直接崩溃退出。5.4 配置外置与环境变量注入前后端和数据库的连接配置全部通过环境变量注入而不是写死在代码配置文件里。SpringBoot的application-prod.yml这么写spring: datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: ${DB_USER} password: ${DB_PASSWORD} redis: host: ${REDIS_HOST} port: ${REDIS_PORT}这样做的好处是代码仓库里不含任何实际部署环境的密钥和地址换环境只要改Compose的环境变量镜像完全不用重新构建。数据库初始化用挂载init.sql到MySQL容器的方式启动时自动建库建表特别适合演示和交付。5.5 部署完成后的两个小提醒整体跑通之后我在生产环境和本地之间来回切换时还遇到两个小问题顺便分享出来。第一容器日志要配置滚动。项目跑久了容器日志和MySQL日志会无限增长很容易把磁盘撑满。可以在Docker的/etc/docker/daemon.json里配置日志轮转这段配置我是直接加到Docker守护进程里的之后新容器默认启用省心很多。{ log-driver: json-file, log-opts: { max-size: 50m, max-file: 10 } }第二前端反向代理后端接口。我在前端Nginx配置里除了托管静态文件还给/api路径配了一个反向代理转发到后端容器。这样前端代码里的API地址统一写/api避免了浏览器直接跨域请求后端的问题部署起来干净利落。location /api/ { proxy_pass http://backend:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }整套容器编排跑下来之后部署这件事的体感就是“项目代码提交完服务器上执行一次docker compose up -d --build四五个容器依次启动再访问服务器IP就看到了登录页。”相比之前手工装JDK、配MySQL、跑Nginx的经历效率提升非常明显。6. 最后说几句我在整条链路里的个人体会这个Project1_06的模拟项目做下来我最深的一个体会是它没有用到任何“高深”的技术但把业务链路完整地走通需要开发者对数据存储的取舍、回调安全、事务一致、部署交付都有清晰的概念。Redis在这里不是锦上添花的缓存而是验证码过期、登录态共享、实时排行榜的承载者支付回调不是写完API就结束而是要防御重复通知、伪造通知、金额篡改这些真实风险Docker部署不是套一个镜像模板就完事时区、健康检查、配置外置、日志轮转都是实际运维会遇到的问题。如果让我给刚要起步做类似项目的朋友一个建议我会说先用沙箱环境把支付流程从下单到回调整个串通再考虑积分排行榜的细分和扩展。支付是整条链路的发动机发动机不稳后面全是空转。下一步我计划在这个项目上做两件事一是给排行榜增加WebSocket实时推送排名变化直接推给前端不用用户手动刷新二是把积分体系升级成等级勋章把日榜、周榜、月榜的合并策略再优化一轮。你如果也正在做这个方向的项目有更好的设计思路欢迎一起交流。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑