- 后端
- 电商
【免费下载链接】django-oscar
Domain-driven e-commerce for Django
Oscar 2.0.1 是 django-oscar 2.0 系列的首个 bugfix 版本,发布于 2019-08-09,集中修复了三个直接影响生产使用的问题:fork 化仪表盘应用(dashboard app)下的导航权限校验、商品列表查询中选项(Option)统计带来的数据库性能开销,以及仪表盘优惠列表页开始/结束日期的显示缺陷。读完本文,你将掌握这三个修复点的底层实现原理、对应的源码位置与测试用例,并能据此在自建 Oscar 项目中验证与跟进修复。
版本背景:2.0 系列的第一个补丁版
Oscar 2.0 是对 django-oscar 的一次大版本升级,而 2.0.1 作为紧随其后的补丁版,不引入新功能,只做定向修复。官方发布说明(docs/source/releases/v2.0.1.rst)将全部改动归入 "Bug fixes" 一节,共三项:
- 修复
oscar.dashboard.nav.default_access_fn,使其能正确检查 fork 化仪表盘应用上配置的权限; - 重构
ProductQuerySet.base_queryset(),用布尔注解取代计数注解,消除检查产品选项时的昂贵查询; - 修复仪表盘优惠(offer)列表模板中开始/结束日期的显示。
对于升级到 2.0 系列的用户,这三项修复分别对应权限安全、列表性能、后台可用性三个维度,值得逐一理解其实现。
修复一:fork 化仪表盘应用的导航权限校验
问题本质:权限从哪个 AppConfig 读取
Oscar 的仪表盘导航由 src/oscar/apps/dashboard/nav.py 中的default_access_fn驱动。该函数接收用户与 URL 名称,返回该用户是否有权访问对应导航项:
def default_access_fn(user, url_name, url_args=None, url_kwargs=None): """...""" if url_name is None: # it's a heading return True url = reverse(url_name, args=url_args, kwargs=url_kwargs) url_match = resolve(url) url_name = url_match.url_name app_config_instance = _dashboard_url_names_to_config()[url_name] permissions = app_config_instance.get_permissions(url_name) return check_permissions(user, permissions)其核心逻辑分三步:先用reverse+resolve将 URL 名称解析为实际路由,再通过_dashboard_url_names_to_config()找到该路由所属的仪表盘AppConfig,最后读取该 config 上配置的权限并交给check_permissions(来自 src/oscar/views/decorators.py)判定。
2.0.1 修复的问题恰恰出在第二步:当开发者按 Oscar 的 fork 机制(参见 docs/source/topics/fork_app.rst)fork 了某个 dashboard 子应用(如oscar.apps.dashboard.offers)并覆写权限时,旧实现可能没有从 fork 后的AppConfig上取权限,导致新配置的权限不生效。修复后,权限查找严格绑定到实际承载该 URL 的AppConfig实例。
_dashboard_url_names_to_config:URL 名称到配置的映射
支撑这一查找的是带@lru_cache缓存的辅助函数:
@lru_cache(maxsize=1) def _dashboard_url_names_to_config(): dashboard_configs = ( config for config in apps.get_app_configs() if isinstance(config, OscarDashboardConfig) ) urls_to_config = {} for config in dashboard_configs: for url in config.urls[0]: name = getattr(url, "name", None) if not name: continue if name in urls_to_config: if urls_to_config[name] != config: raise ImproperlyConfigured( "'{}' exists in both {} and {}!".format(name, config, urls_to_config[name]) ) urls_to_config[name] = config return urls_to_config它遍历所有已加载的、继承自OscarDashboardConfig(定义于 src/oscar/core/application.py)的应用配置,把每个 URL 名称映射到其所属 config。由于apps.get_app_configs()返回的是 Django 应用注册表中 fork 之后的最终版本,因此只要 fork 后的子类被正确注册(INSTALLED_APPS指向 fork 后的路径),导航权限就会从 fork 后的 config 上读取——这正是本次修复生效的关键前提。若两个 dashboard 应用注册了同名 URL 名称,该函数会抛出ImproperlyConfigured,避免权限归属歧义。
测试佐证
tests/integration/dashboard/test_nav.py 为default_access_fn提供了四组行为契约:
- 无 URL 名称(导航标题项)直接放行:
test_default_access_fn_no_url_name断言返回True; - 员工用户可访问
dashboard:index:test_default_access_fn_staff断言为True; - 非员工用户被拒绝:
test_default_access_fn_non_staff_user断言为False; - 无效或非仪表盘 URL 名称分别抛出
NoReverseMatch与KeyError(test_default_access_fn_invalid_url_name、test_default_access_non_dashboard_url_name)。
同文件中的DashboardNavTestCase进一步验证get_nodes(菜单节点生成)对员工与非员工用户的差异输出。这些用例共同保证了修复后的权限判定在常规与异常路径上都行为可预期。
修复二:base_queryset()从计数注解到存在性注解
重构动机:Count 子查询的代价
商品列表页、搜索结果页普遍依赖ProductQuerySet.base_queryset()一次性预取常用关联数据。在 2.0.1 之前,该方法会用Count分别统计商品类(product class)和商品自身关联的选项数量,并把结果注解为num_product_class_options、num_product_options两个字段。由于Count需要实际聚合每一行的关联行数,在选项数量较多、列表页行数较大的场景下,会产生昂贵的子查询与分组开销——而多数业务代码真正关心的只是"有没有选项"这一个布尔事实。
修复后的实现:Exists子查询
修复后的实现位于 src/oscar/apps/catalogue/managers.py 的ProductQuerySet.base_queryset():
def base_queryset(self): """...""" Option = get_model("catalogue", "Option") product_class_options = Option.objects.filter( productclass=OuterRef("product_class") ) product_options = Option.objects.filter(product=OuterRef("pk")) return ( self.select_related("product_class") .annotate( has_product_class_options=Exists(product_class_options), has_product_options=Exists(product_options), ) .prefetch_related( "children", "product_options", "product_class__options", "stockrecords", "images", ) )改动要点:
- 注解语义变更:
num_product_class_options/num_product_options(计数)→has_product_class_options/has_product_options(布尔存在性)。发布说明明确指出,新注解分别表示"商品类是否关联选项"与"商品自身是否关联选项"。 - 查询策略变更:
Count聚合改为Exists相关子查询(配合OuterRef)。数据库执行EXISTS时,一旦找到首条匹配记录即可短路返回,不再需要扫描并统计全部匹配行,这正是性能收益的来源。 - 预取链保持不变:
select_related("product_class")与对children、product_options、product_class__options、stockrecords、images的prefetch_related均保留,继续发挥列表页 N+1 查询的兜底作用。
兼容性提醒:依赖旧注解的代码需同步
这是本次修复中唯一带 API 语义变化的改动。如果自建项目中存在依赖num_product_class_options/num_product_options注解的查询或模板,升级到 2.0.1 后需改为使用新的has_*布尔注解;反过来,只关心"是否有选项"的既有调用可以无痛迁移,且能直接受益于性能提升。
测试佐证
tests/integration/catalogue/test_options.py 完整覆盖了新旧行为:
test_product_has_options_per_product_class/test_product_has_options_per_product:分别验证商品类选项与商品自身选项会反映到Product.has_options;test_queryset_per_product_class:商品类挂选项后,base_queryset()查询出的商品has_options与has_product_class_options均为True;test_queryset_per_product:商品自身挂选项后,has_options与has_product_options均为True;test_queryset_both:商品类与商品同时挂选项时,options属性合并返回且不重复(count() == 1),随后再追加两个选项验证即时可见性(count()变为 2、3)。
这套用例同时守护了"注解语义"与"options 合并去重"两条行为契约,可作为升级回归测试的参考。
修复三:仪表盘优惠列表的日期显示
问题与修复
第三个修复针对仪表盘优惠列表模板:开始日期(start datetime)与结束日期(end datetime)列原先在某些情况下显示异常。修复后的模板 src/oscar/templates/oscar/dashboard/offers/offer_list.html 中,两列分别以start_datetime、end_datetime为排序键渲染表头,并在空值时显示占位符-:
<th>{% anchor 'start_datetime' _('Start date') %}</th> <th>{% anchor 'end_datetime' _('End date') %}</th> ... <td>{{ offer.start_datetime|default:"-" }}</td> <td>{{ offer.end_datetime|default:"-" }}</td>{% anchor %}来自 src/oscar/templatetags/sorting_tags.py,负责生成可点击的排序链接;|default:"-"过滤器则保证未设置起止时间的优惠(数据库中允许为NULL)以-呈现而非空白。
日期字段在数据流中的角色
该模板由 src/oscar/apps/dashboard/offers/views.py 的列表视图渲染(template_name = "oscar/dashboard/offers/offer_list.html"),其查询逻辑与日期字段直接相关:
(Q(start_datetime__lte=now) | Q(start_datetime__isnull=True)) & (Q(end_datetime__gte=now) | Q(end_datetime__isnull=True)) # 进行中 Q(start_datetime__gt=now) | Q(end_datetime__lt=now) # 已结束/未开始即"是否可用"的状态判定完全依赖start_datetime/end_datetime与当前时刻的比较,且两字段均允许为空(视为不限时间)。因此,列表页中这两个日期列既是运营人员判断优惠有效期的直接入口,也是视图层状态分组的依据,其显示修复对后台可用性意义直接。
对应的表单字段定义在 src/oscar/apps/dashboard/offers/forms.py:start_datetime与end_datetime均为forms.DateTimeField,新建优惠时开始时间默认预填当前时刻(self.fields["start_datetime"].initial = timezone.now()),创建时还会校验开始时间必须早于结束时间。理解这一表单-视图-模板链路,有助于在 fork 优惠应用时保持日期行为的完整一致性。
升级与验证建议
对于运行 2.0 系列的 Oscar 项目,升级 2.0.1 时建议按以下顺序验证:
- 权限链路:若 fork 过任何 dashboard 子应用并自定义了
get_permissions,升级后重点回归仪表盘各导航项的可见性;可复用 tests/integration/dashboard/test_nav.py 的思路补充针对 fork 应用的用例。 - 商品查询:全库搜索
num_product_class_options、num_product_options的引用(模板、视图、自定义查询),替换为has_product_class_options、has_product_options;随后对商品列表页做一次查询计数对比(如 Django Debug Toolbar),确认EXISTS子查询生效。 - 优惠列表:打开仪表盘优惠列表页,检查包含未设置起止时间的优惠在内,
Start date/End date两列均正确显示并可排序。
完整改动清单与版本说明可查阅 docs/source/releases/v2.0.1.rst,相关实现与测试分布在 src/oscar/apps/dashboard/nav.py、src/oscar/apps/catalogue/managers.py、src/oscar/templates/oscar/dashboard/offers/offer_list.html 及其配套测试中,可作为进一步阅读的起点。
- 后端
- 电商
【免费下载链接】django-oscar
Domain-driven e-commerce for Django
相关推荐
PDF补丁丁:免费开源的PDF全能工具箱,轻松编辑合并提取PDF文档
PDF补丁丁:免费开源的PDF全能工具箱,轻松编辑合并提取PDF文档 PDF补丁丁是一款功能强大的开源PDF工具箱,专为处理各种PDF文档需求而设计。无论你是需
桌面应用文档RabbitMQ 3.12.8 维护版发布解读:核心修复、Raft 配置上限校验与 Shovel 启动性能优化
RabbitMQ 3.12.8 维护版发布解读:核心修复、Raft 配置上限校验与 Shovel 启动性能优化 RabbitMQ 3.12.8 是 3.12.x
后端消息队列消息路由Vitess v18.0.5 发布说明:查询路由修复、VReplication 健康检查与性能优化全解读
Vitess v18.0.5 发布说明:查询路由修复、VReplication 健康检查与性能优化全解读 导读 本文基于 Vitess 官方仓库中 change
数据库分布式数据库云原生后端数据存储
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考