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

Django序列化器计算钱包实例收支总和出错,求正确实现方案

问题描述

在应用中,每个用户拥有专属钱包存储日常收支记录,需要获取每个钱包实例下关联的收支金额总和。使用serializers.SerializerMethodField()实现时计算结果错误:示例中两个income类型的金额分别为7和12,正确总和应为19,但返回的expense_sum为83。

现有Serializer代码

from django.db.models import Sum
from django.db.models import Q

class WalletInstanceSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.id')
    entry = BudgetEntrySerializer(many=True, read_only=True)
    expense_sum = serializers.SerializerMethodField()

    class Meta:
        model = WalletInstance
        fields = '__all__'

    def get_expense_sum(self, obj):
        return WalletInstance.objects.filter(Q(id=obj.id)|Q(entry__entry_type='income')).aggregate(Sum('entry__amount'))['entry__amount__sum']

返回数据示例

[
    {
        "id": "d458196e-49f1-42db-8bc2-ee1dba438953",
        "owner": 1,
        "entry": [
            {
                "id": 3,
                "owner": 1,
                "title": "dsfdsf",
                "amount": 7,
                "description": "sdfdsf",
                "entry_type": "income",
                "date_added": "2022-08-13",
                "entry_category": {
                    "id": 2,
                    "name": "Transport"
                }
            },
            {
                "id": 4,
                "owner": 1,
                "title": "fesfvsdfgvbtdg",
                "amount": 12,
                "description": "efesf",
                "entry_type": "income",
                "date_added": "2022-08-13",
                "entry_category": {
                    "id": 2,
                    "name": "Transport"
                }
            }
        ],
        "viewable": [
            "admin@gmail.com"
        ],
        "expense_sum": 83,
        "name": "dsdsds",
        "date_added": "2022-08-13"
    }
]

相关Model代码

class BudgetEntry(models.Model):
    STATE= [
        ('income','income'),
        ('expenses','expenses'),
    ]
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='owner_of_entry', on_delete=models.CASCADE)
    title = models.CharField(max_length=20)
    amount = models.IntegerField()
    description = models.CharField(max_length=60, null=True)
    entry_type = models.CharField(max_length=15, choices=STATE, null=True)
    entry_category = models.ForeignKey(Category, null=True, blank=True, related_name='category_of_entry', on_delete=models.SET_NULL)
    date_added = models.DateField(auto_now_add=True)

class WalletInstance(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
    name = models.CharField(max_length=30, null=True)
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='owner', on_delete=models.CASCADE)
    viewable = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='can_view', blank=True)
    entry = models.ManyToManyField(BudgetEntry, related_name='BudgetEntry', blank=True)
    date_added = models.DateField(auto_now_add=True)
问题分析

原代码的错误在于filter(Q(id=obj.id)|Q(entry__entry_type='income'))使用了逻辑或(|),这会筛选出当前钱包或者所有包含income类型收支的钱包,导致聚合计算时包含了其他钱包的收支数据,结果自然错误。

另外,通过WalletInstance反向关联BudgetEntry进行聚合时,多对多关系可能会产生重复行,影响求和结果,更高效的方式是直接从BudgetEntry模型筛选当前钱包关联的记录。

修正方案

方案一:修正SerializerMethodField的查询逻辑

直接从当前钱包的entry关联集合中筛选entry_type='income'的记录并求和,避免跨钱包的错误数据:

from django.db.models import Sum

class WalletInstanceSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.id')
    entry = BudgetEntrySerializer(many=True, read_only=True)
    expense_sum = serializers.SerializerMethodField()

    class Meta:
        model = WalletInstance
        fields = '__all__'

    def get_expense_sum(self, obj):
        # 从当前钱包的entry关联中筛选income类型并求和,无数据时返回0避免None
        return obj.entry.filter(entry_type='income').aggregate(Sum('amount'))['amount__sum'] or 0

方案二:使用annotate提前聚合(更高效)

如果需要批量查询多个钱包,推荐在视图层使用annotate提前聚合数据,减少Serializer中的重复查询,提升性能:

# 视图中的查询逻辑
from django.db.models import Sum, Q

wallet_instances = WalletInstance.objects.annotate(
    expense_sum=Sum(
        'entry__amount',
        filter=Q(entry__entry_type='income'),
        output_field=models.IntegerField()
    )
)

# 此时Serializer可以直接使用annotate生成的字段,无需SerializerMethodField
class WalletInstanceSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.id')
    entry = BudgetEntrySerializer(many=True, read_only=True)
    expense_sum = serializers.IntegerField(read_only=True)  # 直接映射annotate的字段

    class Meta:
        model = WalletInstance
        fields = '__all__'

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 12:03:14