怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

项目背景设定 本地生活平台的商家菜单与商品中台服务端,Java + Spring Boot + MySQL + Redis + MQ。服务对象是到店与外卖商家,既有单店也有连锁品牌(总部统一管菜单、门店有差异化)。商家侧的管理界面在 本地生活 · Vue · 商家管理后台 那一页,这一页讲模型与引擎层;履约与派单在 本地生活履约即时配送派单调度 两页。
为什么选这三个模块 很多人以为菜单就是「商品列表」,实际它是整个题库里商品模型最复杂的一种,而且复杂度的来源和电商完全不同。电商卖的是标品——SKU 是预先确定的、有条码的实体;菜单卖的是配置型商品——「一杯奶茶」要选杯型、甜度、冰量、加料,可选组合成千上万,不可能预生成 SKU。三个模块对应三层难点:规格与加料的动态组合(组合爆炸怎么绕开)、连锁多门店的继承与差异化(总部改一下影响几百家门店)、时段可售与估清一致性(同一个商品早上不卖、卖完了要秒级下架、而下单那一刻必须校准)。

模块一:规格组合与加料的商品建模

  1. 规格组合与加料的商品建模(不预生成组合 + 规格组语义分离 + 价格按基价加增量 + 组合合法性校验 + 下单快照)★★★
    简历这样写 配置型商品的规格与加料建模(商品加规格组加选项三层建模 + 组合不预生成而在下单时校验 + 必选与多选与互斥的规格组语义 + 基价加增量的价格计算 + 组合合法性服务端强校验 + 下单时规格与价格快照):餐饮商品的可选组合随规格组数量指数增长,预生成 SKU 会造成组合爆炸且规格一改需全量重建,因此改为「商品 + 规格组 + 选项」三层建模、组合不落库,仅在下单时校验合法性并按基价加各选项增量计算价格;规格组区分必选/单选/多选/上限/互斥等语义,服务端对必选未选、超出多选上限、互斥同选、选项不属于该商品等做强校验(不信任客户端提交的组合);下单时将规格组合与价格明细快照落单,使商家改价改规格后历史订单仍可原样复现。改造后新增一个规格组由需要重建组合数据改为仅新增配置,客户端伪造组合下单由服务端校验拦下
    展开完整拆解
    为什么要这么设计

    第一版是照着电商的商品模型做的:商品加 SKU 两层,每一个可售的规格组合就是一条 SKU 记录。电商里这么做完全正确——一件衣服的「红色 / L 码」是一个有库存、有条码的实体。

    但餐饮不是这样。一杯奶茶要选杯型(中/大)、甜度(无糖/三分/五分/七分/全糖)、冰量(去冰/少冰/正常)、加料(珍珠/椰果/布丁,可多选)。光这四组,组合数就是 2 × 5 × 3 × 加料的子集数——加料三种就有 8 个子集,合计 240 种。再加一个规格组就翻几倍。这就是组合爆炸。

    预生成 SKU 带来三个具体问题。一是数据量:一家奶茶店几十个商品,每个几百种组合,一家店就是上万条 SKU,几十万家商家的量级不可接受。二是维护成本:商家想加一个「常温」的冰量选项,所有相关组合都要重新生成一遍,而这中间任何一步失败就是数据不一致。三是价格维护:珍珠加 3 元这件事,要在几十条 SKU 上各写一遍,改价就要改几十处。

    所以核心决策是组合不落库:模型只存「商品 + 规格组 + 选项」三层,用户选出来的组合在下单那一刻才校验和计价。这一个决定同时解决了上面三个问题——数据量从组合数降到选项数,加选项只是新增一条配置,价格只在选项上维护一份。

    但组合不落库带来一个新责任:合法性必须由服务端校验,而且不能信任客户端。因为组合不是从库里选出来的,而是客户端提交上来的一组选项 ID。如果不校验,客户端可以提交任意组合——我们真的遇到过提交「不属于该商品的选项」来凑一个低价的情况。要校验的至少有四类:必选组没选、超出多选上限、互斥选项同时选、选项不属于该商品

    第二个必须处理的是价格计算方式。既然组合不落库,价格也不能按组合存,只能算出来:基价加各选项的增量。大杯加 4 元、珍珠加 3 元,都挂在选项上。这也让「所有大杯统一涨 1 元」变成改一个数字,而不是批量改几千条记录。

    第三个是快照。商家改价、改规格、下架某个选项都很频繁。如果历史订单只存选项 ID,商家把「珍珠」删了之后,那张订单就再也显示不出用户买了什么;改价之后订单金额也复算不出来。所以下单时必须把规格组合的名称和价格明细快照落单。这和旅游那边的分摊快照、退改规则快照是同一条原则。

    整体链路
    建模:三层,组合不落库 │ ├─ 商品:名称、图片、基价、分类、所属门店 │ ├─ 规格组:挂在商品上,带语义配置 │ 是否必选 │ 单选 / 多选 │ 多选的上限与下限 │ 组内互斥关系 │ ├─ 选项:挂在规格组下 │ 名称、价格增量、是否默认、是否可售 │ └─ 组合不存表,这是整个模块的核心决策 杯型2 × 甜度5 × 冰量3 × 加料子集8 = 240 种 再加一组就翻几倍 → 组合爆炸 为什么不预生成组合(三个具体代价) ├─ 数据量:一家店上万条 SKU,几十万商家不可接受 ├─ 维护:加一个「常温」选项要重建所有相关组合 └─ 价格:珍珠加 3 元要在几十条记录上各写一遍 下单时的合法性校验(组合来自客户端,不能信) │ ├─ 选项必须属于该商品的规格组 │ → 真遇到过提交别的商品的选项来凑低价 │ ├─ 必选规格组必须有选择 ├─ 单选组不能选多个 ├─ 多选组不能超上限、不能低于下限 ├─ 互斥选项不能同时出现 ├─ 每个选项当前必须可售(见模块三) │ └─ 校验失败返回明确原因,不要只说「参数错误」 客户端要能提示用户「珍珠已售完,请重选」 价格计算(算出来,不查出来) │ ├─ 实付 = 基价 + Σ 各选项价格增量 ├─ 全程最小货币单位整数运算 ├─ 价格只在选项上维护一份 │ 「所有大杯涨 1 元」= 改一个数字 │ 而不是批量改几千条记录 │ └─ 服务端重算,绝不接受客户端传来的价格 客户端传价格 = 把定价权交给客户端 下单快照(商家改动频繁,历史必须可复现) ├─ 落单时快照:规格组名、选项名、各项增量、基价 ├─ 不只存选项 ID │ 商家删掉「珍珠」后,订单要还能显示买了什么 │ 商家改价后,订单金额要还能复算 └─ 和分摊快照、退改规则快照是同一条原则 套餐(本质是「商品的组合」,多一层) ├─ 套餐 = 若干子项 + 套餐价 ├─ 子项可配「可替换组」(主食可选米饭或面条) ├─ 子项自己的规格照常生效 └─ 套餐价与子项原价之和的差额要能分摊 部分退款时要用(和打包产品是同一个问题)
    分步拆解
    1. 先想清楚餐饮商品和电商标品的本质差别。电商 SKU 是预先确定的实体(有条码、有库存),菜单商品是配置型的——组合在用户下单那一刻才成立。照搬电商模型是第一版所有问题的根源。
    2. 建模分三层:商品、规格组、选项。组合不落库。这是整个模块的核心决策。
    3. 算一下组合数就知道为什么不能预生成。杯型 2 × 甜度 5 × 冰量 3 × 加料子集 8 = 240 种,再加一个规格组就翻几倍
    4. 规格组要带完整的语义配置:必选、单选或多选、多选上下限、组内互斥。只有「单选/多选」两个标志位是不够的,实际业务里「至少选一个、最多选三个」很常见。
    5. 价格增量挂在选项上,不挂在组合上。这样「所有大杯涨 1 元」是改一个数字,而不是批量改几千条记录。
    6. 价格一律服务端重算,绝不接受客户端传来的金额。客户端传价格等于把定价权交给客户端,这是基本安全线。
    7. 全程最小货币单位整数运算。加料叠加会连续做加法,浮点误差会累积。
    8. 组合合法性必须服务端强校验,因为组合来自客户端而不是从库里选的。这是「组合不落库」的代价,必须付。
    9. 校验至少四类:选项是否属于该商品、必选组是否已选、单选或多选上下限、互斥是否冲突。我们真遇到过提交不属于该商品的选项来凑低价
    10. 还要校验每个选项当前是否可售。加料卖完了(估清)就不能被选中,这一点和模块三联动。
    11. 校验失败要返回明确原因,不要只回「参数错误」。客户端需要能提示「珍珠已售完,请重新选择」,笼统的报错会让用户反复重试
    12. 下单时把规格组合快照落单:规格组名、选项名、各项增量、基价。不能只存选项 ID。
    13. 快照的理由很具体:商家删掉某个选项后,历史订单还要能显示用户买了什么;改价后订单金额还要能复算。这和分摊快照、退改规则快照是同一条原则——凡是会变的规则参与了计算,就要在成交时刻固化
    14. 套餐单独建模:套餐等于若干子项加套餐价,子项可配可替换组,子项自己的规格照常生效。
    15. 套餐价与子项原价之和的差额要能分摊到子项。部分退款时要用——和打包产品的分摊是完全同一个问题
    关键决策与取舍

    组合不落库是这个模块唯一真正重要的决策,它的代价要说清楚。收益是数据量、维护成本、价格维护三方面的量级改善。代价有两个:一是合法性校验的责任转移到了服务端,客户端提交的是一组选项 ID,服务端必须完整校验,这部分逻辑不能有漏;二是没法给某个具体组合单独定价——如果商家想让「大杯加珍珠」这个特定组合有个特价,基价加增量的模型表达不了。我们的处理是把这类需求引导到「套餐」上(把特定组合做成一个套餐商品),而不是给组合模型开一个例外口子。这个选择的理由是:一旦允许组合级定价,就等于又要落组合了,模型会退回原点。

    为什么价格用「基价 + 增量」而不是「组合价」。组合价更灵活(每个组合可以任意定价),但它必然要求组合落库。基价加增量牺牲了灵活性,换来的是「改一处生效全局」——这对商家侧的运营效率影响很大,尤其连锁品牌批量调价。取舍的判断依据是:商家真实的调价行为绝大多数是「某个选项统一涨价」而不是「某个具体组合单独调价」,模型应该让高频操作变简单,而不是为低频需求保留通用性。

    踩过的坑:照搬电商模型预生成 SKU,一家连锁店的菜单同步跑了四十多分钟还没跑完。商家在总部加了一个规格组,系统要为所有门店的所有相关商品重建组合,任务跑到超时被杀,重跑又从头开始,商家那边看到的是「保存了但没生效」。修法是改为三层建模、组合不落库教训是:模型选错了,后面所有的性能优化都是在给错误的模型擦屁股——当时我们第一反应是优化那个同步任务(加并发、分批),做了才发现问题在模型,不在任务。

    踩过的坑二:信任客户端提交的组合,被提交了不属于该商品的选项。有人发现选项 ID 是全局唯一的,于是拿一个便宜商品的选项 ID 配到贵商品上提交,服务端照单计价,得到了一个不该存在的低价。修法是校验每个选项必须属于该商品的规格组,并把这条加入回归用例。教训是:当「合法值的集合」不再由数据库结构保证时(组合不落库就是这种情况),就必须在应用层显式校验——外键约束能挡住的东西,去掉外键之后要自己挡。

    踩过的坑三:只存选项 ID 不存快照,商家删了选项之后历史订单显示不出内容。商家把「布丁」下架并删除,之前所有加了布丁的订单,详情页那一行变成空白,客服处理退款时不知道用户买了什么。修法是下单时快照规格组合的名称与价格明细教训是:订单是历史事实的记录,它不能依赖仍在变化的商品配置——这条原则我在旅游的分摊快照和退改规则快照上是同一个判断。

    踩过的坑四:接受了客户端传来的价格。早期为了少算一次,下单接口接收客户端计算好的金额,结果被改包提交了一分钱的订单。修法是服务端一律重算,客户端传的价格只用于比对并在不一致时拒单(顺带能发现前后端计价逻辑不一致的 bug)。教训是:客户端传价格等于把定价权交给客户端,这是没有任何讨论空间的红线。

    没做的部分:没做规格组合的销量统计与推荐(「大杯七分糖去冰」是最热组合,可以做一键复购)。这需要按组合聚合统计,而组合不落库让聚合变复杂,当时只做了商品级销量。也没做跨商品的加料共享(同一个「珍珠」被多个商品复用、改一次价全部生效),当时加料是挂在商品上的,连锁品牌反馈过这个需求。

    数字是怎么测的

    菜单同步耗时:改造前后的结构性对比——改造前新增一个规格组需要重建所有相关组合(我们遇到过跑四十多分钟还没完、超时被杀的情况),改造后是新增一条配置记录「从重建数据到新增配置」这个对比比耗时数字更能说明模型选对了,因为耗时数字依赖菜单规模。

    数据量:同一家商家在两种模型下的记录数对比(组合数 vs 选项数)。这个对比是量级的,很直观。要说明商家的规格组配置,因为倍数随规格组数量变化。

    组合校验:确定性验证,写自动化用例覆盖每一类非法组合:选项不属于该商品、必选组未选、单选组选多个、多选超上限、互斥同选、选项已估清。报的是「覆盖了哪几类非法组合」而不是通过率——列出类型比给一个百分比有说服力。

    价格计算一致性:服务端重算与客户端计算不一致的拦截次数。这个数字有双重价值:证明服务端重算在生效;不一致次数突增说明前后端计价逻辑出现了分叉,是个有用的告警信号。

    历史订单可复现:确定性验证——下单后删除某个选项并改价,检查订单详情仍能完整显示规格组合与当时的价格明细。改造前会变空白,改造后正常。

    下单接口耗时:因为组合校验和计价都放到了下单时,要主动报这一段的耗时,否则会被追问「不预生成是不是把成本转移到了下单链路」。诚实地给出校验加计价的 P95,并说明它相比查一条 SKU 记录多了多少。

    不要报「菜单准确率 100%」。菜单内容由商家维护,错了大多是商家配错,不是系统问题正确表述是「模型从组合级降到选项级、新增规格从重建数据变为新增配置、非法组合由服务端强校验并覆盖哪几类、历史订单由快照保证可复现」——四个可核查的事实。

    面试追问
    Q:一杯奶茶有杯型、甜度、冰量、加料,这个商品模型怎么设计? A:三层建模「商品 + 规格组 + 选项」,而且组合不落库——这是整个设计里唯一真正重要的决策。第一版照搬电商的「商品 + SKU」两层,每个可售组合一条 SKU 记录,但餐饮不是标品:杯型 2 × 甜度 5 × 冰量 3 × 加料子集 8 就是 240 种组合,再加一个规格组翻几倍,这就是组合爆炸。预生成有三个具体代价:数据量(一家店上万条 SKU,几十万商家不可接受)、维护(商家加一个「常温」选项要重建所有相关组合)、价格(珍珠加 3 元要在几十条记录上各写一遍)。我们真的被这个打过:连锁商家在总部加了一个规格组,系统要为所有门店重建组合,任务跑四十多分钟超时被杀,商家看到的是「保存了但没生效」。当时第一反应是优化那个同步任务(加并发、分批),做了才发现问题在模型不在任务教训是模型选错了,后面所有性能优化都是在给错误的模型擦屁股。
    Q:组合不落库,怎么保证用户提交的组合是合法的? A:必须服务端强校验,而且不能信任客户端——这是「组合不落库」的代价,必须付。因为组合不是从库里选出来的,而是客户端提交上来的一组选项 ID。要校验至少五类:选项是否属于该商品的规格组、必选组是否已选、单选组是否选了多个、多选是否超上下限、互斥选项是否同时出现,还要校验每个选项当前是否可售(估清了就不能选)。我们踩过一个真实的坑:有人发现选项 ID 是全局唯一的,于是拿一个便宜商品的选项 ID 配到贵商品上提交,服务端照单计价,得到了一个不该存在的低价教训是:当「合法值的集合」不再由数据库结构保证时,就必须在应用层显式校验——外键约束能挡住的东西,去掉外键之后要自己挡。还有一个体验细节:校验失败要返回明确原因,不能只回「参数错误」,客户端得能提示「珍珠已售完,请重新选择」,笼统的报错会让用户反复重试
    Q:价格怎么算?能不能给某个具体组合单独定价? A:价格用「基价 + 各选项增量」算出来,不查出来;给具体组合单独定价我们是刻意不支持的。基价加增量的收益是「改一处生效全局」——「所有大杯涨 1 元」就是改一个数字,而不是批量改几千条记录,这对连锁品牌批量调价影响很大。代价确实是没法给「大杯加珍珠」这个特定组合定特价我们的处理是把这类需求引导到套餐上(把特定组合做成一个套餐商品),而不是给组合模型开例外口子——因为一旦允许组合级定价,就等于又要落组合,模型会退回原点取舍的判断依据是:商家真实的调价行为绝大多数是「某个选项统一涨价」而不是「某个具体组合单独调价」,模型应该让高频操作变简单,而不是为低频需求保留通用性。另外两条硬线:全程最小货币单位整数运算(加料叠加是连续加法,浮点误差会累积);价格一律服务端重算,绝不接受客户端传来的金额——我们早期为了少算一次接收了客户端算好的金额,结果被改包提交了一分钱的订单
    Q:商家把某个加料删了,之前的订单怎么显示? A:靠下单时的快照,这也是我们踩过的坑。早期订单只存选项 ID,商家把「布丁」下架并删除后,之前所有加了布丁的订单,详情页那一行变成空白,客服处理退款时压根不知道用户买了什么。修法是下单时把规格组名、选项名、各项价格增量、基价一起快照落单,而不只存 ID。教训是:订单是历史事实的记录,它不能依赖仍在变化的商品配置。这条原则我在旅游业务的打包价分摊快照、退改规则快照上是完全同一个判断——凡是会变的规则或参数参与了计算,就必须在成交时刻固化下来快照还要存价格明细而不只是总价,因为部分退款时要知道每一项各是多少钱。顺带说套餐的一个相关问题:套餐价低于子项原价之和,那个差额要能分摊到子项,否则用户只退套餐里的一项时算不出该退多少——这和旅游的打包产品分摊是完全同一个问题

模块二:连锁多门店的菜单继承与差异化

  1. 连锁多门店的菜单继承与差异化(模板到门店的继承链 + 门店覆盖独立分层 + 总部改动影响面预演 + 批量下发与失败可重试)★★★
    简历这样写 连锁品牌菜单的继承模型与批量下发(品牌模板到区域到门店的三级继承 + 门店覆盖项独立分层不被下发覆盖 + 字段级继承标记 + 下发前影响面预演 + 批次化下发与逐店失败可重试 + 覆盖项与继承项可视区分):连锁品牌需总部统一管菜单而门店保留差异(价格、上下架、估清),早期下发采用整体覆盖,导致门店的本地调价与选品在每次总部下发后被抹掉;改为三级继承模型(品牌模板 → 区域 → 门店)并把门店覆盖项独立分层、按字段标记继承或覆盖,下发只更新继承字段而不触碰覆盖字段;总部下发前提供影响面预演(将影响多少门店、多少商品、其中多少存在覆盖冲突),下发批次化并支持逐店失败重试,避免部分成功后无从收拾。改造后门店本地调价在总部下发后得以保留,总部误操作由预演环节在下发前暴露
    展开完整拆解
    为什么要这么设计

    连锁品牌的诉求是矛盾的两句话:「菜单要总部统一管,不能各店乱来」「门店必须能有差异,因为各地成本和供应不一样」。这两句都对——品牌形象需要统一,但一线城市的门店成本高要卖贵一点,某个门店的供应链拿不到某种原料就得下架那个商品。

    第一版的实现是总部下发即整体覆盖:总部改完菜单点下发,所有门店的菜单被替换成总部版本。门店自己调过的价格、自己下架的商品,全被抹掉。门店店长发现之后就不再信任系统,改成每次总部下发后自己再改一遍——而这又会在下一次下发时被抹掉,形成一个持续的对抗。有个区域经理直接在群里问「能不能别下发了」。

    问题的本质和之前遇过的一样:一份数据有多个写入方(总部和门店),而它们在抢同一个字段。「谁后写谁赢」在有自动化写入方(总部批量下发)参与时,人(门店)永远赢不了。

    所以核心设计是继承 + 分层

    三级继承链:品牌模板 → 区域 → 门店。区域这一层是必要的——连锁品牌的差异化通常是按区域来的(华东区统一一个价),如果只有「总部」和「门店」两层,区域级的调整就要在几十家门店上各操作一遍。

    门店的覆盖项独立分层,并且按字段标记「继承」还是「覆盖」。关键在于字段级而不是记录级:门店可能只覆盖了价格,商品名称、图片、规格配置仍然跟随总部。如果按记录级处理,门店一改价就等于整条记录脱离继承,之后总部改图片这家店就收不到了。

    下发只更新继承字段,不触碰覆盖字段。这是解决对抗的关键一步。

    第二个设计来自另一类事故:总部一次误操作影响了几百家门店,而下发之前谁都不知道会影响多少。总部想给某个商品调价,操作时选错了范围,把一整个品类都下发了,几百家门店的几十个商品价格全变。发现时已经卖了两小时。

    所以加了下发前的影响面预演:这次下发会影响多少门店、多少商品、其中有多少存在覆盖冲突(门店覆盖过但总部这次要改同一个字段)。把「不可见的批量影响」变成下发前的一屏确认

    第三个是下发的可靠性。几百家门店的下发不可能是一个事务,中间必然有失败(某个门店的数据有问题、某次调用超时)。第一版失败之后不知道哪些成功哪些没成功,只能整个重新下发一遍,而重新下发又会把中间门店自己的操作覆盖掉。所以要批次化:每次下发一个批次,逐店记录结果,失败的可单独重试

    最后一个容易被忽略但很影响信任的点:界面上要能区分「这个值是继承来的」还是「本店覆盖的」。门店店长必须能一眼看出哪些是自己改过的,否则他不知道总部下发会不会影响自己。

    整体链路
    继承模型(三级,缺了区域层会很痛) │ ├─ 品牌模板:总部维护的标准菜单 ├─ 区域:按区域的统一调整(华东区统一价) ├─ 门店:单店的差异化 │ └─ 为什么必须有区域层 连锁的差异化通常按区域来 只有总部和门店两层 → 区域调整要在几十家店各操作一遍 分层与字段级继承标记(解决多写入方抢字段) │ ├─ 每个字段标记:继承 / 覆盖 │ 门店只覆盖价格 │ 名称、图片、规格配置仍跟随总部 │ ├─ 必须是字段级,不能是记录级 │ 记录级 → 门店一改价,整条脱离继承 │ 之后总部改图片这家店收不到 │ └─ 和房源修正层、组织手工层是同一个模式 有多个写入方时,各给一层并定义优先级 下发(只更新继承字段,不触碰覆盖字段) │ ├─ 这一步是解决「总部与门店对抗」的关键 │ 第一版整体覆盖 → 门店改的全被抹掉 │ → 店长不再信任系统,每次下发后自己再改一遍 │ → 又被下一次下发抹掉,形成持续对抗 │ └─ 门店覆盖的字段,下发一律跳过 下发前的影响面预演(把不可见的批量影响变成一屏确认) │ ├─ 本次将影响 多少门店 / 多少商品 ├─ 其中有多少存在覆盖冲突 │ 门店覆盖过,而总部这次要改同一字段 ├─ 冲突项列出来,让总部决定「跳过」还是「强制覆盖」 │ └─ 真实事故:总部调价选错范围 把一整个品类下发了,几百家店几十个商品价格全变 卖了两小时才发现 批次化下发(几百家门店不可能是一个事务) │ ├─ 每次下发生成批次号 ├─ 逐店记录结果:成功 / 失败 / 失败原因 ├─ 失败的可单独重试,不必整批重发 │ 第一版失败后不知道哪些成功 │ 只能整批重发 → 又覆盖了中间门店的操作 │ └─ 支持按批次回滚 可见性(不显示继承关系,信任就建不起来) ├─ 界面上区分「继承自总部」与「本店已覆盖」 ├─ 门店可主动「恢复继承」放弃自己的覆盖 ├─ 总部能看到哪些门店覆盖了哪些字段 └─ 店长要能一眼看出哪些是自己改过的 否则他不知道下发会不会影响自己
    分步拆解
    1. 先识别出这是「一份数据多个写入方」的问题。总部和门店在抢同一个字段,「谁后写谁赢」在有自动化写入方参与时,人永远赢不了——这是第一版对抗的根源。
    2. 建三级继承链:品牌模板 → 区域 → 门店。区域这一层是必要的,连锁的差异化通常按区域来,只有两层的话区域调整要在几十家门店各操作一遍。
    3. 门店覆盖项独立分层,同步下发只写继承层。和房源治理的「修正层」、组织同步的「手工层」是完全同一个模式。
    4. 继承标记必须是字段级,不能是记录级。门店可能只覆盖价格,名称图片规格仍跟随总部;记录级会导致门店一改价就整条脱离继承,之后总部改图片这家店收不到
    5. 下发时跳过门店已覆盖的字段。这一步直接解决了总部与门店的对抗。
    6. 下发前做影响面预演:影响多少门店、多少商品、多少存在覆盖冲突。把不可见的批量影响变成下发前的一屏确认。
    7. 覆盖冲突要列出明细,让总部决定「跳过」还是「强制覆盖」。强制覆盖是合法需求(总部统一调价就是要盖掉门店的),但它必须是一个显式选择而不是默认行为
    8. 下发批次化,生成批次号,逐店记录结果与失败原因。几百家门店不可能是一个事务,中间必然有失败
    9. 失败的门店可单独重试,不必整批重发。第一版失败后不知道哪些成功,只能整批重发,而重发又覆盖了中间门店的操作
    10. 支持按批次回滚。误操作发生后要能整批回退,而不是逐店手工改。
    11. 界面上必须区分「继承自总部」与「本店已覆盖」。不显示继承关系,信任就建不起来——店长得一眼看出哪些是自己改过的。
    12. 门店要能主动「恢复继承」放弃自己的覆盖。否则一旦覆盖就永久脱离总部更新,只能靠人记着。
    13. 总部要能看到哪些门店覆盖了哪些字段。这是总部做决策的输入——如果一半门店都覆盖了某个价格,说明总部的定价不合理。
    14. 区域层的变更也要走同一套预演与批次机制。不要因为区域比总部小就省掉这些保护。
    关键决策与取舍

    字段级继承 vs 记录级继承,这是这个模块最实质的设计选择。记录级实现简单(一个标志位表示这条记录是否脱离继承),但它把继承变成了全有或全无:门店只想改个价格,结果整条记录脱离总部管理,之后总部更新商品图片、修正商品描述、调整规格配置,这家店全都收不到,日积月累门店菜单会和总部严重分叉。字段级的代价是存储和实现复杂度(要为每个可覆盖字段维护继承标记,合并读取时要逐字段判断),以及读取性能(一次菜单查询要合并三层数据)。缓解手段是把合并结果缓存成门店的「有效菜单」,继承层或覆盖层变更时失效。取舍很划算:复杂度是一次性的,而分叉是持续恶化的。

    为什么保留「强制覆盖」这个能力,而不是绝对保护门店覆盖项。看起来「门店改过的就永远不动」最安全,但它会让总部失去管控能力——品牌统一调价、合规要求下架某个商品,这些必须能盖掉门店的设置。所以强制覆盖必须存在。关键是让它成为显式选择而不是默认行为:预演时把冲突项列出来,总部逐项或整体决定。这个设计的思路是「不禁止危险操作,但让它无法在不知情的情况下发生」——和审批引擎里「干预能力要有但需二次确认加留痕」是同一个思路。

    踩过的坑:总部下发整体覆盖,门店的本地调价被反复抹掉,最后形成对抗。门店店长每次总部下发后自己再改一遍,又在下一次下发时被抹掉,有个区域经理直接在群里问「能不能别下发了」。这个坑最值得讲的是它的表现形式——它不是一次崩溃,而是系统与使用者之间的信任被慢慢磨掉,最后表现为「大家都绕过系统」。修法是分层加字段级继承。教训是:当系统的行为让用户的工作被反复浪费时,用户不会来提 bug,他们会放弃这个功能,所以这类问题很难从工单里发现,要靠观察使用数据(比如门店的重复修改率)。

    踩过的坑二:总部选错下发范围,几百家门店几十个商品的价格被改,卖了两小时才发现。操作者以为只选了一个商品,实际选中了整个品类。问题在于下发前完全看不到影响面——点击下发之后就直接执行了。修法是加影响面预演教训是:批量操作必须在执行前把影响面量化并要求确认,「影响 3 家门店」和「影响 400 家门店」应该给操作者完全不同的心理提示,而系统有责任把这个数字摆在他面前。

    踩过的坑三:下发部分失败后只能整批重发,重发又覆盖了中间门店的操作。一次下发中有几十家门店失败,我们重新下发全部门店,而那期间有些成功的门店已经做了本地调整,重发把它们又盖了一遍。修法是批次化加逐店结果记录,失败的单独重试教训是:批量操作必须记录每个个体的结果,否则失败后唯一的补救手段就是全量重做,而全量重做在有并发写入的场景里会造成二次伤害。

    踩过的坑四:界面上看不出哪个值是继承的、哪个是覆盖的。店长改了价之后不知道自己已经脱离了总部对这个字段的管理,后来总部做了促销价下发,他没收到,还以为系统漏了。修法是界面上明确区分并提供「恢复继承」教训是:继承类的模型必须把继承关系可视化,用户看不见的机制等于不存在,他会用自己的模型去理解系统,然后困惑。

    没做的部分:没做门店覆盖的有效期(临时调价,到期自动恢复继承)。门店反馈过这个需求(做活动临时降价,活动完忘了改回来),但需要定时任务和覆盖项的生命周期管理,当时没做。也没做继承冲突的自动建议(「八成门店都覆盖了这个价格,建议总部调整模板价」),只做了数据展示。

    数字是怎么测的

    门店覆盖项是否被下发覆盖:确定性验证。构造场景——门店覆盖某商品价格,总部下发同一商品(改的是名称),检查门店价格保持、名称跟随更新这个用例必须精确到字段级,只验证「记录还在」是不够的。

    门店重复修改率:这是个很有意思的指标,用来度量前面那个「信任被磨掉」的问题——同一个门店对同一个字段在短期内反复修改的次数。改造前这个数很高(每次下发后店长都要改回来),改造后应该显著下降。它比任何自述都能证明对抗消失了。

    影响面预演的作用:预演后被操作者主动取消的下发次数。这个数字直接证明预演拦住了误操作——「有 N 次下发在看到影响面后被取消」比「我们加了预演功能」有力得多

    下发的成功率与重试:逐店成功数、失败数、失败原因分布、重试后成功数关键是要能报出「失败的门店可以单独重试」这个能力,而不只是一个成功率。

    菜单读取耗时:因为字段级继承要合并三层数据,必须主动报这一段的耗时,否则会被追问「三层合并不慢吗」。给出合并后缓存的命中率与未命中时的 P95,并说明缓存的失效策略。

    覆盖率分布:各字段被门店覆盖的比例。这个数据对业务有直接价值——如果某个价格字段八成门店都覆盖了,说明总部的模板定价不合理,该改的是模板而不是让门店各自改。

    不要报「菜单一致性 100%」。门店差异化是业务要求的功能而不是缺陷,追求一致性本身就是理解错了。正确表述是「门店覆盖项在下发中的保留由字段级用例保证、门店重复修改率的下降、预演拦下的误操作次数、下发失败可逐店重试」

    面试追问
    Q:连锁品牌总部要统一管菜单,门店又要有差异,怎么设计? A:三级继承链加字段级继承标记。三级是品牌模板 → 区域 → 门店——区域这一层是必要的,因为连锁的差异化通常按区域来(华东区统一一个价),只有总部和门店两层的话,区域调整要在几十家门店各操作一遍。关键在于继承标记必须是字段级而不是记录级:门店可能只覆盖价格,商品名称、图片、规格配置仍然跟随总部。记录级实现更简单(一个标志位表示这条是否脱离继承),但它把继承变成全有或全无——门店只想改个价格,结果整条记录脱离总部管理,之后总部更新图片、修正描述、调整规格,这家店全都收不到,日积月累门店菜单会和总部严重分叉。字段级的代价是实现复杂度和读取时要合并三层,缓解手段是把合并结果缓存成门店的「有效菜单」,继承层或覆盖层变更时失效。取舍很划算:复杂度是一次性的,而分叉是持续恶化的。
    Q:总部下发菜单,会不会把门店自己改的东西覆盖掉? A:第一版会,而且这个坑的表现形式很值得讲。当时下发是整体覆盖,门店自己调过的价格、自己下架的商品全被抹掉。店长发现后不再信任系统,改成每次总部下发后自己再改一遍,又在下一次下发时被抹掉,形成持续对抗,有个区域经理直接在群里问「能不能别下发了」。它不是一次崩溃,而是系统与使用者之间的信任被慢慢磨掉,最后表现为「大家都绕过系统」。教训是:当系统的行为让用户的工作被反复浪费时,用户不会来提 bug,他们会放弃这个功能——所以这类问题很难从工单里发现,我们后来是靠观察门店重复修改率(同一门店对同一字段短期内反复改的次数)才量化出来的。修法是把门店覆盖项独立分层、按字段标记继承或覆盖、下发只写继承字段这和我们在房源信息的「修正层」、组织同步的「手工层」是完全同一个模式:一份数据有多个写入方时,各给一层并定义优先级,因为「谁后写谁赢」在有自动化写入方参与时,人永远赢不了。
    Q:那总部想强制统一调价,怎么办? A:保留「强制覆盖」能力,但让它成为显式选择而不是默认行为。绝对保护门店覆盖项看起来最安全,但会让总部失去管控——品牌统一调价、合规要求下架某个商品,这些必须能盖掉门店设置。所以强制覆盖必须存在。具体做法是下发前的影响面预演:告诉总部「本次将影响多少门店、多少商品,其中多少存在覆盖冲突(门店覆盖过而这次要改同一字段)」,把冲突项列成明细,让总部逐项或整体决定「跳过」还是「强制覆盖」设计思路是「不禁止危险操作,但让它无法在不知情的情况下发生」——和审批引擎里「干预能力要有但需二次确认加留痕」是同一个思路。预演这一步是我们从一次真实事故里加的:总部想给一个商品调价,操作时选错了范围,把一整个品类都下发了,几百家门店几十个商品价格全变,卖了两小时才发现教训是批量操作必须在执行前把影响面量化并要求确认——「影响 3 家门店」和「影响 400 家门店」应该给操作者完全不同的心理提示。
    Q:给几百家门店下发,中间失败了怎么办? A:批次化,逐店记录结果,失败的单独重试。几百家门店的下发不可能是一个事务,中间必然有失败(某门店数据有问题、某次调用超时)。我们踩过的坑是第一版失败后不知道哪些成功哪些没成功,只能整批重发——而那期间已经成功的门店里有些做了本地调整,重发把它们又盖了一遍,造成二次伤害。修法是每次下发生成批次号、逐店记录成功失败与失败原因、失败的可单独重试、并支持按批次回滚教训是批量操作必须记录每个个体的结果,否则失败后唯一的补救手段就是全量重做,而全量重做在有并发写入的场景里一定会造成二次伤害。另外一个容易被忽略但很影响信任的点:界面上要能区分「这个值是继承来的」还是「本店覆盖的」,并提供「恢复继承」。我们踩过——店长改了价后不知道自己已脱离总部对该字段的管理,后来总部下发促销价他没收到,还以为系统漏了继承类的模型必须把继承关系可视化,用户看不见的机制等于不存在。

模块三:时段可售与估清一致性

  1. 时段可售与估清一致性(时段规则统一判定 + 估清秒级生效 + 下单时最终校准 + 营业与配送时间分离 + 时区与跨天处理)★★★
    简历这样写 菜单可售性的时段规则与估清一致性(营业时间与配送时间与商品时段三层规则统一判定 + 估清经缓存与事件双通道秒级生效 + 下单时以服务端为准最终校准 + 跨天时段与商家本地时区处理 + 可售性判定收敛为单一入口):商品可售性由营业时间、配送时段、商品自身售卖时段、估清状态共同决定,早期判定逻辑散落在列表与详情与下单三处导致三者结论不一致(列表可见、详情可买、下单被拒),因此把判定收敛为单一入口并在三处复用;估清采用缓存直写加变更事件双通道使商家点击估清后秒级对用户生效,同时下单时以服务端判定为最终依据(不信任客户端与列表缓存);时段规则支持跨天(夜宵 22:00 至次日 02:00)与商家本地时区,避免跨时区连锁品牌的时段错判。改造后「下单才被告知已售完」的比例由估清双通道与单一判定入口显著收敛,跨天时段的错判不再复现
    展开完整拆解
    为什么要这么设计

    「这个商品现在能不能买」看起来是一个布尔值,实际它由四个独立条件共同决定:门店在营业时间内、当前在可配送时段内、商品自身的售卖时段覆盖当前、商品没有被估清。任何一条不满足都不能买,而每一条的数据来源和变化频率都不同

    第一版最大的问题是判定逻辑散落在三个地方:列表页为了性能自己写了一套简化判断(只看营业时间),详情页写了一套完整点的,下单接口又写了一套。三套逻辑必然分叉,用户的体验是:列表里看得到、点进详情能加购、点下单被拒绝。这种「一步步往前走然后在最后一步被拦」是最挫伤转化的形态,而且客服解释不清。

    所以第一个设计很朴素但很关键:把可售性判定收敛为单一入口,列表、详情、下单三处调用同一份逻辑。性能问题用批量接口和缓存解决,而不是用「简化版逻辑」解决。这一条是这个模块里最容易被忽略却收益最大的改动。

    第二个是估清的实时性。估清(sold out)是餐饮特有的高频操作——厨房发现某个菜的原料用完了,店员在商家后台点一下估清。这个操作对实时性的要求极高:如果生效慢了几分钟,这期间下的单商家做不出来,只能打电话给用户道歉退款,而配送单可能已经派给骑手了

    第一版估清只写库,用户侧读的是列表缓存,缓存 TTL 是几分钟,也就是估清后最长几分钟内用户还能下单。修法是双通道:估清时直接写缓存(让用户侧立刻看到)加发变更事件(让依赖方如搜索索引同步更新)。两条通道互为兜底——缓存直写快但可能失败,事件可靠但稍慢。

    第三个,也是必须承认的一点:无论多快都会有窗口,所以下单时必须最终校准。用户可能在页面停留了十分钟,期间商品被估清。所以下单接口必须重新判定一次可售性,而且以服务端判定为准,不信任客户端也不信任列表缓存。这和旅游那边「下单前二次确认价格」是完全同一个思路——展示可以用缓存,成交必须校准

    第四个是时段规则本身的复杂度,这里有两个真实踩过的坑。

    跨天时段。夜宵菜单是「22:00 到次日 02:00」,如果简单地用「开始时间 ≤ 当前 ≤ 结束时间」判断,凌晨 1 点会判为不在时段内(因为 1 < 22)。夜宵商品在最该卖的时候卖不出去,商家投诉了才发现。

    商家本地时区。连锁品牌跨时区经营时,「早餐时段 6:00-10:00」是商家当地的 6 点还是服务器时区的 6 点?第一版用服务器时区,导致某些门店的早餐菜单在错误的时间上下架。正确做法是时段一律按商家所在时区判定。

    最后是营业时间与配送时段要分开。它们经常不同——到店可以营业到 22 点,但外卖只配送到 21 点(骑手要收工)。混成一个字段就无法表达这个业务事实。

    整体链路
    可售性 = 四个条件的合取(缺一不可) │ ├─ 门店在营业时间内 ├─ 当前在可配送时段内(与营业时间不同,要分开存) │ 到店营业到 22:00,但外卖只配送到 21:00 ├─ 商品自身售卖时段覆盖当前(早餐 / 午市 / 夜宵) ├─ 商品未被估清 │ └─ 四条数据来源和变化频率都不同 前三条是配置(低频),估清是运营动作(高频) 判定收敛为单一入口(这是收益最大的一改) │ ├─ 列表 / 详情 / 下单三处调用同一份判定逻辑 │ ├─ 第一版三处各写一套 → 必然分叉 │ 列表看得到 → 详情能加购 → 下单被拒 │ → 一步步往前走然后最后一步被拦 │ → 最挫伤转化,客服也解释不清 │ └─ 性能问题用批量接口 + 缓存解决 不要用「简化版逻辑」解决 → 简化版就是分叉的起点 估清的双通道(餐饮特有的高频高实时操作) │ ├─ 为什么实时性要求极高 │ 厨房原料用完 → 店员点估清 │ 生效慢几分钟 → 这期间的单做不出来 │ → 只能打电话道歉退款 │ → 而配送单可能已经派给骑手了 │ ├─ 通道一:直接写缓存,用户侧立刻可见 ├─ 通道二:发变更事件,搜索索引等依赖方同步 │ └─ 两条互为兜底 缓存直写快但可能失败 事件可靠但稍慢 下单时最终校准(承认窗口一定存在) │ ├─ 用户可能在页面停留十分钟,期间商品被估清 ├─ 下单接口重新判定可售性,以服务端为准 ├─ 不信任客户端,也不信任列表缓存 │ └─ 和「下单前二次确认价格」同一思路 展示可以用缓存,成交必须校准 时段规则的两个真实坑 │ ├─ 跨天时段:夜宵 22:00 至次日 02:00 │ 简单用「开始 <= 当前 <= 结束」会判错 │ 凌晨 1 点:1 < 22 → 判为不在时段内 │ → 夜宵在最该卖的时候卖不出去 │ 正确:结束时间小于开始时间即视为跨天,分段判定 │ └─ 商家本地时区:跨时区连锁 「早餐 6:00-10:00」是商家当地还是服务器时区 第一版用服务器时区 → 门店早餐在错误时间上下架 正确:时段一律按商家所在时区判定 拒绝时的提示(决定用户会不会流失) ├─ 区分原因:未营业 / 不在配送时段 / 不在售卖时段 / 已估清 ├─ 未营业 → 告知营业时间,可预约 ├─ 已估清 → 提示并推荐相似商品 └─ 统一回「该商品不可售」等于把用户赶走
    分步拆解
    1. 先把可售性拆成四个独立条件:营业时间、配送时段、商品售卖时段、估清状态。它是四条的合取,每条的数据来源和变化频率都不同,混在一起想就会漏。
    2. 营业时间与配送时段必须分开存。到店可以营业到 22 点但外卖只配送到 21 点(骑手收工),混成一个字段就无法表达这个业务事实
    3. 把可售性判定收敛为单一入口,列表、详情、下单三处复用。这是最容易被忽略却收益最大的改动
    4. 性能问题用批量接口加缓存解决,绝不用「简化版逻辑」解决。简化版就是分叉的起点——第一版列表页为了快只看营业时间,于是有了「列表看得到、下单被拒」。
    5. 估清要走双通道:直接写缓存 + 发变更事件。缓存直写让用户侧立刻可见,事件让搜索索引等依赖方同步,两条互为兜底
    6. 理解估清为什么实时性要求这么高。生效慢几分钟,这期间的单商家做不出来,只能打电话道歉退款,而配送单可能已经派给骑手了——损失是多方的。
    7. 下单接口必须重新判定可售性,以服务端为准。用户可能在页面停留十分钟,无论估清多快都会有窗口,所以成交前必须校准
    8. 不信任客户端提交的可售标记,也不信任列表缓存。和「下单前二次确认价格」是同一思路:展示可以用缓存,成交必须校准
    9. 跨天时段要单独处理:结束时间小于开始时间即视为跨天,分段判定。否则夜宵菜单在凌晨 1 点会被判为不在时段内(1 < 22),在最该卖的时候卖不出去。
    10. 时段一律按商家所在时区判定,不用服务器时区。跨时区连锁品牌里,「早餐 6:00-10:00」必须是商家当地的 6 点。
    11. 时段配置要支持按星期不同。周末的营业时间和工作日经常不一样,这是基本需求不是高级功能。
    12. 拒绝时要区分原因返回:未营业 / 不在配送时段 / 不在售卖时段 / 已估清。统一回「该商品不可售」等于把用户赶走。
    13. 不同原因给不同引导:未营业告知营业时间并支持预约,已估清则推荐相似商品。这一步直接影响流失。
    14. 估清要支持自动恢复。次日开店自动取消估清,否则店员忘了取消,那个商品第二天一整天都卖不出去——这是很常见的商家投诉。
    关键决策与取舍

    判定收敛为单一入口,代价是列表页要为可售性付出额外开销。第一版之所以在列表写简化逻辑,就是因为完整判定要读估清状态、时段配置、营业配置,一次列表几十个商品就是几十次判断。收敛之后的解法不是回退到简化逻辑,而是做批量判定接口加缓存:一次请求把这一屏商品的可售性批量算出来,结果短 TTL 缓存。取舍是列表接口的耗时略增,换来的是三处结论一致。我认为这个取舍毫无疑问值得,因为「列表看得到但买不了」的转化损失远大于几十毫秒。

    估清用「缓存直写 + 事件」双通道而不是只用一条。只用缓存直写:快,但缓存写失败就完全没生效,而且搜索索引不知道。只用事件:可靠,但要经过消息队列和消费者,延迟通常在秒级以上,而估清恰恰是最需要立刻生效的操作双通道的代价是要处理两条路径的一致性(缓存写成功但事件消费失败、或反过来),缓解手段是事件消费时以数据库为准做一次校准,让事件通道成为最终一致的兜底而缓存通道负责即时性。

    踩过的坑:三处判定逻辑分叉,用户「下单才被告知已售完」。列表为了性能只看营业时间,详情看得多一点,下单最完整。用户体验是一步步往前走,最后一步被拦,客服也解释不清为什么列表还显示着。修法是收敛为单一入口教训是:同一个业务判断在多处实现,必然分叉——不是「可能」而是「必然」,因为需求变更时不可能每次都记得改三处。凡是发现「同一个问题有多个版本的答案」,第一件事应该是合并而不是分别修。

    踩过的坑二:跨天时段判错,夜宵菜单在凌晨卖不出去。判定用的是「开始时间 ≤ 当前时间 ≤ 结束时间」,而夜宵是 22:00 到次日 02:00,凌晨 1 点时 1 < 22,直接判为不在时段。商家投诉「我明明配了夜宵为什么客人买不到」才发现。修法是结束时间小于开始时间即识别为跨天,拆成两段判定教训是:涉及时间区间的判断必须覆盖跨天用例,而跨天是个很容易在测试时想不到的场景(白天测试永远测不出来)。

    踩过的坑三:时段用服务器时区判定,跨时区门店的早餐菜单在错误时间上下架。连锁品牌的门店分布在不同时区,「早餐 6:00-10:00」被按服务器时区算,某些门店的早餐在当地凌晨就上了、上午就下了。修法是时段一律按商家所在时区判定,门店配置里必须有时区字段教训是:任何面向本地业务的时间判断都要问一句「谁的时间」——这条在旅游的「距入住 24 小时」、在结算的「账期日」上都是同一个问题。

    踩过的坑四:估清不会自动恢复,店员忘了取消导致商品整天卖不出去。晚上原料用完点了估清,第二天开店忘了取消,那个商品一整天零销量,商家到晚上核对营业额时才发现。修法是估清默认在次日开店时自动恢复,同时保留「长期下架」作为另一个独立操作教训是:临时性状态必须有自动恢复机制,依赖人去撤销一个临时状态,一定会有忘记的时候,而忘记的代价全部落在商家身上。

    没做的部分:没做基于库存的自动估清(对接商家的进销存,原料不足自动估清相关商品)。这需要商品与原料的配方关系,多数商家没有维护这个数据。也没做估清的预测提醒(「这个商品按当前售速一小时后会售完」),只做了手动估清。

    数字是怎么测的

    「下单才被告知不可售」的比例:这是这个模块最该报的指标。报下单被可售性校验拒绝的次数占下单请求的比例,改造前后对比。但要诚实说明它不可能归零——用户在页面停留期间商品被估清是真实场景,能消除的是「逻辑分叉造成的」那部分,不能消除「时间窗造成的」那部分。把两者区分开报,比给一个总的下降幅度更可信。

    估清生效时长:从商家点击估清到用户侧不可见的时长,分两条通道分别报(缓存直写路径、事件路径)。改造前是缓存 TTL(几分钟),改造后是缓存直写的耗时。这个对比很直观。

    三处判定的一致性:确定性验证——构造各种不可售场景(未营业、不在配送时段、不在售卖时段、已估清),检查列表、详情、下单三处的结论完全一致这类正确性用确定性用例覆盖,而且要每个条件各测一遍。

    跨天时段与时区:确定性验证,而且要写成固定用例——夜宵时段在凌晨 1 点判为可售、早餐时段按商家时区判定。这两个用例必须常驻回归,因为它们在正常开发时段测不出来。

    列表接口耗时:因为收敛判定后列表要付出额外开销,必须主动报,否则会被追问。给出批量判定的 P95 与缓存命中率,并说明「用批量加缓存而不是简化逻辑」这个选择。

    估清自动恢复:自动恢复的次数,以及「因未及时取消估清导致整天不可售」的投诉是否消失。前者证明机制在跑,后者是业务结果。

    不要报「可售性判定准确率 100%」。估清和时段配置由商家维护,商家配错了不是系统不准;而时间窗造成的偏差是物理存在的。正确表述是「判定收敛为单一入口后三处结论一致(由用例保证)、估清生效时长从缓存 TTL 降到直写耗时、跨天与时区由常驻回归用例覆盖」——说清机制与验证方式。

    面试追问
    Q:用户在列表里能看到商品,点下单却说卖完了,怎么解决? A:这个问题要分成两部分,一部分能彻底解决,一部分只能收敛。能解决的是「逻辑分叉」:我们第一版把可售性判定写在三处——列表为了性能只看营业时间、详情看得多一点、下单最完整,三套逻辑必然分叉,用户体验就是一步步往前走最后一步被拦,客服也解释不清。修法是把判定收敛为单一入口,列表详情下单三处复用性能问题用批量判定接口加缓存解决,绝不用「简化版逻辑」解决——简化版就是分叉的起点教训是:同一个业务判断在多处实现必然分叉,不是「可能」而是「必然」,因为需求变更时不可能每次都记得改三处。只能收敛不能解决的是「时间窗」:用户在页面停留十分钟,期间商品被估清,这是物理存在的。所以下单时必须重新判定并以服务端为准——这和旅游业务「下单前二次确认价格」是同一思路:展示可以用缓存,成交必须校准。报数字时我会把这两部分分开,而不是给一个总的下降幅度。
    Q:商家点了估清,怎么让用户侧立刻生效? A:双通道:直接写缓存 + 发变更事件。先说为什么实时性要求这么高——厨房原料用完店员点估清,如果生效慢几分钟,这期间下的单商家做不出来,只能打电话道歉退款,而配送单可能已经派给骑手了,损失是多方的。第一版估清只写库,用户侧读列表缓存,TTL 是几分钟,也就是估清后最长几分钟内用户还能下单双通道的分工是:缓存直写负责即时性(用户侧立刻可见),变更事件负责可靠性与扩散(搜索索引等依赖方同步更新)。为什么不只用一条:只用缓存直写,写失败就完全没生效而且搜索索引不知道;只用事件,要过消息队列和消费者,延迟通常秒级以上,而估清恰恰最需要立刻生效代价是要处理两条路径的一致性,缓解手段是事件消费时以数据库为准做一次校准,让事件成为最终一致的兜底。另外一个很实际的点:估清要能自动恢复——我们踩过店员忘了取消、商品整天零销量的坑,现在默认次日开店自动恢复,「长期下架」是另一个独立操作。
    Q:夜宵菜单是晚上 10 点到凌晨 2 点,这个时段怎么判断? A:这正是我们踩过的坑,而且它在白天测试永远测不出来。第一版判定用「开始时间 ≤ 当前时间 ≤ 结束时间」,夜宵是 22:00 到次日 02:00,凌晨 1 点时 1 < 22,直接判为不在时段内——夜宵商品在最该卖的时候卖不出去,是商家投诉「我明明配了夜宵为什么客人买不到」才发现的。修法是结束时间小于开始时间即识别为跨天,拆成 [22:00, 24:00) 和 [00:00, 02:00) 两段分别判定教训是涉及时间区间的判断必须覆盖跨天用例,而且这个用例要常驻回归,因为正常开发时段测不出来。还有一个同类但更隐蔽的坑是时区:连锁品牌的门店分布在不同时区,「早餐 6:00-10:00」被按服务器时区算,某些门店的早餐在当地凌晨就上了、上午就下了。修法是时段一律按商家所在时区判定,门店配置必须有时区字段教训是任何面向本地业务的时间判断都要先问一句「谁的时间」——这条在旅游的「距入住 24 小时」、在结算的「账期日」上是完全同一个问题。
    Q:商品不可售的时候,接口怎么返回? A:必须区分原因返回,不能统一回「该商品不可售」——那等于把用户赶走。可售性是四个条件的合取:门店在营业时间内、当前在可配送时段内、商品自身售卖时段覆盖当前、商品未被估清,四条的处理引导完全不同。未营业:告知营业时间,并支持预约(用户是有意愿的,只是来早了或来晚了);不在配送时段:告知配送时间,并提示可以到店;不在售卖时段:告知该商品的售卖时段(「夜宵 22 点开始供应」);已估清:明确说已售完,并推荐相似商品这一步直接影响流失——用户已经选到了具体商品,说明意愿很强,一个笼统的报错就把这个意愿浪费了。顺带说一个建模上的点营业时间和配送时段必须分开存,它们经常不同——到店可以营业到 22 点,但外卖只配送到 21 点(骑手要收工),混成一个字段就无法表达这个业务事实,而返回时也就没法给出准确的引导。

没有匹配的内容,换个关键词试试。

项目拆解 · 商家菜单与商品中台(后端)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据