搞定飞机托运行李价格计算,这3个实战项目细节救了我
搞定飞机托运行李价格计算,这3个实战项目细节救了我
很多兄弟都卡在同一个地方:语法背得滚瓜烂熟,LeetCode 算法题也能刷过,但真让你搭个完整的项目,脑子就一片空白。别急,这种“手残”不是你的问题,是缺乏实战项目的打磨。今天我们就拿一个看似简单但逻辑坑很多的业务场景——飞机托运行李价格计算,从零手撸一个完整的计算模块。
别小看这个需求,它涵盖了数据清洗、多规则判断、异常处理、单元测试以及性能优化,是一个极佳的入门级全栈练手题。我曾在掘金技术社区看到很多大厂面试真题里,类似的计费逻辑(如快递费、打车费)都是高频考点。咱们不整虚的,直接上代码,把坑填平。
项目目标与需求拆解
先搞清楚我们要干什么。航空公司托运行李的计费规则通常不是线性的,而是分段计价或者按重量档位收费。为了贴近真实场景,我们设定以下规则:免费额度:经济舱 20kg,公务舱 30kg,头等舱 40kg。
超重部分:超过免费额度后,每 1kg(不足 1kg 按 1kg 计)收取固定费用。不同舱位费率不同。
特殊件:如果行李包含易碎品或精密仪器,需额外支付保价费,且单件重量超过 30kg 时,费率翻倍。
输入校验:必须处理非法输入(如负数重量、非数字字符串)。我们的目标不是写一个 if-else 就完事,而是构建一个可扩展、可测试、高内聚的计算引擎。这能帮你从“写代码”进阶到“设计系统”。
目录结构设计
在动手写代码前,先规划好目录。好的结构能让后续维护变得轻松,这也是很多新手容易忽略的工程化细节。
luggage_calculator/
├── __init__.py
├── config.py # 配置管理:舱位费率、免费额度
├── exceptions.py # 自定义异常
├── core/
│ ├── __init__.py
│ ├── calculator.py # 核心计算逻辑
│ └── validator.py # 输入校验模块
├── tests/
│ ├── __init__.py
│ └── test_calculator.py # 单元测试
└── main.py # 入口文件/演示脚本这里采用了分层架构:config 存静态数据,core 存业务逻辑,tests 存测试用例。这种分离思想在任何中大型项目中都是通用的,养成习惯,受益终身。
核心代码实现
接下来是重头戏。我们将核心逻辑封装在 calculator.py 中,并引入策略模式的思想,虽然这里为了简洁用字典映射代替了复杂的策略类,但逻辑是一样的:将变化的规则与不变的逻辑分离。
1. 配置与异常定义
# config.py
CABIN_RATES = {economy: {free_limit: 20, rate_per_kg: 15.0, premium_rate_multiplier: 2.0},business: {free_limit: 30, rate_per_kg: 25.0, premium_rate_multiplier: 2.0},first: {free_limit: 40, rate_per_kg: 40.0, premium_rate_multiplier: 2.0}
}# exceptions.py
class LuggageError(Exception):行李计算基础异常passclass InvalidWeightError(LuggageError):无效重量异常passclass UnknownCabinError(LuggageError):未知舱位异常pass2. 输入校验模块
很多 Bug 都源于对输入的不信任。在掘金技术社区的许多分享中,防御性编程被反复提及。我们单独写一个 validator.py。
# core/validator.py
from exceptions import InvalidWeightError, UnknownCabinError
from config import CABIN_RATESdef validate_input(weight: float, cabin_type: str, is_premium: bool = False):校验输入参数的合法性:param weight: 行李重量 (kg):param cabin_type: 舱位类型:param is_premium: 是否特殊件# 检查重量是否为数字if not isinstance(weight, (int, float)):raise InvalidWeightError(重量必须是数字类型)# 检查重量是否为正数if weight = 0:raise InvalidWeightError(重量必须大于0)# 检查舱位是否支持if cabin_type not in CABIN_RATES:raise UnknownCabinError(f不支持的舱位类型: {cabin_type})# 检查特殊件标志if not isinstance(is_premium, bool):raise ValueError(is_premium 必须是布尔值)3. 核心计算逻辑
这是整个模块的心脏。注意看注释,每一步都在处理边界情况。
# core/calculator.py
from config import CABIN_RATES
from core.validator import validate_input
import mathclass LuggageCalculator:def __init__(self):self.config = CABIN_RATESdef calculate_fee(self, weight: float, cabin_type: str, is_premium: bool = False) - dict:计算托运行李费用:return: 包含详细费用的字典# 1. 前置校验validate_input(weight, cabin_type, is_premium)# 2. 获取当前舱位配置cabin_config = self.config[cabin_type]free_limit = cabin_config[free_limit]base_rate = cabin_config[rate_per_kg]# 3. 计算超重重量# 向上取整是关键:10.1kg 超重部分按 11kg 计算?不,是超出部分向上取整。# 假设免费额度内不收费,超出部分按 1kg 为单位向上取整excess_weight = max(0, weight - free_limit)billable_kg = math.ceil(excess_weight) # 向上取整# 4. 判断是否应用特殊费率# 规则:单件超过30kg 且 是特殊件,费率翻倍multiplier = 1.0if is_premium and weight 30:multiplier = cabin_config[premium_rate_multiplier]final_rate = base_rate * multiplier# 5. 计算总费用total_fee = billable_kg * final_rate# 6. 返回结构化数据,便于前端展示或日志记录return {total_fee: round(total_fee, 2),billable_kg: billable_kg,rate_per_kg: round(final_rate, 2),free_limit_used: min(weight, free_limit),cabin_type: cabin_type,is_premium_applied: bool(multiplier 1.0)}这段代码有几个亮点:math.ceil 的使用:模拟了现实世界中“不足 1kg 按 1kg 计”的规则。
结构化返回:没有直接返回一个浮点数,而是返回一个包含明细的字典。这在调试和对账时极其重要。
逻辑解耦:校验、配置获取、计算逻辑清晰分开。运行与测试
写代码不写测试,等于没写。我们用 pytest 来编写单元测试,确保我们的逻辑在各种边界情况下都是正确的。
# tests/test_calculator.py
import pytest
from core.calculator import LuggageCalculator
from exceptions import InvalidWeightError, UnknownCabinError@pytest.fixture
def calculator():return LuggageCalculator()def test_economy_within_limit(calculator):# 经济舱 15kg,未超重,费用应为 0result = calculator.calculate_fee(15, economy)assert result[total_fee] == 0assert result[billable_kg] == 0def test_economy_over_limit(calculator):# 经济舱 25kg,超重 5kg,费率 15元/kgresult = calculator.calculate_fee(25, economy)assert result[total_fee] == 75.0assert result[billable_kg] == 5def test_premium_heavy_item(calculator):# 公务舱 35kg,特殊件。免费30kg,超重5kg。# 基础费率25元,因为30kg且特殊件,费率翻倍为50元。result = calculator.calculate_fee(35, business, is_premium=True)assert result[total_fee] == 250.0assert result[rate_per_kg] == 50.0def test_invalid_weight(calculator):with pytest.raises(InvalidWeightError):calculator.calculate_fee(-10, economy)def test_unknown_cabin(calculator):with pytest.raises(UnknownCabinError):calculator.calculate_fee(10, vip)运行测试命令:
pytest tests/ -v如果看到所有测试通过,恭喜你,核心逻辑已经跑通。这个环节的价值在于,当你后续修改费率逻辑时,测试会立刻告诉你哪里改错了,而不是等上线后用户投诉才发现问题。
优化扩展与避坑指南
代码跑通了,但这只是开始。在实际生产环境中,还有几个坑需要你提前踩平:并发与线程安全:
如果这是一个 Web 服务的一部分,LuggageCalculator 实例如果被多个线程共享,目前的代码是安全的,因为它没有修改实例状态(无状态计算)。但如果你引入了缓存(比如缓存计算结果以提升性能),就必须加锁或使用线程本地存储。费率动态化:
目前费率是硬编码在 config.py 里的。在真实业务中,费率可能会随季节、航线变化。建议将配置从代码中剥离,放入数据库或 Redis,并设计一个配置加载器,支持热更新。日志记录:
在 calculate_fee 方法中加入 logging 模块,记录每次计算的输入、输出和耗时。当用户投诉“为什么我要付这么多钱”时,你可以通过日志还原当时的计算过程,这是排查问题的救命稻草。国际化考虑:
货币单位、重量单位(磅 vs 公斤)都需要抽象出来。不要硬编码 15.0,而应该定义一个 Currency 和 Unit 枚举,方便后续扩展多币种和多单位支持。API 封装:
如果用 Flask 或 FastAPI 封装,记得对返回的 JSON 数据进行序列化优化,避免暴露内部配置细节。小结
通过这个飞机托运行李价格计算的实战项目,我们不仅解决了一个具体的业务问题,更锻炼了工程化的思维。从目录规划、异常处理、单元测试到性能优化,每一步都是工业级开发的缩影。
很多开发者觉得业务逻辑简单,不屑于深入,但正是这些看似简单的 CRUD 和计算逻辑,构成了系统的基石。把基础打牢,比盲目追逐新技术框架更重要。
在掘金技术社区的讨论区,经常有朋友问:在涉及金额计算时,是用 float 还是 Decimal?这是一个经典争议。我目前的做法是在前端展示时用 float,在后端核心计费时用 Python 的 decimal 模块,以避免二进制浮点数精度丢失问题。
你公司项目里是怎么处理这类计费逻辑的?有没有遇到过精度丢失或者规则冲突的坑?欢迎在评论区分享你的经验,我们一起避坑。