苍穹外卖项目走到day12,意味着后端开发正式进入收工阶段。到了这一天,其实增删改查早就不稀奇了,剩下的全是磨人的硬骨头:支付回调、订单状态扭转、超时取消、数据看板、实时来单提醒,还有跟前端联调时那些说不清道不明的诡异问题。这篇就把后端最后一天要做的核心事情掰开揉碎讲清楚,包含我实际开发中踩过的坑、改过的方案和总结出的套路,给正在做苍穹外卖或者类似外卖/电商项目的朋友一个可以直接参考的收尾清单。
1. 后端最后一天到底在收什么尾
1.1 先别急着写代码,把待办清单盘清楚
很多人到项目最后一天会陷入一种“好像什么都做完了,又好像哪里都没做完”的状态。这很正常,因为后端收尾阶段的任务和前十天完全不同——前面的需求是“功能开发”,最后的需求是“闭环保障”。
以苍穹外卖day12常见的任务范围来说,核心就这几块:支付回调处理、订单状态机完善、超时未支付订单自动取消、营业额与销量统计报表、商家端来单实时提醒,再加上接口联调和部署验证。这些任务有一个共同特点:它们都不是单靠CRUD能解决的,而是涉及跨系统交互、异步处理、状态一致性、定时任务的时序问题。
我把一天的任务按“影响面”和“依赖关系”拆开排了个优先级,列为四类:
第一类是交易闭环,支付回调必须保证幂等,订单状态不能跳变,这是资金和业务正确性的底线,优先级最高。第二类是超时取消,这部分影响用户体验和库存释放,虽然可以延后,但它依赖延迟消息或定时任务,需要预留调试时间。第三类是统计报表,涉及多表聚合查询和指标口径问题,是典型的“看起来简单、做起来一堆细节”的活儿。第四类是WebSocket来单提醒,属于体验加成项,放最后不过分。
1.2 任务拆解与优先级排序的经验参考
这四类任务的依赖关系可以画成一条链:支付回调决定订单状态变化,订单状态变化触发超时任务判断和来单提醒,而统计报表的数据来源又是已经完成支付、状态合法的订单。所以按依赖顺序做,比按喜好做要顺畅得多。
优先级排序上,我一般用“风险优先”而不是“难度优先”。支付回调的风险最高,因为涉及金额、订单、流水三张表的数据一致性,做不好不是功能bug,是资损事故。超时取消的风险其次,看似简单,但定时任务没写好会导致大量数据库空扫或者重复关单。统计报表更多是“需求口径”问题,不会崩,但会吵。WebSocket即使没做,也不影响主流程。
所以如果时间真的只剩一天,我的建议顺序是:支付回调与状态机优先,超时取消其次,统计看板再次,WebSocket最后。如果时间充足,就按照下面各节的内容一步步来。
2. 交易闭环:支付回调与订单状态机
2.1 订单状态机:给订单装上“红绿灯”
外卖订单从用户下单到完成,会经历一系列状态变化。这不仅仅是给订单表加一个status字段那么简单,难点在于“每一个状态能跳到哪儿、谁允许跳、什么条件下跳”都要有明确规则。
苍穹外卖项目里,我习惯把状态定义为一组整数常量:待支付、已支付待接单、已接单待配送、配送中、已完成、已取消、退款中、已退款。其中退款相关的可以按需求取舍,但核心那几个状态必须完整。
状态机的核心逻辑是拒绝非法跳转。比如待支付订单不能直接变成已完成,已取消订单不能再被支付回调改成已支付。这个校验不能只靠前端,后端必须在更新语句里带上状态条件。
下面是我项目里用的判断逻辑伪代码,核心就是“更新前先比对当前状态”:
// 商家接单:必须是待接单状态才能接 int rows = orderMapper.updateStatusByCondition( orderId, OrderStatus.TO_BE_RECEIVED, // 期望的当前状态 OrderStatus.RECEIVED // 目标状态 ); if (rows == 0) { throw new BusinessException("订单状态已变化,无法接单"); }用这种“条件更新”的方式,天然解决了并发下单、并发取消时两个人同时改订单状态的问题。因为数据库的update语句是行锁级别的,两个线程同时执行时只有一个能匹配到“当前状态=待接单”的记录,另一个更新行数为0,直接抛出异常或者走兜底逻辑。
这比先select再update靠谱得多——先查后改在低并发下没问题,但一旦多个请求同时处理同一个订单,就会出现两个线程都读到待接单,然后一个改成已接单,另一个又改成已取消,状态直接乱套。所以状态机落地时,我强烈建议把“条件更新”作为标配。
2.2 支付回调的幂等与签名校验
支付回调是整个苍穹外卖项目里最容易出安全事故的环节。用户在第三方支付平台完成支付,支付平台会向我们的后端服务器发一个异步通知,告诉我们“这笔订单支付成功”。问题在于:通知可能重复发送,通知里的数据可能是伪造的,通知到达顺序也可能乱。
先说验签。第三方支付平台的通知会带签名,我们需要用平台公钥验证签名,校验通过才处理业务逻辑。这一步不能省,否则任何人都可以伪造一个“支付成功”通知打到我们接口上,订单就能被白嫖。苍穹外卖项目里,最常见的方式是把通知参数按一定规则拼接,加上密钥做MD5或RSA签名,后端用同样的规则重新计算,比对一致才认为可信。
验签代码大致长这样:
// 验签:按支付平台规则拼接参数并校验签名 boolean signValid = PaySignatureUtil.verify( requestParams, // 回调参数 "商户密钥", // 平台分配的key sign // 回调里带的签名 ); if (!signValid) { // 直接返回失败,让支付平台稍后重试或人工介入 return "验签失败"; }再说幂等。支付成功通知一旦处理成功,下次再来同样的通知,我们绝对不能重复给用户加余额、重复把订单改成已支付。办法就是建一张支付流水表,以“订单号+支付交易号”作为唯一索引,处理前先尝试插入流水,插入成功才继续更新订单;如果插入时发现重复键,说明这笔通知已经处理过,直接返回成功。
伪代码如下:
try { payFlowMapper.insert(flow); // 唯一键约束兜底 } catch (DuplicateKeyException e) { log.warn("重复回调,订单号:{}", orderId); return "success"; // 幂等返回,不再处理 } // 插入成功,说明第一次处理,开始更新订单状态 orderMapper.updateStatusByCondition(orderId, OrderStatus.PENDING_PAYMENT, OrderStatus.PAID);实际操作中还要注意一个细节:支付金额要以支付平台回调里的实际支付金额为准,不能以我们库里下单时记录的金额为准。虽然正常情况两者应该一致,但万一用户下单后商品价格调整、或者支付平台返回了优惠金额,都应该以回调金额更新实付金额字段,而不是直接忽略。保证每一笔订单都有完整的支付流水记录,这是后续对账的基础。
对了,回调接口还有一个反直觉的“坑”:支付平台说“我们通知你”,但如果你的接口处理失败返回非200,平台会按照间隔递增加倍重试。所以回调接口最忌讳“明明成功了却因为验签失败返回失败”,一旦返回失败,平台会反复重试,把日志刷爆。正确的做法是:验签失败时可以返回失败并记录日志,但要区分是哪一类失败,方便排查;如果是因为业务上“重复回调”导致无法插入流水,要返回成功,避免无谓重试。
2.3 支付成功后的联动逻辑
订单状态从待支付改成已支付,不是终点。苍穹外卖里,支付成功后通常要联动做几件事:通知商家端有新订单(WebSocket推送)、记录订单状态变更日志、可能还要发取餐码或者短信通知用户。
这些联动操作有一个关键选择:同步做还是异步做?如果同步做,支付回调接口里就要依次执行推送、写日志、发短信,任何一个环节超时都会拖慢回调响应,支付平台那边等不到快速响应就可能重试。所以我的做法是:回调接口只做最重要的事——验签、幂等插入流水、更新订单状态——这三个动作必须在一个事务里完成。其他联动操作(WebSocket推送、写操作日志)通过Spring的事件机制或者消息队列异步执行,回调接口处理完核心逻辑后立刻返回success。
实际操作中,联动逻辑可以这样组织:更新订单状态后,发布一个“订单已支付”事件,监听器里做WebSocket推送和状态日志记录。这样回调接口的响应时间能控制在几十毫秒内,支付平台的重试压力也小很多。
3. 超时未支付自动取消:定时任务与延迟消息怎么选
3.1 业务需求与三种主流方案
外卖订单跟电商订单一样,用户下单后如果迟迟不付款,占用了商家的接单名额、占用了库存,所以必须有一个“超时自动取消”机制。苍穹外卖项目的需求一般是“下单后15分钟未支付,自动取消订单”。
实现这个需求有三条主流路线:定时扫描数据库、RabbitMQ延迟队列、Redisson延迟队列。选哪条取决于项目情况和你的技术储备。
定时扫描方案最简单,就是写一个定时任务,每隔一段时间扫一次订单表,把“创建时间早于15分钟前且状态仍是待支付”的订单批量改成已取消。优点是实现快、没有额外中间件依赖;缺点是轮询有延迟(比如每分钟扫一次,订单最久可能超时60秒才被取消),而且随着订单量增加,每次扫描都是对数据库的一次全表范围查询,压力不小。
RabbitMQ延迟队列方案利用的是消息的TTL机制:先把消息发到普通队列,设置消息过期时间15分钟,消息过期后通过死信交换机路由到真正处理取消操作的队列。优点是可以做到准实时触发、对数据库压力小;缺点是需要引入RabbitMQ,项目里如果还没有消息中间件,就得先搭建环境。
Redisson延迟队列方案类似,把取消任务封装成一个延迟任务放到Redis的ZSet里,由Redisson的看门狗机制在到期时触发。优点是纯Redis实现、不依赖额外消息中间件;缺点是需要对Redisson有一定了解,并且要处理服务重启时的任务恢复问题。
3.2 延迟队列方案的落地实现
如果你项目里已经有RabbitMQ,我推荐用延迟队列方案,因为外卖业务后续一般还会做订单消息推送、活动消息触达,消息中间件迟早要上。延迟队列的声明方式很关键:我们需要声明两个队列,一个是“等待队列”,消息进入后先待15分钟;另一个是“取消订单队列”,等待队列里的消息过期后被发往这里。
声明配置的伪代码如下,重点在设置消息TTL和死信交换机:
# 等待队列:订单下单后进入队列,ttl=15分钟,过期后投递到延时处理交换机Java侧的队列声明大致是这样的:
@Bean public Queue orderWaitQueue() { return QueueBuilder.durable("order.wait.queue") .ttl(15 * 60 * 1000) // 消息过期时间 .deadLetterExchange("order.dlx.exchange") // 死信交换机 .deadLetterRoutingKey("order.cancel") .build(); }生产者在下单接口里发一条消息,内容就是订单ID:
rabbitTemplate.convertAndSend("order.exchange", "order.pay.wait", orderId);消费者监听取消订单队列:
@RabbitListener(queues = "order.cancel.queue") public void onOrderCancel(Long orderId) { // 收到消息后不能直接取消,要再查一次订单状态 Order order = orderMapper.selectById(orderId); if (order == null || order.getStatus() != OrderStatus.PENDING_PAYMENT) { return; // 订单不存在或已支付,跳过 } orderMapper.updateStatusByCondition(orderId, OrderStatus.PENDING_PAYMENT, OrderStatus.CANCELED); }这里最容易被忽略的细节是:消费者收到延迟任务时,订单的状态可能已经变了。用户在14分59秒的时候完成了支付,延迟消息15分钟到点后才到达消费者,此时订单已经是已支付状态,不能再取消。所以消费端必须二次校验状态,而不是看到消息就关单。
3.3 定时扫描方案的细节和坑
如果你不想引入RabbitMQ,定时扫描也不丢人,苍穹外卖这种学习项目用定时扫描完全够。但扫描方案要做得优雅,有几个坑必须避开。
第一个坑是分批处理。千万别写一条update把所有超时订单一次性改完,万一订单量大,一个update可能锁住几万行,导致其他业务查询全部阻塞。扫的时候要分页,一次处理比如200条,处理完再取下一批。
第二个坑是状态条件必须带全。更新语句里要同时带上“status=待支付”和“create_time < 当前时间减15分钟”两个条件,任何一条不满足都不更新。这样即使定时任务跑得频繁,也不会误伤刚下单还没超过15分钟的订单。
第三个坑是避免大事务。每一批取消操作里,除了改订单状态,可能还要释放库存、记录日志。如果这些操作全部包在一个事务里,任何一步失败都会导致整批回滚,影响范围太大。我一般拆成“查订单集合、逐条更新主状态、异步处理库存释放和日志”,把核心状态更新放在一个独立小事务里,外围操作失败不影响主流程。
3.4 两种方案之外的Redis占位法(小技巧)
如果你既不想上RabbitMQ、又不想扫全表,还有一个折中技巧:在下单时往Redis里写一个带过期时间的key,比如order:timeout:{orderId},过期时间设为15分钟。Redis的key过期时会触发键空间通知,我们订阅这个通知事件,在回调里执行取消逻辑。
这个方案的优点是几乎不增加额外依赖(Redis项目里肯定有),达到的效果类似延迟队列。缺点有两个:一是Redis的key过期事件并不保证准时触发,官方说“过期事件在key失效后才会发布,但时机不确定”,所以极端情况下可能有延迟;二是生产环境需要开启notify-keyspace-events配置,否则根本收不到过期通知。我一般把这个方案当作“没有MQ时的取巧方案”,不建议在核心交易链路上用。
4. 数据看板:销量统计与报表查询优化
4.1 看板到底要看哪些指标,指标口径比SQL更重要
苍穹外卖的后端最后一天,通常还包含一个数据看板模块,给商家展示今日营业额、有效订单数、平均客单价、销量Top5、近7日趋势等统计信息。
这类功能的难点从来不是SQL怎么写,而是指标口径怎么定。比如“今日营业额”到底按订单的下单时间统计,还是按支付时间统计?如果用户昨天23:50下单、今天00:10支付,这笔钱算昨天还是今天?我做的口径是“按支付时间统计”,因为营业额代表实际到手的钱,必须以支付动作为准。又比如“订单数”,有效订单数通常是排除已取消和退款订单的,但如果写SQL的时候忘了过滤status,最后出来的数字就会对不上。
这些都是开发前就要和需求方确认清楚的,否则开发完了才发现统计口径不对,返工成本极高。我在项目里会先把口径定义写成一句话,比如“今日营业额=今日支付成功的订单实付金额合计(排除退款订单)”,然后SQL就照着这个口径去写。
4.2 聚合SQL与日期补零的处理
营业额和订单数的查询相对简单,核心是一个带状态的聚合查询:
SELECT COUNT(*) AS orderCount, IFNULL(SUM(amount), 0) AS turnover FROM orders WHERE status IN (已完成, 已退款, 待评价等有效状态) AND pay_time >= '2025-01-06 00:00:00' AND pay_time < '2025-01-07 00:00:00';销量Top5则要关联订单明细表,按菜品ID分组求和:
SELECT dish_id, SUM(number) AS total FROM order_detail WHERE order_id IN ( SELECT id FROM orders WHERE pay_time >= 开始时间 AND pay_time < 结束时间 AND status IN (有效状态) ) GROUP BY dish_id ORDER BY total DESC LIMIT 5;比较麻烦的是“近7日趋势”图:前端要的是一个连续的日期数组,比如1月1日到1月7日,每天一个营业额数据。问题是数据库里可能某一天一笔订单都没有,按天分组的SQL就会缺这一天,导致前端图表断档。直接返回缺的天,前端画出来的折线图就少了一个点,看着很怪。
解决办法有两种。第一种是用MySQL 8.0的递归CTE生成连续日期序列,然后左连接统计结果,缺数据的天自动补零。SQL大体结构是这样:
WITH RECURSIVE date_range AS ( SELECT CURDATE() - INTERVAL 6 DAY AS day UNION ALL SELECT day + INTERVAL 1 DAY FROM date_range WHERE day < CURDATE() ) SELECT d.day, IFNULL(SUM(o.amount), 0) AS turnover FROM date_range d LEFT JOIN orders o ON DATE(o.pay_time) = d.day AND o.status IN (有效状态) GROUP BY d.day ORDER BY d.day;第二种方案更通用:SQL只负责查出“有数据的那些天的统计”,Java侧再生成一个近7天的日期列表,把查询结果填进去,缺的天补0。对于苍穹外卖这种体量的项目,第二种方案完全够用,而且不依赖数据库版本。我实际用的是第二种,逻辑简单、前端不用改。
4.3 缓存与实时性的取舍
统计看板有个很有意思的矛盾:如果每次都实时跑SQL,订单量一大,看板接口可能几百毫秒甚至上秒级才返回,页面体验很差;如果完全走缓存,数据又可能不新鲜,商家看一眼“今日营业额”是30分钟前的,又觉得不实时。
我的做法是“短时缓存+异步预计算”。今日营业额这种指标,用Redis缓存,过期时间设60秒。缓存失效后第一个请求会触发一次实时计算,算完写回缓存。这能保证极端情况下最多延迟60秒,而正常请求基本都走缓存,接口响应时间能控制在几十毫秒。
近7日趋势这种更重的统计,我使用定时任务在每天凌晨算好存Redis,白天直接读。因为趋势数据一天内变化不大,没必要实时算。销量Top5也可以用类似策略,每10分钟刷新一次。
实践里还要注意一个细节:统计SQL的where条件里的状态枚举和支付时间字段,一定要建好索引。否则订单表数据量一上来,全表扫描一次会让整个数据库CPU飙升。我建的是组合索引(pay_time, status),查询效率提升非常明显。
5. 来单提醒:WebSocket还是轮询
5.1 选型分析:轮询真的够用吗
商家端需要实时感知新订单,这个需求实现方式无非两种:前端定时轮询接口,或者后端用WebSocket主动推送。
轮询方案的优点是实现极其简单,前端setInterval每隔3秒调一次“查新订单”接口就行;缺点是浪费资源——哪怕没有任何新订单,前端也在不停地打后端接口,商家端页面多开几个,后端压力就上去了。订单量小的项目可能无所谓,但我们做的是“实时提醒”,轮询的实时性最多也就3秒,体验算不上好。
WebSocket方案则是建立一条长连接,后端有新订单时直接推给商家端,实时性拉满,也不会有无效请求打到后端。缺点是需要做连接管理、断线重连、心跳保活,代码量比轮询多不少。
以苍穹外卖的规模来说,我最终选了WebSocket。原因很直接:这是项目的体验亮点,而且实现本身并不复杂,Spring Boot对WebSocket的支持已经很完善。
5.2 Spring Boot集成WebSocket的落地步骤
集成WebSocket的步骤拆开就四步:加依赖、写Handler、注册拦截器、前端接消息。
依赖方面,我用的是spring-boot-starter-websocket,这是Spring Boot官方starter,不需要额外版本管理。
Handler的核心是管理Session集合。我在项目里维护了一个ConcurrentHashMap,key是商家ID,value是对应的WebSocketSession。客户登录时,通过拦截器获取当前用户的ID,把它和Session绑定。这样后端某一笔订单支付成功后,就能定位到对应的商家Session,把“您有新订单”的消息推过去。
Handler核心逻辑如下:
@Component public class OrderWebSocketHandler extends TextWebSocketHandler { // 使用ConcurrentHashMap存放商家ID和Session的映射 private final Map<Long, WebSocketSession> sessionMap = new ConcurrentHashMap<>(); @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 客户端发来的消息,一般用于心跳保活 } // 向指定商家推送新订单提醒 public void sendMessageToMerchant(Long merchantId, String content) { WebSocketSession session = sessionMap.get(merchantId); if (session != null && session.isOpen()) { session.sendMessage(new TextMessage(content)); } } }下面这段是注册拦截器的配置,主要作用是在握手阶段从请求参数里取出商家ID,存入WebSocketSession的attributes中,方便Handler后续识别是谁连接上来了:
@Configuration public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(orderWebSocketHandler, "/ws/merchant") .addInterceptors(new HttpSessionHandshakeInterceptor()) .setAllowedOrigins("*"); // 实际项目里这里要按需配置,不能全放开 } }落地上有几个坑我印象很深。
坑一:连接数管理。很多人写完Handler后忘了在连接关闭时移除Session,导致商家端一刷新页面,旧连接还占着Map,新连接又加进去,推消息时要么推给了死连接,要么内存慢慢涨。所以afterConnectionClosed里必须移除Session。
坑二:心跳保活。WebSocket连接虽然长时间不通信一般不会断,但如果在服务端前面挂了负载均衡或者网关,空连接长时间无消息可能被中间层断开。我建议前端每30秒发一个ping消息,服务端收到后回应pong,这样连接一直处于活跃状态。
坑三:推送失败的处理。WebSocket推送不是百分百可靠的,Session可能在你发送前一刻刚好断开。所以推送时要捕获异常,推完还要确认发送成功;如果失败,可以退化为“把未读消息存Redis,等商家下次轮询时补拉”,保证消息不丢。
5.3 支付成功后的推送时机
WebSocket推送不要放在支付回调事务里。前面说回调接口只做核心操作,推送这类联动操作要异步执行。我在订单更新完成后,通过Spring的ApplicationEventPublisher发布一个“订单已支付”事件,WebSocket的监听器收到事件后去查订单的商家ID,再向对应商家推送新订单消息。
这样做的另一个好处是解耦:如果哪天你不想用WebSocket了,改成短信通知或者站内信,只需要替换监听器,支付回调的代码一行都不用动。
6. 联调部署与常见问题排查实录
6.1 前后端联调的高频岔路口
后端最后一天,很大一部分时间其实花在联调上。我梳理了三个最常见的岔路口。
第一个是参数格式问题。后端接口要求Long类型的订单ID,前端传到后端变成了字符串,框架反序列化时如果配置不严格,可能直接报类型转换错误。这类问题最好在前端联调前就用统一异常处理把错误信息包装成可读的JSON返回,不然排查半天都不知道错在哪。
第二个是时间字段问题。前端传时间字符串,后端LocalDateTime接收时格式对不上,接口直接400。我的解决方式是全局配置JSON序列化和反序列化格式,统一成yyyy-MM-dd HH:mm:ss,别在单个字段上加注解,否则漏了一个又是一个坑。
第三个是Token传递问题。联调时前端明明登录成功了,但调业务接口还是401。这个大多是因为前端把Token放在请求头里,而后端拦截器只从请求参数里取。前后端约定一个统一的Token传递方式,最好写进接口文档的第一页,能少吵很多架。
6.2 上线部署的检查清单
后端开发完毕,联调通过,并不意味着可以直接上线。我每次部署前会对着一个清单逐项检查,缺一项都可能出问题。
数据库相关:建表脚本是否执行完整、索引是否补齐、初始化数据是否导入。Redis相关:密码是否配置、序列化方式是否统一。文件上传相关:本地存储路径是否存在、目录权限是否正确;如果用了云存储,相关配置是否填齐。
JVM和内存相关:默认的JVM参数在大并发下常常不够用。我给苍穹外卖这类Spring Boot项目建议的启动参数是-Xms512m -Xmx512m起步,如果服务器内存允许,可以给到1G。太小的话,GC频繁,接口响应时间会很不稳定。
部署后的验证步骤也要列好:先确认接口健康检查通过,再走一遍用户下单到支付到商家接单的完整流程,重点看“支付回调日志”有没有输出、订单状态有没有正确流转、WebSocket有没有收到推送。全流程验证通过,才算真正收工。
6.3 常见问题速查表
我把这个阶段遇到的高频问题整理成一个速查表,方便你排查时对照:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 支付回调一直重试 | 回调接口返回失败或超时 | 查看日志定位是验签失败还是业务异常,返回成功才会停止重试 |
| 订单状态被覆盖成旧状态 | 并发更新未做条件判断 | 改用status条件更新,用影响行数判断是否成功 |
| 定时取消误伤已支付订单 | 取消逻辑未二次校验状态 | 消费端重新查询订单状态,已支付直接跳过 |
| 统计看板日期断档 | 聚合SQL缺少无数据日期 | 用Java侧日期补齐或递归CTE补零 |
| WebSocket频繁掉线 | 未做心跳保活或Session未移除 | 添加ping/pong心跳,连接关闭时清理Session |
| 联调时接口返回400 | 时间格式或参数类型不匹配 | 统一全局JSON序列化格式,检查前端传递类型 |
| 部署后接口响应缓慢 | 索引缺失或缓存未加 | 给查询条件建组合索引,统计接口加短时缓存 |
除了表格里的这些,还有一个比较隐蔽的问题容易被忽略:订单超时时间到底按哪个时间算。如果前端下单时传入一个“期望送达时间”或者“过期时间”,后端不能盲目用这个字段判断超时。我的经验是以下单时服务端时间作为起点,加15分钟,存成expire_time字段,取消任务统一拿这个字段和当前时间比较。避免因客户端时间不准、时区不一致导致超时判断出错。
6.4 压测提醒与日志规范(收尾前的自查)
最后一天如果有富余时间,我建议做个简单压测,不一定用工具,自己写个循环多线程调下单接口就行。重点看两个指标:下单接口在并发50时是否报错,支付回调重复执行时订单状态是否保持正确。
压测发现的很多问题其实是日志不规范导致的。比如回调日志没打订单号、金额、结果,排查时根本无从下手。我给状态机和支付回调设计了结构化日志,每一行固定包含“订单号、操作、原状态、目标状态、操作人、时间”,这个习惯帮我在生产环境排查问题时省了无数时间。哪怕是学习项目,也建议养成这个习惯——日志不是写给机器看的,是写给未来三天的自己看的。
写在最后
说实话,苍穹外卖这类项目做到最后一天,拼的已经不是代码能力了,而是对业务闭环的理解和对细节的把控。我在实际开发中最大的体会是:支付回调、状态机、超时取消、报表统计、WebSocket推送,这些功能单拎出来每一个都不难,但把它们放在同一个系统里,让它们彼此不冲突、数据不出错,才是真正的难点。
所以最后再分享一个实用技巧:在所有状态变更的地方,统一使用“条件更新+结构化日志”这套组合拳。条件更新解决并发问题,结构化日志解决排查问题。你把这个习惯带进任何项目,都能少踩一大半的坑。
后端部分到此就收工了,剩下的路就交给前端联调、测试验证和运维部署。希望这篇“后端最后一天”的实操复盘,能帮你把苍穹外卖的收尾做得更稳、更顺。