Skip to main content

牛券常见面试问题汇总

作者:程序员马丁

在线博客:https://nageoffer.com

note

热门项目实战社群,收获国内众多知名公司面试青睐,近千名同学面试成功!助力你在校招或社招上拿个offer。

这一篇算是给牛券项目画个阶段性的句号。前面几章把领券、预约提醒、延时消息、结算规则、多线程优化这些核心流程都走了一遍,很多同学看完之后,私底下问得最多的,其实不是“代码怎么写”,而是“面试的时候,我到底该怎么讲?面试官会怎么问?这些细节怎么转成能聊得动的亮点?”

所以我干脆单独拎出一篇,把围绕牛券项目经常被问到的面试问题做个系统梳理:哪些点是高频问题,哪些细节稍微深挖一下就能拉开和普通简历的差距,哪些地方容易被面试官追问、又容易答偏。这篇不会再重复贴大段代码,而是站在“面试输出”的视角,把前面那些复杂的技术实现,压成一套可复用、能讲顺、逻辑自洽的问答思路。

如果你已经把牛券的主线文章看完,这篇可以当作一个总复盘;如果你是准备拿牛券项目去对标校招 / 社招面试,那这篇也可以当成“临门一脚”的提纲,帮你把项目里的知识点串成一段完整的故事。

创建优惠券模板责任链

1. 为什么要用责任链模式?

在没有引入责任链之前,创建优惠券模板的参数校验通常会写成下面几种形式:

  • createCouponTemplate 方法里堆一长串 if / else 判断;
  • 或者抽一个 validateXXX(requestParam) 大方法,里面塞满各种校验逻辑。

这种写法有几个明显问题:

  1. 职责不清晰、方法越来越臃肿:所有校验揉在一起,一个方法里既判断必填项、又判断格式、还要查库校验业务约束,导致方法越来越长,很难快速看懂「这一段是在干嘛」。
  2. 扩展性差,改动成本高:新增一个校验规则要么去改那段 if / else,要么继续往 validateXXX 里塞逻辑,很难做到「增加功能不修改原有代码」,违反开闭原则。
  3. 复用困难:某些校验逻辑(比如时间范围合法性、店铺是否存在等)本来可以复用到其他业务(比如修改模板、复制模板),但因为写死在方法里,想抽出来又很痛苦。
  4. 测试不友好:大方法里什么逻辑都有,单元测试要覆盖每个分支非常麻烦,也不方便针对某个校验规则单独写测试。

引入责任链后,把复杂的校验拆成多个「处理器」,每个处理器只关注一件事(比如非空校验、基础格式校验、业务校验等),既符合单一职责原则,又方便新增/下线某个规则,整体结构会清晰很多。

2. 执行创建优惠券会走哪些责任链处理器?

例如:

  1. 必填项非空校验处理器
    • 校验优惠券名称、优惠类型、有效时间、库存等是否为空;
    • 类:CouponTemplateCreateParamNotNullChainFilter
  2. 基础格式与范围校验处理器
    • 校验时间区间 valid_start_time < valid_end_time
    • 校验库存必须是正数;
    • 校验 JSON 字段结构是否符合预期、数值是否在合理范围内;
    • 类:CouponTemplateCreateParamBaseVerifyChainFilter
  3. 业务一致性校验处理器
    • 校验店铺编号是否存在、是否属于当前登录用户;
    • 校验商品编码是否存在、商品是否属于该店铺;
    • 校验平台券/店铺券与来源、成本承担逻辑是否匹配;
    • 类:CouponTemplateCreateParamBizVerifyChainFilter
  4. 扩展规则校验处理器(可选)
    • 比如校验「同一时间同一店铺类似优惠活动是否超出上限」;
    • 校验运营侧通用规则(如某些品类禁止打折等)。

每个处理器都实现统一接口 MerchantAdminAbstractChainHandler<T>,对外暴露一个 handler(T requestParam) 方法,责任明确、粒度清晰,后续想加某个新校验,只需要新增一个实现类接入责任链即可。

3. 责任链模式如何实现?

启动时扫描所有实现了 MerchantAdminAbstractChainHandler 的 Bean,按 mark() 分组,并根据 Ordered 接口的顺序进行排序,简要流程:

  1. 启动时扫描 Bean

    Map<String, MerchantAdminAbstractChainHandler> chainFilterMap =
    applicationContext.getBeansOfType(MerchantAdminAbstractChainHandler.class);

    把 Spring 容器中所有责任链处理器 Bean 拿出来。

  2. 按 mark 分组

    • 遍历这些 Bean,调用每个 Bean 的 mark() 方法;
    • 以 mark 为 key,把属于同一场景(例如「创建优惠券模板」)的处理器放入同一个 List<MerchantAdminAbstractChainHandler> 里。
  3. 按优先级排序

    • 各处理器都实现了 Ordered 接口;
    • 通过 unsortedChainHandlers.sort(Comparator.comparing(Ordered::getOrder));
    • 实现「order 值越小,处理越靠前」,比如:
      • 0:非空校验
      • 10:基础格式校验
      • 20:业务校验
  4. 执行业务链 当业务中调用:

    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 区分业务」,因此可以很方便复用到其他场景,例如:

  • 修改优惠券模板;
  • 创建优惠券发放批次;
  • 审批操作校验;
  • 商家后台其他表单类操作的参数校验。

复用方法:

  1. 定义新的业务标识:在 ChainBizMarkEnum 中新增一个标识,例如:

    MERCHANT_ADMIN_UPDATE_COUPON_TEMPLATE_KEY,
    MERCHANT_ADMIN_CREATE_COUPON_BATCH_KEY
  2. 为新场景编写处理器

    • 处理器依然实现 MerchantAdminAbstractChainHandler<对应DTO>
    • mark() 中返回新的标识;
    • getOrder() 中指定执行顺序。
  3. 业务中直接调用

    merchantAdminChainContext.handler(
    ChainBizMarkEnum.MERCHANT_ADMIN_UPDATE_COUPON_TEMPLATE_KEY.name(),
    updateReqDTO
    );

这样,责任链基础代码 (MerchantAdminAbstractChainHandlerMerchantAdminChainContext) 都是通用的,真正和业务耦合的只有:

  • 具体处理器里的校验逻辑;
  • 每条链对应的 mark 枚举。

创建优惠券模板分库分表

1. 为什么优惠券模板这张表需要考虑分库分表?单表顶不住吗?

从业务侧看,优惠券的主体是商家。按“3000 万商家,每个商家平均创建 100 张优惠券模板”来估算,单表数据量大概会到 30 亿行。而且这个数字还是“当下的保守估计”,随着商家增长和运营玩法迭代,未来只会越来越大。

单表撑不住主要体现在几方面:

  • 存储压力:几十亿行堆在一张表里,备份、迁移、DDL 调整都非常痛苦。
  • 查询性能:即便走索引,页分裂、索引层级增加后,磁盘 IO 次数会明显上升,延迟抖动会变大。
  • 写入性能:高并发写入场景下,行锁/页锁竞争会越来越明显。

所以与其等到单表撑爆再救火,不如在设计之初就按「海量数据 + 未来增长」的维度规划好分库分表方案,把优惠券模板做成天然可扩展的存储结构。

解锁付费内容,👉 戳