diff --git a/backend/view.py b/backend/view.py index 025417c..663fd84 100644 --- a/backend/view.py +++ b/backend/view.py @@ -6939,13 +6939,14 @@ class CaiwuView(APIView): today_start = datetime.combine(today, datetime.min.time()) today_end = today_start + timedelta(days=1) - # 今日收入 / 支出(全平台各俱乐部合计) - today_inc = DailyIncomeStat.query.filter(date=today).aggregate( - total_amount=Sum('total_amount'), - total_count=Sum('total_count'), + from jituan.services.live_income_stats import ( + income_daily_stats_last_days, + income_for_date, + income_total_all_time, ) - today_income_amount = float(today_inc['total_amount'] or 0.00) - today_income_count = int(today_inc['total_count'] or 0) + + # 今日收入:从真实微信支付汇总,不读 daily_income_stat + today_income_amount, today_income_count = income_for_date(today) today_pay = DailyPayoutStat.query.filter(date=today).aggregate( total_amount=Sum('total_amount'), @@ -7016,23 +7017,20 @@ class CaiwuView(APIView): all_zuzhang_yue = float(UserZuzhang.query.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00) all_shangjia_yue = float(UserShangjia.query.aggregate(total=Sum('yue'))['total'] or 0.00) - # 平台总收支及利润 - total_income = float(DailyIncomeStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00) + # 平台总收支及利润(收入侧实时汇总) + total_income, _ = income_total_all_time() total_payout = float(DailyPayoutStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00) platform_profit = round(total_income - total_payout, 2) - # 近30天每日收支明细(按日期合并各俱乐部) + # 近30天每日收支明细 daily_stats = [] since = today - timedelta(days=30) income_by_date = { - str(row['date']): { - 'income': float(row['total_amount'] or 0), - 'income_count': int(row['total_count'] or 0), + row['date']: { + 'income': row['income'], + 'income_count': row['income_count'], } - for row in DailyIncomeStat.query.filter(date__gte=since).values('date').annotate( - total_amount=Sum('total_amount'), - total_count=Sum('total_count'), - ) + for row in income_daily_stats_last_days(30) } payout_by_date = { str(row['date']): float(row['total_amount'] or 0) @@ -7408,12 +7406,13 @@ class SzxxView(APIView): else: return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) - # ---------- 收入分组查询 ---------- - income_qs = filter_queryset_by_club( - DailyIncomeStat.query.filter(**income_filter), request, - ).values(group_field) \ - .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \ - .order_by(group_field) + # ---------- 收入:从真实支付汇总;支出仍读 daily_payout_stat ---------- + from jituan.services.live_income_stats import ( + income_summary_filter, + income_time_series, + ) + + income_map = income_time_series(granularity, year, month, request) # ---------- 支出分组查询 ---------- payout_qs = filter_queryset_by_club( @@ -7424,13 +7423,12 @@ class SzxxView(APIView): # ---------- 合并数据 ---------- data_map = {} - for item in income_qs: - key = item[group_field] + for key, vals in income_map.items(): data_map[key] = { - 'income': float(item['total_income'] or 0), - 'income_count': item['total_count'] or 0, + 'income': vals['income'], + 'income_count': vals.get('income_count', 0), 'payout': 0.0, - 'payout_count': 0 + 'payout_count': 0, } for item in payout_qs: @@ -7459,17 +7457,25 @@ class SzxxView(APIView): time_series.sort(key=lambda x: x['date']) # ---------- 汇总数据 ---------- - income_summary = filter_queryset_by_club( - DailyIncomeStat.query.filter(**summary_filter), request, - ).aggregate( - total_income=Sum('total_amount'), total_count=Sum('total_count') - ) + if granularity == 'day': + summary_day = int(summary_parts[2]) + total_income, total_income_count = income_summary_filter( + 'day', int(summary_parts[0]), int(summary_parts[1]), summary_day, request, + ) + elif granularity == 'month': + total_income, total_income_count = income_summary_filter( + 'month', int(summary_parts[0]), int(summary_parts[1]), None, request, + ) + else: + total_income, total_income_count = income_summary_filter( + 'year', int(summary_parts[0]), None, None, request, + ) + payout_summary = filter_queryset_by_club( DailyPayoutStat.query.filter(**summary_filter), request, ).aggregate( total_payout=Sum('total_amount'), total_count=Sum('total_count') ) - total_income = float(income_summary['total_income'] or 0) total_payout = float(payout_summary['total_payout'] or 0) total_profit = round(total_income - total_payout, 2) @@ -7480,7 +7486,7 @@ class SzxxView(APIView): 'time_series': time_series, 'summary': { 'total_income': total_income, - 'total_income_count': income_summary['total_count'] or 0, + 'total_income_count': total_income_count, 'total_payout': total_payout, 'total_payout_count': payout_summary['total_count'] or 0, 'total_profit': total_profit diff --git a/config/management/commands/restore_daily_income_from_orders.py b/config/management/commands/restore_daily_income_from_orders.py new file mode 100644 index 0000000..c0c43e1 --- /dev/null +++ b/config/management/commands/restore_daily_income_from_orders.py @@ -0,0 +1,47 @@ +""" +从订单/充值表恢复 daily_income_stat(不删订单,只重算统计表)。 +repair/rebuild 搞乱后用这个,不要用 repair_wechat_income。 + + 预览:python manage.py restore_daily_income_from_orders + 执行:python manage.py restore_daily_income_from_orders --yes +""" +from datetime import date + +from django.core.management.base import BaseCommand + +from jituan.services.szjilu_accounting import restore_daily_income_from_orders + + +class Command(BaseCommand): + help = '从订单/充值表恢复日收入统计(订单不动)' + + def add_arguments(self, parser): + parser.add_argument('--since-year', type=int, default=0) + parser.add_argument('--until-year', type=int, default=0) + parser.add_argument('--yes', action='store_true') + + def handle(self, *args, **options): + until_year = options['until_year'] or date.today().year + since_year = options['since_year'] or (until_year - 1) + dry_run = not options['yes'] + + if dry_run: + self.stdout.write(self.style.WARNING('【预览】订单数据不会动\n')) + else: + self.stdout.write(self.style.WARNING('【执行】按订单/充值重算日统计...\n')) + + r = restore_daily_income_from_orders(since_year, until_year, dry_run=dry_run) + self.stdout.write(f'范围: {r["since_year"]} ~ {r["until_year"]}') + self.stdout.write(f' 支付笔数(订单表): {r["payment_rows"]}') + self.stdout.write(f' 日统计行数: {r["daily_rows"]}') + self.stdout.write(f' 当前 daily 合计: {r["before_daily_total"]:.2f} 元') + self.stdout.write(self.style.SUCCESS( + f' 恢复后 daily 合计: {r["after_daily_total"]:.2f} 元' + )) + + if dry_run: + self.stdout.write(self.style.WARNING( + '\n确认: python manage.py restore_daily_income_from_orders --yes' + )) + else: + self.stdout.write(self.style.SUCCESS('\n完成。推 kefu 后刷新财务页。')) diff --git a/config/management/commands/sync_szjilu_from_live.py b/config/management/commands/sync_szjilu_from_live.py new file mode 100644 index 0000000..09a96f4 --- /dev/null +++ b/config/management/commands/sync_szjilu_from_live.py @@ -0,0 +1,12 @@ +"""按真实支付重算 szjilu 收支记录表(与每日收入看板无关,可选执行)。""" +from django.core.management.base import BaseCommand + +from jituan.services.live_income_stats import sync_szjilu_from_live_payments + + +class Command(BaseCommand): + help = '按订单/充值重算 szjilu 表 TotalIncome、TotalFlow、DailyFlow(不动 daily_income_stat)' + + def handle(self, *args, **options): + n = sync_szjilu_from_live_payments() + self.stdout.write(self.style.SUCCESS(f'已更新 {n} 条 szjilu 记录')) diff --git a/jituan/services/caiwu_stats.py b/jituan/services/caiwu_stats.py index bdf73e5..3732b81 100644 --- a/jituan/services/caiwu_stats.py +++ b/jituan/services/caiwu_stats.py @@ -12,8 +12,13 @@ from jituan.services.club_context import ( resolve_club_scope, ) from jituan.constants import DATA_SCOPE_ALL +from jituan.services.live_income_stats import ( + income_daily_stats_last_days, + income_for_date, + income_total_all_time, +) -from config.models import DailyIncomeStat, DailyPayoutStat, Szjilu +from config.models import DailyPayoutStat, Szjilu from orders.models import Order from products.models import Czjilu, Huiyuangoumai from users.models import UserDashou, UserGuanshi, UserShangjia, UserZuzhang @@ -66,10 +71,6 @@ def _huiyuangoumai_qs(request): return filter_club_char_field(Huiyuangoumai.query.all(), request, field='club_id') -def _daily_income_qs(request): - return filter_club_char_field(DailyIncomeStat.query.all(), request, field='club_id') - - def _daily_payout_qs(request): return filter_club_char_field(DailyPayoutStat.query.all(), request, field='club_id') @@ -84,19 +85,11 @@ def build_caiwu_payload(request): order_qs = _order_qs(request) cz_qs = _czjilu_qs(request) hy_qs = _huiyuangoumai_qs(request) - income_qs = _daily_income_qs(request) payout_qs = _daily_payout_qs(request) - scope = resolve_club_scope(request) - today_income_qs = income_qs.filter(date=today) today_payout_qs = payout_qs.filter(date=today) - inc_agg = today_income_qs.aggregate( - total_amount=Sum('total_amount'), - total_count=Sum('total_count'), - ) - today_income_amount = float(inc_agg['total_amount'] or 0.00) - today_income_count = int(inc_agg['total_count'] or 0) + today_income_amount, today_income_count = income_for_date(today, request) pay_agg = today_payout_qs.aggregate( total_amount=Sum('total_amount'), @@ -154,67 +147,43 @@ def build_caiwu_payload(request): szjilu_payload = _build_szjilu_payload(request) - # 账户总收入/总支出:始终全集团合计(切换俱乐部也不变少) - total_income = float( - DailyIncomeStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00 - ) + total_income, _ = income_total_all_time() total_payout = float( DailyPayoutStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00 ) - # 俱乐部利润:当前视图范围内累计收入减累计支出 - scoped_total_income = float(income_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00) + scoped_total_income, _ = income_total_all_time(request) scoped_total_payout = float(payout_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00) platform_profit = round(scoped_total_income - scoped_total_payout, 2) daily_stats = [] since = today - timedelta(days=30) - if scope == DATA_SCOPE_ALL: - income_by_date = { - str(row['date']): { - 'income': float(row['total_amount'] or 0), - 'income_count': int(row['total_count'] or 0), - } - for row in income_qs.filter(date__gte=since).values('date').annotate( - total_amount=Sum('total_amount'), - total_count=Sum('total_count'), - ) + income_by_date = { + row['date']: { + 'income': row['income'], + 'income_count': row['income_count'], } - payout_by_date = { - str(row['date']): float(row['total_amount'] or 0) - for row in payout_qs.filter(date__gte=since).values('date').annotate( - total_amount=Sum('total_amount'), - ) - } - all_dates = sorted( - set(income_by_date.keys()) | set(payout_by_date.keys()), - reverse=True, - )[:30] - for d in all_dates: - inc = income_by_date.get(d, {'income': 0.0, 'income_count': 0}) - payout_amt = payout_by_date.get(d, 0.00) - daily_stats.append({ - 'date': d, - 'income': inc['income'], - 'income_count': inc['income_count'], - 'payout': payout_amt, - 'profit': round(inc['income'] - payout_amt, 2), - }) - else: - income_list = income_qs.filter(date__gte=since).order_by('-date')[:30] - payout_dict = { - str(p.date): float(p.total_amount) - for p in payout_qs.filter(date__gte=since) - } - for inc in income_list: - d = str(inc.date) - payout_amt = payout_dict.get(d, 0.00) - daily_stats.append({ - 'date': d, - 'income': float(inc.total_amount), - 'income_count': inc.total_count, - 'payout': payout_amt, - 'profit': round(float(inc.total_amount) - payout_amt, 2), - }) + for row in income_daily_stats_last_days(30, request) + } + payout_by_date = { + str(row['date']): float(row['total_amount'] or 0) + for row in payout_qs.filter(date__gte=since).values('date').annotate( + total_amount=Sum('total_amount'), + ) + } + all_dates = sorted( + set(income_by_date.keys()) | set(payout_by_date.keys()), + reverse=True, + )[:30] + for d in all_dates: + inc = income_by_date.get(d, {'income': 0.0, 'income_count': 0}) + payout_amt = payout_by_date.get(d, 0.00) + daily_stats.append({ + 'date': d, + 'income': inc['income'], + 'income_count': inc['income_count'], + 'payout': payout_amt, + 'profit': round(inc['income'] - payout_amt, 2), + }) return { 'club_id': club_id, diff --git a/jituan/services/live_income_stats.py b/jituan/services/live_income_stats.py new file mode 100644 index 0000000..ac6ae8b --- /dev/null +++ b/jituan/services/live_income_stats.py @@ -0,0 +1,264 @@ +""" +每日收入:从真实微信支付订单/充值单实时汇总。 +不读 daily_income_stat(该表可被 repair 搞乱,与展示解耦)。 + +每日支出:仍读 daily_payout_stat(repair 未动支出)。 +收支记录 szjilu:独立表,本模块不负责展示。 +""" +from calendar import monthrange +from datetime import date, datetime, timedelta +from decimal import Decimal + +from django.db.models import Count, Q, Sum +from django.db.models.functions import ExtractYear, TruncDate, TruncMonth + +from jituan.constants import CLUB_ID_DEFAULT +from jituan.services.club_context import ( + filter_club_char_field, + filter_queryset_by_club, +) + +_ORDER_PAID = [1, 2, 3, 4, 6, 7, 8] +_CZ_PAID_LEIXING = [1, 2, 3, 4, 5] +_ZERO = Decimal('0.00') + + +def _orders_qs(request=None): + from orders.models import Order + qs = Order.query.filter(Status__in=_ORDER_PAID) + if request is not None: + qs = filter_queryset_by_club(qs, request, club_field='ClubID') + return qs + + +def _cz_qs(request=None): + from products.models import Czjilu + qs = Czjilu.query.filter(zhuangtai=3, leixing__in=_CZ_PAID_LEIXING) + if request is not None: + qs = filter_club_char_field(qs, request, field='club_id') + return qs + + +def _merge_amount_count(order_rows, cz_rows, key_field='d'): + out = {} + for row in order_rows: + k = row[key_field] + if k is None: + continue + if k not in out: + out[k] = {'amount': _ZERO, 'count': 0} + out[k]['amount'] += Decimal(str(row['total'] or 0)) + out[k]['count'] += int(row['cnt'] or 0) + for row in cz_rows: + k = row[key_field] + if k is None: + continue + if k not in out: + out[k] = {'amount': _ZERO, 'count': 0} + out[k]['amount'] += Decimal(str(row['total'] or 0)) + out[k]['count'] += int(row['cnt'] or 0) + return out + + +def income_for_date(stat_date, request=None): + """指定日期的微信收入金额与笔数。""" + day_start = datetime.combine(stat_date, datetime.min.time()) + day_end = day_start + timedelta(days=1) + o = _orders_qs(request).filter( + UpdateTime__gte=day_start, UpdateTime__lt=day_end, + ).aggregate(total=Sum('Amount'), cnt=Count('OrderID')) + c = _cz_qs(request).filter( + UpdateTime__gte=day_start, UpdateTime__lt=day_end, + ).aggregate(total=Sum('jine'), cnt=Count('dingdan_id')) + amt = Decimal(str(o['total'] or 0)) + Decimal(str(c['total'] or 0)) + cnt = int(o['cnt'] or 0) + int(c['cnt'] or 0) + return float(amt), cnt + + +def income_total_all_time(request=None): + """历史累计微信收入。""" + o = _orders_qs(request).aggregate(total=Sum('Amount'), cnt=Count('OrderID')) + c = _cz_qs(request).aggregate(total=Sum('jine'), cnt=Count('dingdan_id')) + amt = Decimal(str(o['total'] or 0)) + Decimal(str(c['total'] or 0)) + cnt = int(o['cnt'] or 0) + int(c['cnt'] or 0) + return float(amt), cnt + + +def income_daily_stats_last_days(days=30, request=None): + """近 N 天每日收入(用于财务看板 daily_stats)。""" + since = date.today() - timedelta(days=days) + since_dt = datetime.combine(since, datetime.min.time()) + + o_rows = ( + _orders_qs(request) + .filter(UpdateTime__gte=since_dt) + .annotate(d=TruncDate('UpdateTime')) + .values('d') + .annotate(total=Sum('Amount'), cnt=Count('OrderID')) + ) + c_rows = ( + _cz_qs(request) + .filter(UpdateTime__gte=since_dt) + .annotate(d=TruncDate('UpdateTime')) + .values('d') + .annotate(total=Sum('jine'), cnt=Count('dingdan_id')) + ) + merged = _merge_amount_count(list(o_rows), list(c_rows), 'd') + result = [] + for d, v in sorted(merged.items(), reverse=True)[:days]: + result.append({ + 'date': str(d), + 'income': float(v['amount']), + 'income_count': v['count'], + }) + return result + + +def income_time_series(granularity, year, month=None, request=None): + """ + 收支详细页时间序列(仅收入侧,支出由调用方合并 daily_payout_stat)。 + granularity: day | month | year + """ + if granularity == 'day': + if not year or not month: + return {} + _, last = monthrange(year, month) + start = datetime(year, month, 1) + end = datetime(year, month, last) + timedelta(days=1) + o_rows = ( + _orders_qs(request) + .filter(UpdateTime__gte=start, UpdateTime__lt=end) + .annotate(d=TruncDate('UpdateTime')) + .values('d') + .annotate(total=Sum('Amount'), cnt=Count('OrderID')) + ) + c_rows = ( + _cz_qs(request) + .filter(UpdateTime__gte=start, UpdateTime__lt=end) + .annotate(d=TruncDate('UpdateTime')) + .values('d') + .annotate(total=Sum('jine'), cnt=Count('dingdan_id')) + ) + merged = _merge_amount_count(list(o_rows), list(c_rows), 'd') + out = {} + for d, v in merged.items(): + out[d.day] = {'income': float(v['amount']), 'income_count': v['count']} + return out + + if granularity == 'month': + start = datetime(year, 1, 1) + end = datetime(year + 1, 1, 1) + o_rows = ( + _orders_qs(request) + .filter(UpdateTime__gte=start, UpdateTime__lt=end) + .annotate(m=TruncMonth('UpdateTime')) + .values('m') + .annotate(total=Sum('Amount'), cnt=Count('OrderID')) + ) + c_rows = ( + _cz_qs(request) + .filter(UpdateTime__gte=start, UpdateTime__lt=end) + .annotate(m=TruncMonth('UpdateTime')) + .values('m') + .annotate(total=Sum('jine'), cnt=Count('dingdan_id')) + ) + merged = _merge_amount_count(list(o_rows), list(c_rows), 'm') + out = {} + for m, v in merged.items(): + out[m.month] = {'income': float(v['amount']), 'income_count': v['count']} + return out + + o_rows = ( + _orders_qs(request) + .annotate(y=ExtractYear('UpdateTime')) + .values('y') + .annotate(total=Sum('Amount'), cnt=Count('OrderID')) + ) + c_rows = ( + _cz_qs(request) + .annotate(y=ExtractYear('UpdateTime')) + .values('y') + .annotate(total=Sum('jine'), cnt=Count('dingdan_id')) + ) + merged = _merge_amount_count(list(o_rows), list(c_rows), 'y') + return { + int(k): {'income': float(v['amount']), 'income_count': v['count']} + for k, v in merged.items() + } + + +def income_summary_filter(granularity, year, month=None, day=None, request=None): + """详细页顶部汇总区间的收入。""" + if granularity == 'day' and year and month and day: + return income_for_date(date(year, month, day), request) + if granularity == 'month' and year and month: + start = datetime(year, month, 1) + if month == 12: + end = datetime(year + 1, 1, 1) + else: + end = datetime(year, month + 1, 1) + elif granularity == 'year' and year: + start = datetime(year, 1, 1) + end = datetime(year + 1, 1, 1) + else: + return income_total_all_time(request) + + o = _orders_qs(request).filter( + UpdateTime__gte=start, UpdateTime__lt=end, + ).aggregate(total=Sum('Amount'), cnt=Count('OrderID')) + c = _cz_qs(request).filter( + UpdateTime__gte=start, UpdateTime__lt=end, + ).aggregate(total=Sum('jine'), cnt=Count('dingdan_id')) + amt = Decimal(str(o['total'] or 0)) + Decimal(str(c['total'] or 0)) + cnt = int(o['cnt'] or 0) + int(c['cnt'] or 0) + return float(amt), cnt + + +def sync_szjilu_from_live_payments(): + """按真实支付重算 szjilu 收支记录表(与每日收入展示无关,可单独执行)。""" + from config.models import Szjilu + + today = date.today() + today_start = datetime.combine(today, datetime.min.time()) + today_end = today_start + timedelta(days=1) + + clubs = set() + for row in _orders_qs().values('ClubID').distinct(): + clubs.add((row.get('ClubID') or '').strip() or CLUB_ID_DEFAULT) + for row in _cz_qs().values('club_id').distinct(): + clubs.add((row.get('club_id') or '').strip() or CLUB_ID_DEFAULT) + + def _club_orders(cid): + qs = _orders_qs() + if cid == CLUB_ID_DEFAULT: + return qs.filter(Q(ClubID=cid) | Q(ClubID__isnull=True) | Q(ClubID='')) + return qs.filter(ClubID=cid) + + updated = 0 + for cid in clubs: + o_total = _club_orders(cid).aggregate(t=Sum('Amount'))['t'] or 0 + c_total = _cz_qs().filter(club_id=cid).aggregate(t=Sum('jine'))['t'] or 0 + total = Decimal(str(o_total)) + Decimal(str(c_total)) + + o_day = _club_orders(cid).filter( + UpdateTime__gte=today_start, UpdateTime__lt=today_end, + ) + o_day_amt = o_day.aggregate(t=Sum('Amount'))['t'] or 0 + c_day = _cz_qs().filter( + club_id=cid, UpdateTime__gte=today_start, UpdateTime__lt=today_end, + ).aggregate(t=Sum('jine'))['t'] or 0 + day_flow = Decimal(str(o_day_amt)) + Decimal(str(c_day)) + + szjilu, _ = Szjilu.objects.get_or_create( + club_id=cid, + defaults={ + 'TotalIncome': _ZERO, 'TotalFlow': _ZERO, 'TotalExpense': _ZERO, + 'DailyExpense': _ZERO, 'DailyFlow': _ZERO, + }, + ) + szjilu.TotalIncome = total + szjilu.TotalFlow = total + szjilu.DailyFlow = day_flow + szjilu.save(update_fields=['TotalIncome', 'TotalFlow', 'DailyFlow', 'UpdateTime']) + updated += 1 + return updated diff --git a/jituan/services/szjilu_accounting.py b/jituan/services/szjilu_accounting.py index fe5a0ea..837f1de 100644 --- a/jituan/services/szjilu_accounting.py +++ b/jituan/services/szjilu_accounting.py @@ -950,6 +950,62 @@ def collect_today_live_wechat_payments(stat_date=None): return items +def restore_daily_income_from_orders(since_year=None, until_year=None, dry_run=True): + """ + 从订单/充值表重算 daily_income_stat(订单数据不动,只覆盖统计表)。 + """ + from collections import defaultdict + + from django.db.models import Sum + + until_year = until_year or date.today().year + since_year = since_year or (until_year - 1) + + items = collect_legitimate_wechat_payments(since_year, until_year) + by_day_club = defaultdict(lambda: {'amt': _ZERO, 'cnt': 0}) + for _ref, cid, jine, stat_d, _pay_dt, _kind in items: + key = (stat_d, cid) + by_day_club[key]['amt'] += jine + by_day_club[key]['cnt'] += 1 + + before = float(DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0) + total_after = sum((v['amt'] for v in by_day_club.values()), _ZERO) + + result = { + 'since_year': since_year, + 'until_year': until_year, + 'payment_rows': len(items), + 'daily_rows': len(by_day_club), + 'before_daily_total': before, + 'after_daily_total': float(total_after), + 'dry_run': dry_run, + } + if dry_run: + return result + + with transaction.atomic(): + DailyIncomeStat.objects.filter( + date__gte=date(since_year, 1, 1), + date__lte=date(until_year, 12, 31), + ).delete() + for (stat_d, cid), agg in by_day_club.items(): + DailyIncomeStat.objects.create( + date=stat_d, + club_id=cid, + year=stat_d.year, + month=stat_d.month, + day=stat_d.day, + total_amount=agg['amt'], + total_count=agg['cnt'], + ) + sync_szjilu_income_from_logs() + + result['after_daily_total'] = float( + DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0 + ) + return result + + def repopulate_today_income_stats(stat_date=None, dry_run=True): """只重建指定日期的日收入(不删历史其它天),修复今日显示为 0。""" from collections import defaultdict