
在游戏逻辑、运营后台甚至物联网设备状态判断里经常会出现一类看起来不起眼、实际很值得系统化设计的需求当系统检测到某种物品组合时需要输出类似“检测到双果冻双红宝石”的提示信息。这里的“果冻”和“红宝石”可以替换成任意道具、资源、订单状态或告警信号“双”表示数量达到 2 个或以上。这类需求写起来不难但很容易因为硬编码、条件维护混乱、缺少边界测试而出现问题。本文就用一个完整的 Python 实战项目来拆解这类“组合物品检测系统”的设计思路覆盖数据模型、规则配置、匹配算法、日志输出、单元测试和常见排错。无论你是做游戏 Mod、后台管理系统的库存预警还是想学习规则引擎的落地写法这篇文章都能直接参考。1. 背景与需求拆解1.1 什么是组合物品检测组合物品检测简单说就是“根据一组物品的数量关系判断是否满足某个预设条件”。它不是一个新概念日常开发里到处都是游戏背包中收集到 2 个果冻和 2 个红宝石后提示可以合成稀有道具电商运营后台订单同时包含某 3 类赠品且数量达标时触发自动发券运维监控中当同一台服务器同时出现 CPU 高负载和内存高占用时发出组合告警自动化测试里当页面同时出现两个指定元素时执行附加断言。在这些场景中“检测到双果冻双红宝石”只是最终展示出来的一句话。这句话背后实际上是“数量统计 阈值判断 条件匹配 结果输出”四个步骤的组合。1.2 为什么需要单独设计而不是直接 if 判断有读者可能会说这有什么好设计的两个 if 不就完了if inventory.count(jelly) 2 and inventory.count(ruby) 2: print(检测到双果冻双红宝石)确实单看这一个需求上面的代码完全够用。但实际项目中物品组合规则会不断变化明天可能从“双果冻双红宝石”变成“三果冻一蓝宝石”后天可能要求“果冻和红宝石数量都在 2 到 5 之间”再后来可能要求“果冻、红宝石、药水同时存在且果冻数量大于红宝石数量”。如果用 if 硬编码每变更一次业务规则就要修改主逻辑代码甚至需要重新发版。而把规则抽取成可配置的数据结构让代码只负责“执行规则”就能把变化控制在配置层。1.3 本文适合哪些读者本文适合以下读者对 Python 面向对象设计和数据模型有初步了解想学习如何组织小型业务系统需要在游戏、后台或测试框架中实现“多条件检测”功能想了解 YAML 配置驱动的代码结构想学习如何为工具类代码编写单元测试。学完本文后你将能够使用 Python 数据类定义物品和库存模型将“检测到双果冻双红宝石”这类规则抽象为可配置的规则对象实现一个可复用的规则匹配器支持最少数量、最大数量等约束通过 YAML 文件管理规则避免硬编码使用 pytest 编写关键测试用例掌握此类系统在真实工程中的最佳实践。2. 环境准备与项目结构2.1 运行环境本文示例代码基于 Python 3.10 及以上版本主要用到标准库和少量第三方库。版本差异不大Python 3.8 以上通常也能运行但部分类型注解写法需要微调。建议你提前安装好 Python并创建一个独立虚拟环境。推荐依赖如下PyYAML6.0 pytest7.0其中 PyYAML 用于解析规则配置文件pytest 用于运行测试。如果你只跑示例主流程不跑测试甚至不需要安装 pytest。创建虚拟环境并安装依赖python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate pip install PyYAML pytest2.2 项目目录结构为了让代码结构更清晰我建议按下面的目录组织项目rule-detector/ ├── detector/ │ ├── __init__.py │ ├── models.py # 物品、库存数据模型 │ ├── rules.py # 规则定义与匹配器 │ ├── logger.py # 日志工具 │ └── rule_loader.py # YAML 规则加载 ├── configs/ │ └── rules.yaml # 规则配置文件 ├── tests/ │ └── test_rules.py # 单元测试 └── main.py # 示例入口这样划分的好处是每个模块职责单一models.py只描述“物品”和“库存”的数据结构rules.py只负责规则的表示与匹配rule_loader.py处理配置文件的读取main.py负责把数据、规则、输出组装起来。后面在真实项目中你可以把某一层替换成数据库或消息队列不会影响其他模块。3. 数据模型设计3.1 定义物品对象组合检测的第一步是知道“有哪些物品、各有多少”。在 Python 中我们通常使用dataclass来定义这类纯数据结构。创建detector/models.pyfrom dataclasses import dataclass, field from typing import Dict, List dataclass class Item: 物品对象 code: str # 物品编码如 jelly / ruby name: str # 物品展示名称如 果冻 / 红宝石 category: str common # 物品分类预留扩展字段 extra: Dict[str, object] field(default_factorydict) # 预留扩展属性 dataclass class Inventory: 库存对象保存一组物品 items: List[Item] field(default_factorylist) def add_item(self, item: Item) - None: self.items.append(item) def count_by_code(self) - Dict[str, int]: 统计每种物品编码的数量。 例如输入两个 jelly 两个 ruby输出 {jelly: 2, ruby: 2} counter: Dict[str, int] {} for item in self.items: counter[item.code] counter.get(item.code, 0) 1 return counter这里有两个设计点值得说明。第一物品使用code而不是直接用中文名“果冻”来标识。这是因为在代码和配置中英文字符串更不容易出现编码和匹配问题也便于以后接入数据库。展示给用户的消息里才使用中文名。第二count_by_code方法把库存对象变成“编码 - 数量”的统计字典。规则匹配器只需要接收这个字典不需要感知库存里的具体物品对象。这样一来规则系统与具体业务数据解耦后续无论数据是来自列表、数据库还是消息队列都能复用同一套匹配逻辑。3.2 手工构造测试数据为了验证数据模型可以先在交互式环境里跑一下python -c from detector.models import Item, Inventory inventory Inventory() inventory.add_item(Item(codejelly, name果冻)) inventory.add_item(Item(codejelly, name果冻)) inventory.add_item(Item(coderuby, name红宝石)) inventory.add_item(Item(coderuby, name红宝石)) print(inventory.count_by_code()) 预期输出{jelly: 2, ruby: 2}看到这个统计结果就意味着“检测到双果冻双红宝石”的数量基础已经准备好了。4. 规则定义与匹配逻辑4.1 规则对象一条规则需要包含以下要素规则 ID唯一标识方便日志和追踪规则名称给人看的描述条件列表每个条件指定物品编码和数量范围消息模板条件满足时输出的文字日志级别info、warning 等方便与日志系统对接。在detector/rules.py中定义from dataclasses import dataclass, field from typing import Dict, List, Optional dataclass class RuleCondition: 单条条件某个物品的数量范围 item_code: str min_count: int 1 max_count: Optional[int] None dataclass class Rule: 规则对象 rule_id: str name: str conditions: List[RuleCondition] field(default_factorylist) message: str level: str info detail: Dict[str, int] field(default_factorydict) def check(self, counts: Dict[str, int]) - bool: 检查传入的数量统计是否满足该规则的所有条件。 self.detail.clear() for condition in self.conditions: actual counts.get(condition.item_code, 0) self.detail[condition.item_code] actual if actual condition.min_count: return False if condition.max_count is not None and actual condition.max_count: return False return Truecheck方法可能是一段会真实复用到各种项目里的核心逻辑。它做的事情很直白从统计字典中取出当前物品的实际数量缺失时按 0 处理记录每个条件的实际值到detail方便后续输出排查如果实际数量小于min_count不满足如果配置了max_count且实际数量超过上限不满足所有条件都通过返回 True。这里之所以把缺失物品按 0 处理是因为规则匹配器不应该因为某个物品没出现就抛异常而是应该用“数量为 0”的自然逻辑判断为不满足。这种容错处理对生产环境非常重要。4.2 规则匹配器规则不是只有一条实际系统中往往有几十上百条规则。所以还需要一个匹配器负责遍历所有规则并返回所有满足条件的规则。继续在detector/rules.py中添加dataclass class RuleMatcher: 规则匹配器遍历规则列表返回所有命中的规则 rules: List[Rule] field(default_factorylist) def match(self, counts: Dict[str, int]) - List[Rule]: matched: List[Rule] [] for rule in self.rules: if rule.check(counts): matched.append(rule) return matchedmatch方法返回的是规则对象列表而不是直接打印提示。这样设计的好处是调用方可以决定后续动作输出日志、发送通知、写入数据库甚至触发游戏合成逻辑都由上层决定。规则系统本身只负责“判断是否命中”。4.3 用代码手工构造规则在日常开发和调试阶段手工构造规则是最快的验证方式。from detector.rules import Rule, RuleCondition, RuleMatcher rules [ Rule( rule_idjelly_x2_ruby_x2, name双果冻双红宝石, conditions[ RuleCondition(item_codejelly, min_count2), RuleCondition(item_coderuby, min_count2), ], message检测到双果冻双红宝石, levelinfo, ) ] matcher RuleMatcher(rules) counts {jelly: 2, ruby: 2, potion: 1} matched matcher.match(counts) for rule in matched: print(rule.message)运行后输出检测到双果冻双红宝石如果counts缺少红宝石这一个物品比如只有 2 个果冻则条件不满足matched为空列表什么都不会输出。这符合业务预期双果冻只是部分条件不能单独触发。5. 使用 YAML 管理规则配置5.1 为什么选择 YAML当规则数量变多以后把规则写在 Python 代码里会带来两个问题非开发人员无法维护修改规则需要重新发版。YAML 配置文件的优势在于可读性好、支持注释、易于版本对比。运营人员会写文档就能维护规则结构。我们需要写一个加载器把 YAML 内容转换成Rule对象。5.2 创建 rules.yaml在项目根目录创建configs/rules.yamlrules: - rule_id: jelly_x2_ruby_x2 name: 双果冻双红宝石 message: 检测到双果冻双红宝石 level: info conditions: - item: jelly min_count: 2 - item: ruby min_count: 2 - rule_id: jelly_x3_potion_x1 name: 三果冻一药水 message: 检测到三果冻一药水可合成能量果冻 level: info conditions: - item: jelly min_count: 3 - item: potion min_count: 1注意YAML 中的中文不需要特殊处理只要文件本身以 UTF-8 编码保存即可。但加载文件时一定要显式指定encodingutf-8否则 Windows 环境下可能因默认编码问题出现乱码。5.3 编写规则加载器创建detector/rule_loader.pyfrom typing import List import yaml from detector.rules import Rule, RuleCondition def load_rules_from_yaml(path: str) - List[Rule]: 从 YAML 文件加载规则列表。 with open(path, r, encodingutf-8) as f: data yaml.safe_load(f) rules: List[Rule] [] for raw_rule in data.get(rules, []): conditions [ RuleCondition( item_codecond[item], min_countcond.get(min_count, 1), max_countcond.get(max_count), ) for cond in raw_rule.get(conditions, []) ] rule Rule( rule_idraw_rule[rule_id], nameraw_rule[name], conditionsconditions, messageraw_rule.get(message, raw_rule[name]), levelraw_rule.get(level, info), ) rules.append(rule) return rules这段代码有几个细节需要注意。第一cond.get(min_count, 1)表示如果某个条件没有写min_count默认按 1 处理。这样配置可以更简洁。比如只要求“红宝石存在”只需要写item: ruby不需要写min_count: 1。第二raw_rule.get(message, raw_rule[name])保证即使忘记写message也会把规则名称作为提示信息输出不会出现空消息。第三所有读取配置的地方都做了容错但关键字段如rule_id、name如果缺失仍然会直接抛 KeyError。这是故意的因为规则 ID 和名称缺失属于配置错误应该尽早暴露而不是默认静默处理。5.4 主入口 main.py现在可以写一个完整的入口把数据、规则配置、匹配、日志输出串起来。创建main.pyfrom detector.models import Item, Inventory from detector.rules import RuleMatcher from detector.rule_loader import load_rules_from_yaml from detector.logger import get_logger logger get_logger(main) def build_test_inventory() - Inventory: inventory Inventory() inventory.add_item(Item(codejelly, name果冻)) inventory.add_item(Item(codejelly, name果冻)) inventory.add_item(Item(coderuby, name红宝石)) inventory.add_item(Item(coderuby, name红宝石)) inventory.add_item(Item(codepotion, name药水)) return inventory def main(): # 1. 准备库存数据 inventory build_test_inventory() counts inventory.count_by_code() logger.info(当前物品统计: %s, counts) # 2. 加载规则 rules load_rules_from_yaml(configs/rules.yaml) logger.info(已加载规则数量: %d, len(rules)) # 3. 执行匹配 matcher RuleMatcher(rules) matched matcher.match(counts) # 4. 输出结果 if matched: for rule in matched: logger.log(getattr(__import__(logging), rule.level.upper()), 命中规则[%s] %s详情: %s, rule.rule_id, rule.message, rule.detail) else: logger.info(未命中任何规则) if __name__ __main__: main()为了方便输出detector/logger.py里实现一个简单的日志工具import logging import sys def get_logger(name: str) - logging.Logger: logger logging.getLogger(name) if not logger.handlers: handler logging.StreamHandler(sys.stdout) fmt %(asctime)s [%(levelname)s] %(name)s: %(message)s handler.setFormatter(logging.Formatter(fmt)) logger.addHandler(handler) logger.setLevel(logging.INFO) # 避免日志重复输出 logger.propagate False return logger运行python main.py预期输出类似2025-02-01 12:00:00 [INFO] main: 当前物品统计: {jelly: 2, ruby: 2, potion: 1} 2025-02-01 12:00:00 [INFO] main: 已加载规则数量: 2 2025-02-01 12:00:00 [INFO] main: 命中规则[jelly_x2_ruby_x2] 检测到双果冻双红宝石详情: {jelly: 2, ruby: 2}到这里“检测到双果冻双红宝石”这个需求已经从硬编码 if 判断升级成了一个可配置、可扩展的规则检测系统。6. 编写单元测试验证边界情况6.1 为什么必须写测试写少量核心逻辑的时候大脑还能处理各种情况。但规则系统随着时间推移条件组合会越来越多。如果不写测试很可能在一次配置调整后旧有的“双果冻双红宝石”规则被不小心改坏而你还不知道。使用 pytest 编写单元测试能够保证规则正常命中数量不足时不误报最大数量限制生效物品完全缺失时不异常YAML 加载解析正确。6.2 测试用例创建tests/test_rules.pyimport pytest from detector.rules import Rule, RuleCondition, RuleMatcher from detector.rule_loader import load_rules_from_yaml def build_rule(): return Rule( rule_idjelly_x2_ruby_x2, name双果冻双红宝石, conditions[ RuleCondition(item_codejelly, min_count2), RuleCondition(item_coderuby, min_count2), ], message检测到双果冻双红宝石, ) def test_match_when_condition_met(): rule build_rule() matcher RuleMatcher([rule]) result matcher.match({jelly: 2, ruby: 2}) assert len(result) 1 assert result[0].rule_id jelly_x2_ruby_x2 def test_not_match_when_one_item_missing(): rule build_rule() matcher RuleMatcher([rule]) result matcher.match({jelly: 2}) assert result [] def test_not_match_when_count_less_than_min(): rule build_rule() matcher RuleMatcher([rule]) result matcher.match({jelly: 1, ruby: 2}) assert result [] def test_max_count_limit(): rule Rule( rule_idjelly_between_2_and_5, name果冻数量在2到5之间, conditions[ RuleCondition(item_codejelly, min_count2, max_count5), ], message果冻数量符合区间, ) matcher RuleMatcher([rule]) assert len(matcher.match({jelly: 2})) 1 assert len(matcher.match({jelly: 5})) 1 assert matcher.match({jelly: 6}) [] def test_missing_item_treated_as_zero(): rule build_rule() matcher RuleMatcher([rule]) result matcher.match({ruby: 3}) assert result [] def test_load_yaml_rules(tmp_path): yaml_content rules: - rule_id: demo_rule name: 示例规则 message: 检测到示例规则 conditions: - item: jelly min_count: 2 config_file tmp_path / rules.yaml config_file.write_text(yaml_content, encodingutf-8) rules load_rules_from_yaml(str(config_file)) assert len(rules) 1 assert rules[0].rule_id demo_rule assert rules[0].conditions[0].min_count 2运行测试pytest tests/ -v正常情况下所有用例都会通过。如果将来有人修改了Rule.check的逻辑导致数量判断出错这些测试会第一时间暴露问题。7. 进阶扩展从字符串解析到更复杂的规则7.1 解析“检测到双果冻双红宝石”这类句式在某些场景下规则不是由 YAML 配置而来而是来自一段外部文本比如聊天机器人输入、运营活动描述、工单自动解析。这时需要从“检测到双果冻双红宝石”中提取出“果冻数量为 2、红宝石数量为 2”这种结构化信息。可以写一个简单的解析函数支持“双”“三”和物品名称的映射CN_NUM_MAP { 双: 2, 两: 2, 三: 3, 四: 4, 五: 5, } def parse_detect_message(text: str) - dict: 从 检测到双果冻双红宝石 中解析出 {jelly: 2, ruby: 2}。 这里假设物品名称通过 code_name_map 映射为编码。 code_name_map { 果冻: jelly, 红宝石: ruby, 药水: potion, } result {} i 0 while i len(text): # 跳过固定的“检测到”前缀或其他不相关内容 if text[i] in CN_NUM_MAP: count CN_NUM_MAP[text[i]] # 紧跟在数量词后面的就是物品名 item_name text[i 1] code code_name_map.get(item_name) if code: result[code] result.get(code, 0) count i 2 else: i 1 return result if __name__ __main__: print(parse_detect_message(检测到双果冻双红宝石))输出{jelly: 2, ruby: 2}这种解析能力在实际项目中很有用。比如运营人员提交一句“检测到三果冻一药水”系统可以自动转成对应的规则条件减少人工配置 YAML 的成本。但这只是一个简化示例真实场景还需要处理“三个果冻”“2 个红宝石”等不同表达需要接入分词或正则表达式。7.2 支持条件组模拟 OR 逻辑目前规则里的条件是 AND 关系所有条件都满足才算命中。但有些业务需要 OR 关系比如“检测到双果冻或者检测到双红宝石”同样触发提示。可以把条件分成多个条件组每组内部是 AND组与组之间是 OR。改造后的匹配器代码思路如下dataclass class RuleGroup: 条件组组内所有条件都满足才算本组命中 conditions: List[RuleCondition] field(default_factorylist) def check(self, counts: Dict[str, int]) - bool: for condition in self.conditions: actual counts.get(condition.item_code, 0) if actual condition.min_count: return False if condition.max_count is not None and actual condition.max_count: return False return True然后Rule中不再只保存conditions而是保存groupsdataclass class Rule: rule_id: str name: str groups: List[RuleGroup] field(default_factorylist) message: str level: str info def check(self, counts: Dict[str, int]) - bool: # OR 关系任意一个条件组命中规则就命中 for group in self.groups: if group.check(counts): return True return False这样做之后规则的表达能力会强很多适合那种“达到任意一个里程碑就算完成”的业务。不过需要提醒一点配置复杂度也会上升YAML 里要引入groups层级在维护时务必写清楚注释。7.3 命中后执行动作目前Rule只有message满足条件后只是打日志。在实际系统中命中规则后通常要执行动作比如给玩家发放合成奖励发送告警邮件更新数据库状态调用外部 Webhook。推荐的做法是引入一个“动作”概念在规则对象中增加动作类型和参数- rule_id: jelly_x2_ruby_x2 name: 双果冻双红宝石 message: 检测到双果冻双红宝石 actions: - type: webhook params: url: http://example.com/callback - type: log params: level: info然后在命中规则后遍历actions根据type分发到对应的执行器。类似的设计在很多规则引擎中都有本文不做完整实现只保留这个方向作为扩展思路。8. 常见问题与排查思路在实际开发中最容易出的问题往往不是匹配逻辑本身而是数据、配置、编码等细节。下面整理了一份排查清单。问题现象常见原因解决思路配置了规则但没有提示物品编码和规则中的item不一致打印counts检查实际编码规则被重复命中同一个rule_id在配置文件中出现多次加载时校验规则 ID 唯一性数量为 3 时也提示“双XX”min_count2表示“至少 2 个”会包含 3 个如需“恰好 2 个”增加max_count2YAML 中文显示乱码文件编码不是 UTF-8或者读取时未指定编码保存为 UTF-8读取时encodingutf-8修改 YAML 后不生效程序启动时载入一次规则后续没有重新加载开发环境配置热加载或重启进程物品缺失时抛 KeyError代码直接使用counts[jelly]使用counts.get(jelly, 0)规则配置错误没有提示加载器对缺失rule_id没有校验加载后执行校验函数缺失关键字段直接抛异常如果不确定是哪一类问题最有效的排查方式是打印关键信息logger.info(当前物品统计: %s, counts) for rule in rules: logger.info(规则[%s] 检查结果: %s, rule.rule_id, rule.check(counts))这样可以快速判断是“数据不对”还是“规则没加载”。9. 最佳实践与工程建议9.1 用编码代替显示名称所有规则、代码、接口传递都应该使用稳定的物品编码比如jelly、ruby。显示名称只是给人看的不能作为程序判断的 key。否则一旦功能改为“红宝石”所有配置都失效。另一方面编码要避免歧义。ruby_red和red_ruby这种命名容易让人困惑。建议维护一份编码规范表统一使用类型_名称的格式例如gem_ruby、food_jelly。9.2 规则尽量做成配置而不是硬编码前文已经反复强调这个系统的核心价值就是规则可配置。建议把configs/rules.yaml纳入版本管理变更规则时走代码评审流程。这样任何一次规则修改都有记录出问题时可以快速回滚。如果规则规模很大超过几千条建议把规则存入数据库并增加生效时间、版本号等字段。加载时可以增加缓存规则变更通过发布事件刷新内存中的规则列表。9.3 日志中要带规则 ID 和命中详情只输出“检测到双果冻双红宝石”虽然在业务上完整但对于排查问题还不够。更规范的日志应该包含规则 ID规则名称每个条件的要求值和实际值。例如2025-02-01 12:00:00 [INFO] main: 命中规则[jelly_x2_ruby_x2] 检测到双果冻双红宝石条件详情: {jelly: 2/2, ruby: 2/2}这样当用户反馈“我没合成成功但系统提示合成”时你能从日志里看出是哪个规则在哪个时间点命中的更快定位问题。9.4 做好边界测试组合检测类逻辑最容易出错的边界包括目标物品数量等于阈值下限目标物品数量等于阈值上限目标物品完全缺失目标物品数量为负数虽然业务上不应该出现但防御性处理更好并发场景下库存数量变化导致的一致性问题。前四类可以通过单元测试覆盖最后一类需要结合具体业务考虑是否在匹配过程中加锁或使用快照。9.5 保持匹配器的无状态设计RuleMatcher和Rule都应该尽量保持无状态也就是说同一个输入counts应该始终产生同一个匹配结果。不要在执行匹配过程中依赖全局变量或外部环境。这样做的好处是方便测试方便并发调用方便未来做分布式部署。即使需要在规则内保存命中详情也应该在check方法内部维护而不是把counts直接挂到规则对象的生命周期里。9.6 不要把所有业务都塞进规则系统规则系统能解决的问题是“当数量满足条件时做什么提示或动作”但它不应该代替业务主流程。比如“合成系统”除了检测物品数量还要扣除材料、判断合成成功率、处理道具生成结果。把这些逻辑全部塞进规则配置会让配置变得极其复杂且容易失控。建议的边界是规则系统负责“判断是否满足条件”和“触发某类动作”而复杂的业务操作仍由独立的 Service 完成。规则配置里只放动作类型和必要参数比如action: craft_energy_jelly具体怎么合成回到代码里实现。这样既保留了配置灵活性又不会让规则文件变得像编程语言一样难维护。9.7 性能优化思路当规则数量非常多时逐条遍历匹配可能成为性能瓶颈。这里介绍一个简单的分桶思路在加载规则时统计每条规则涉及的所有item_code然后建立“物品编码 - 规则列表”的索引。class IndexedRuleMatcher: def __init__(self, rules: List[Rule]): self.rules rules self.index: Dict[str, List[Rule]] {} for rule in rules: for condition in rule.conditions: self.index.setdefault(condition.item_code, []).append(rule) def match(self, counts: Dict[str, int]) - List[Rule]: matched_rules set() for item_code, count in counts.items(): if count 0: continue for rule in self.index.get(item_code, []): if rule.check(counts): matched_rules.add(rule) return list(matched_rules)这个优化利用了“如果某个物品在库存中不存在那么所有需要该物品的规则都不可能命中”这一点。不过需要注意当规则条件包含max_count时索引逻辑要更谨慎因为“物品数量为 5”也可能导致“数量在 2 到 5 之间”的规则命中。如果业务量不大几千条规则逐条遍历完全没问题不需要提前优化。建议先写简单可靠版本再通过压测决定是否引入索引。10. 总结从“检测到双果冻双红宝石”这样一句看似简单的提示出发本文完整拆解了组合物品检测系统的设计与实现。核心要点包括用数据类Item和Inventory管理物品及库存数量将“双果冻双红宝石”抽象成由RuleCondition组成的Rule对象用RuleMatcher统一遍历规则并返回命中结果用 YAML 管理规则配置让业务变化不需要修改代码用 pytest 覆盖基础匹配、最大数量限制、YAML 加载等关键场景用日志记录命中规则 ID 和条件详情方便线上排查问题。如果你正在开发一个背包统计、成就检测、库存预警或自动化判断系统建议先把物品编码规范定好再梳理清楚规则条件的表达能力最后辅以单元测试。像“双果冻双红宝石”这种看似鸡毛蒜皮的需求一旦做成了可配置、可测试、可观测的小系统后续新增任何组合规则都会变得非常轻松。顺手把项目里的规则配置多写几条把测试补齐再切换到main.py里替换成真实数据源你就能在现有业务中直接复用这套方案了。