牛券常见面试问题汇总
作者:程序员马丁
Ragent AI —— 从 0 到 1 纯手工打造企业级 Agentic RAG,拒绝 Demo 玩具!AI 时代,助你拿个offer。
这一篇算是给牛券项目画个阶段性的句号。前面几章把领券、预约提醒、延时消息、结算规则、多线程优化这些核心流程都走了一遍,很多同学看完之后,私底下问得最多的,其实不是“代码怎么写”,而是“面试的时候,我到底该怎么讲?面试官会怎么问?这些细节怎么转成能聊得动的亮点?”
所以我干脆单独拎出一篇,把围绕牛券项目经常被问到的面试问题做个系统梳理:哪些点是高频问题,哪些细节稍微深挖一下就能拉开和普通简历的差距,哪些地方容易被面试官追问、又容易答偏。这篇不会再重复贴大段代码,而是站在“面试输出”的视角,把前面那些复杂的技术实现,压成一套可复用、能讲顺、逻辑自洽的问答思路。
如果你已经把牛券的主线文章看完,这篇可以当作一个总复盘;如果你是准备拿牛券项目去对标校招 / 社招面试,那这篇也可以当成“临门一脚”的提纲,帮你把项目里的知识点串成一段完整的故事。
创建优惠券模板责任链
1. 为什么要用责任链模式?
在没有引入责任链之前,创建优惠券模板的参数校验通常会写成下面几种形式:
- 在
createCouponTemplate方法里堆一长串if / else判断; - 或者抽一个
validateXXX(requestParam)大方法,里面塞满各种校验逻辑。
这种写法有几个明显问题:
- 职责不清晰、方法越来越臃肿:所有校验揉在一起,一个方法里既判断必填项、又判断格式、还要查库校验业务约束,导致方法越来越长,很难快速看懂「这一段是在干嘛」。
- 扩展性差,改动成本高:新增一个校验规则要么去改那段
if / else,要么继续往validateXXX里塞逻辑,很难做到「增加功能不修改原有代码」,违反开闭原则。 - 复用困难:某些校验逻辑(比如时间范围合法性、店铺是否存在等)本来可以复用到其他业务(比如修改模板、复制模板),但因为写死在方法里,想抽出来又很痛苦。
- 测试不友好:大方法里什么逻辑都有,单元测试要覆盖每个分支非常麻烦,也不方便针对某个校验规则单独写测试。
引入责任链后,把复杂的校验拆成多个「处理器」,每个处理器只关注一件事(比如非空校验、基础格式校验、业务校验等),既符合单一职责原则,又方便新增/下线某个规则,整体结构会清晰很多。
2. 执行创建优惠券会走哪些责任链 处理器?
例如:
- 必填项非空校验处理器
- 校验优惠券名称、优惠类型、有效时间、库存等是否为空;
- 类:
CouponTemplateCreateParamNotNullChainFilter。
- 基础格式与范围校验处理器
- 校验时间区间
valid_start_time < valid_end_time; - 校验库存必须是正数;
- 校验 JSON 字段结构是否符合预期、数值是否在合理范围内;
- 类:
CouponTemplateCreateParamBaseVerifyChainFilter。
- 校验时间区间
- 业务一致性校验处理器
- 校验店铺编号是否存在、是否属于当前登录用户;
- 校验商品编码是否存在、商品是否属于该店铺;
- 校验平台券/店铺券与来源、成本承担逻辑是否匹配;
- 类:
CouponTemplateCreateParamBizVerifyChainFilter。
- 扩展规则校验处理器(可选)
- 比如校验「同一时间同一店铺类似优惠活动是否超出上限」;
- 校验运营侧通用规则(如某些品类禁止打折等)。
每个处理器都实现统一接口 MerchantAdminAbstractChainHandler<T>,对外暴露一个 handler(T requestParam) 方法,责任明确、粒度清晰,后续想加某个新校验,只需要新增一个实现类接入责任链即可。
3. 责任链模式如何实现?
启动时扫描所有实现了 MerchantAdminAbstractChainHandler 的 Bean,按 mark() 分组,并根据 Ordered 接口的顺序进行排序,简要流程:
-
启动时扫描 Bean:
Map<String, MerchantAdminAbstractChainHandler> chainFilterMap =
applicationContext.getBeansOfType(MerchantAdminAbstractChainHandler.class);把 Spring 容器中所有责任链处理器 Bean 拿出来。
-
按 mark 分组
- 遍历这些 Bean,调用每个 Bean 的
mark()方法; - 以 mark 为 key,把属于同一场景(例如「创建优惠券模板」)的处理器放入同一个
List<MerchantAdminAbstractChainHandler>里。
- 遍历这些 Bean,调用每个 Bean 的
-
按优先级排序
- 各处理器都实现了
Ordered接口; - 通过
unsortedChainHandlers.sort(Comparator.comparing(Ordered::getOrder)); - 实现「order 值越小,处理越靠前」,比如:
- 0:非空校验
- 10:基础格式校验
- 20:业务校验
- 各处理器都实现了
-
执行业务链 当业务中调用:
merchantAdminChainContext.handler(mark, requestParam);时,就会拿到这个
mark对应的处理器列表,然后顺序执行:abstractChainHandlers.forEach(each -> each.handler(requestParam));
这样一来,责任链的组装、排序、执行都被封装到了 MerchantAdminChainContext 里,开发者只需要关心「我传哪个 mark、requestParam 是什么」就够了。
4. 责任链执行遇到异常如何中断?
在当前 MerchantAdminChainContext 实现里,是通过顺序 forEach 调用每个处理器的:
abstractChainHandlers.forEach(each -> each.handler(requestParam));
要实现「失败即中断」的效果,最常见也是最简单的做法是:在处理器内部抛出异常,例如统一的业务异常 BizException / ClientException 等:
@Override
public void handler(CouponTemplateSaveReqDTO requestParam) {
if (requestParam.getStock() == null || requestParam.getStock() <= 0) {
throw new ClientException("库存必须为正整数");
}
}
一旦抛出异常:
- 当前处理器的后续逻辑不会执行;
forEach会立即终止;- 上层
createCouponTemplate可以捕获异常并返回统一的错误响应。 - 最终会通过 Web 全局异常拦截器捕获并返回前端。
扩展思路:
如果以后想支持「不中断,只记录告警」的场景,也可以在责任链模型上做扩展,例如:
- 在
handler方法里返回一个状态对象; - 状态对象中包含
pass / fail / warn等标记; - 上层根据状态决定是否继续执行、是否告警等。
但在牛券实现「参数校验」场景下,校验失败直接抛异常中断,是最简单也最常见的实践。
5. 如何复用责任链模式?
当前框架的设计是「业务无关」+「通过 mark 区分业务」,因此可以很方便复用到其他场景,例如:
- 修改优惠券模板;
- 创建优惠券发 放批次;
- 审批操作校验;
- 商家后台其他表单类操作的参数校验。
复用方法:
-
定义新的业务标识:在
ChainBizMarkEnum中新增一个标识,例如:MERCHANT_ADMIN_UPDATE_COUPON_TEMPLATE_KEY,
MERCHANT_ADMIN_CREATE_COUPON_BATCH_KEY -
为新场景编写处理器
- 处理器依然实现
MerchantAdminAbstractChainHandler<对应DTO>; - 在
mark()中返回新的标识; - 在
getOrder()中指定执行顺序。
- 处理器依然实现
-
业务中直接调用
merchantAdminChainContext.handler(
ChainBizMarkEnum.MERCHANT_ADMIN_UPDATE_COUPON_TEMPLATE_KEY.name(),
updateReqDTO
);
这样,责任链基础代码 (MerchantAdminAbstractChainHandler、MerchantAdminChainContext) 都是通用的,真正和业务耦合的只有:
- 具体处理器里的校验逻辑;
- 每条链对应的 mark 枚举。