You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django 1.11:按用户分组并基于首次注解计算总阅读时长

Django 1.11 按用户汇总阅读会话总时长问题解决

这个问题我之前维护Django 1.x项目时也碰到过,核心是没搞清楚QuerySet里values和annotate的执行顺序逻辑,先给你拆解错误原因,再给两种可行的解决方法:

错误原因分析

你遇到的KeyError: 'duration'是Django QuerySet的特性导致的:
当你调用.values('user')时,它会重置当前QuerySet要查询的字段集合,只保留user这个关联字段,之前通过annotate(duration=...)生成的临时计算字段会被直接丢弃。后续再调用.annotate(total_duration=Sum('duration'))时,QuerySet里已经没有duration字段了,自然就抛出KeyError。

而如果把duration加到values参数里,分组逻辑就变成了按user + duration组合分组,这会把同一个用户的不同时长的会话分开统计,完全不符合你“按用户汇总所有会话时长”的需求。

可行解决方法

方法一:直接分组汇总(推荐,更高效)

不需要单独给每个会话计算时长,直接在分组时用Sum包裹Func函数,一次性计算每个用户的总时长。这样跳过了中间临时字段的生成,避免了values过滤字段的问题:

from django.db.models import F, Func, Sum

usernames = ['user1', 'user2']
# 按用户分组,直接汇总所有会话的时长总和
queryset = ReadingSession.objects.filter(user__username__in=usernames)\
    .values('user__username', 'user_id')  # 用username或user_id作为分组依据,方便后续查看结果
    .annotate(
        total_duration=Sum(
            Func(F('ended_at'), F('started_at'), function='age')
        )
    )

# 遍历输出结果
for record in queryset:
    print(f"用户 {record['user__username']} 的总阅读时长:{record['total_duration']}")

方法二:保留临时字段到汇总阶段

如果你确实需要先拿到每个会话的单独时长,再进行汇总,可以调整QuerySet的执行顺序,把最终的values(指定输出字段)放到最后,确保duration字段在汇总时仍然存在:

from django.db.models import F, Func, Sum

usernames = ['user1', 'user2']
queryset = ReadingSession.objects.filter(user__username__in=usernames)\
    .annotate(duration=Func(F('ended_at'), F('started_at'), function='age'))\
    .values('user__username', 'user_id')  # 先指定分组依据
    .annotate(total_duration=Sum('duration'))\
    .values('user__username', 'total_duration')  # 最后只输出需要的字段

print(queryset)

注意事项

  • 这里用到的age是PostgreSQL的内置函数,如果你用的是其他数据库(比如MySQL),需要替换成对应计算时间差的函数(比如MySQL的TIMESTAMPDIFF)。
  • Django会把数据库返回的时间间隔类型转成Python的datetime.timedelta对象,你可以直接用它的方法(比如total_seconds())转换成更易读的格式。

内容的提问来源于stack exchange,提问作者Watchoutman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:30:22