diff --git a/backend/view.py b/backend/view.py index 87c63c8..025417c 100644 --- a/backend/view.py +++ b/backend/view.py @@ -6939,15 +6939,20 @@ class CaiwuView(APIView): today_start = datetime.combine(today, datetime.min.time()) today_end = today_start + timedelta(days=1) - # 今日收入 - today_income = DailyIncomeStat.query.filter(date=today).first() - today_income_amount = float(today_income.total_amount) if today_income else 0.00 - today_income_count = today_income.total_count if today_income else 0 + # 今日收入 / 支出(全平台各俱乐部合计) + today_inc = DailyIncomeStat.query.filter(date=today).aggregate( + total_amount=Sum('total_amount'), + total_count=Sum('total_count'), + ) + today_income_amount = float(today_inc['total_amount'] or 0.00) + today_income_count = int(today_inc['total_count'] or 0) - # 今日支出 - today_payout = DailyPayoutStat.query.filter(date=today).first() - today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00 - today_payout_count = today_payout.total_count if today_payout else 0 + today_pay = DailyPayoutStat.query.filter(date=today).aggregate( + total_amount=Sum('total_amount'), + total_count=Sum('total_count'), + ) + today_payout_amount = float(today_pay['total_amount'] or 0.00) + today_payout_count = int(today_pay['total_count'] or 0) # 今日订单(已付款及之后的状态) today_orders = Order.query.filter( @@ -7016,21 +7021,38 @@ class CaiwuView(APIView): 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 = [] - income_qs = DailyIncomeStat.query.order_by('-date')[:30] - payout_dict = { - str(p.date): float(p.total_amount) - for p in DailyPayoutStat.query.filter(date__gte=today - timedelta(days=30)) + 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), + } + for row in DailyIncomeStat.query.filter(date__gte=since).values('date').annotate( + total_amount=Sum('total_amount'), + total_count=Sum('total_count'), + ) } - for inc in income_qs: - d = str(inc.date) + payout_by_date = { + str(row['date']): float(row['total_amount'] or 0) + for row in DailyPayoutStat.query.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': float(inc.total_amount), - 'income_count': inc.total_count, - 'payout': payout_dict.get(d, 0.00), - 'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2) + 'income': inc['income'], + 'income_count': inc['income_count'], + 'payout': payout_amt, + 'profit': round(inc['income'] - payout_amt, 2), }) return Response({ diff --git a/jituan/services/caiwu_stats.py b/jituan/services/caiwu_stats.py index f2f69df..e499008 100644 --- a/jituan/services/caiwu_stats.py +++ b/jituan/services/caiwu_stats.py @@ -87,13 +87,29 @@ def build_caiwu_payload(request): income_qs = _daily_income_qs(request) payout_qs = _daily_payout_qs(request) - today_income = income_qs.filter(date=today).first() - today_income_amount = float(today_income.total_amount) if today_income else 0.00 - today_income_count = today_income.total_count if today_income else 0 - - today_payout = payout_qs.filter(date=today).first() - today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00 - today_payout_count = today_payout.total_count if today_payout else 0 + scope = resolve_club_scope(request) + today_income_qs = income_qs.filter(date=today) + today_payout_qs = payout_qs.filter(date=today) + if scope == DATA_SCOPE_ALL: + 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) + pay_agg = today_payout_qs.aggregate( + total_amount=Sum('total_amount'), + total_count=Sum('total_count'), + ) + today_payout_amount = float(pay_agg['total_amount'] or 0.00) + today_payout_count = int(pay_agg['total_count'] or 0) + else: + today_income = today_income_qs.first() + today_income_amount = float(today_income.total_amount) if today_income else 0.00 + today_income_count = today_income.total_count if today_income else 0 + today_payout = today_payout_qs.first() + today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00 + today_payout_count = today_payout.total_count if today_payout else 0 today_orders = order_qs.filter( CreateTime__gte=today_start, @@ -150,20 +166,54 @@ def build_caiwu_payload(request): platform_profit = round(total_income - total_payout, 2) daily_stats = [] - income_list = income_qs.order_by('-date')[:30] - payout_dict = { - str(p.date): float(p.total_amount) - for p in payout_qs.filter(date__gte=today - timedelta(days=30)) - } - for inc in income_list: - d = str(inc.date) - daily_stats.append({ - 'date': d, - 'income': float(inc.total_amount), - 'income_count': inc.total_count, - 'payout': payout_dict.get(d, 0.00), - 'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2), - }) + 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'), + ) + } + 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), + }) return { 'club_id': club_id, diff --git a/jituan/services/szjilu_accounting.py b/jituan/services/szjilu_accounting.py index 79a799e..6de87e6 100644 --- a/jituan/services/szjilu_accounting.py +++ b/jituan/services/szjilu_accounting.py @@ -3,6 +3,7 @@ import logging from decimal import Decimal from django.db import IntegrityError, transaction +from django.db.utils import OperationalError, ProgrammingError from config.models import PlatformIncomeLog, Szjilu from jituan.constants import CLUB_ID_DEFAULT @@ -18,6 +19,8 @@ _SZJILU_DEFAULTS = { 'DailyFlow': _ZERO, } +_PLATFORM_LOG_ERRORS = (ProgrammingError, OperationalError) + def normalize_szjilu_club_id(club_id): return (club_id or '').strip() or CLUB_ID_DEFAULT @@ -43,17 +46,27 @@ def apply_szjilu_income(amount, club_id=None): szjilu.save() +def _apply_income_stats(amount, club_id): + """写入 daily_income_stat + szjilu(核心统计,必须成功)。""" + from orders.utils import update_daily_income + + cid = normalize_szjilu_club_id(club_id) + jine = Decimal(str(amount)) + update_daily_income(jine, cid) + apply_szjilu_income(jine, cid) + + def record_wechat_income_once(amount, club_id=None, biz_ref=None): """ 微信入账:同一 biz_ref 只记一次 daily_income_stat + szjilu。 - 返回 True=本次新入账;False=已记过(重复回调/重复代码路径)。 + 返回 True=本次新入账;False=已记过(重复回调)。 + 若 platform_income_log 表不可用,仍记入收入(与改造前一致,不能因幂等表故障丢统计)。 """ ref = (biz_ref or '').strip() if not ref: raise ValueError('biz_ref 不能为空') cid = normalize_szjilu_club_id(club_id) jine = Decimal(str(amount)) - from orders.utils import update_daily_income try: with transaction.atomic(): @@ -65,12 +78,28 @@ def record_wechat_income_once(amount, club_id=None, biz_ref=None): except IntegrityError: logger.info('平台入账已存在,跳过重复记账 biz_ref=%s', ref) return False + except _PLATFORM_LOG_ERRORS as exc: + logger.warning( + 'platform_income_log 不可用(%s),跳过幂等直接记入收入 biz_ref=%s', + exc, ref, + ) + _apply_income_stats(jine, cid) + return True + except Exception as exc: + logger.error( + 'platform_income_log 写入异常,仍记入收入 biz_ref=%s err=%s', + ref, exc, exc_info=True, + ) + _apply_income_stats(jine, cid) + return True try: - update_daily_income(jine, cid) - apply_szjilu_income(jine, cid) + _apply_income_stats(jine, cid) except Exception: - PlatformIncomeLog.objects.filter(biz_ref=ref).delete() + try: + PlatformIncomeLog.objects.filter(biz_ref=ref).delete() + except Exception: + pass raise logger.info('平台入账记账成功 biz_ref=%s club=%s amount=%s', ref, cid, jine) return True diff --git a/orders/views.py b/orders/views.py index 60db18a..50f37e5 100644 --- a/orders/views.py +++ b/orders/views.py @@ -307,11 +307,14 @@ class WechatPayNotifyView(APIView): # ============================================= - # 更新收支记录(总累计) - from jituan.services.szjilu_accounting import record_wechat_income_once - record_wechat_income_once( - dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{out_trade_no}', - ) + # 更新收支记录(总累计);统计失败不得回滚订单支付 + try: + from jituan.services.szjilu_accounting import record_wechat_income_once + record_wechat_income_once( + dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{out_trade_no}', + ) + except Exception as e: + logger.error(f'订单收支统计失败 {out_trade_no}: {e}', exc_info=True) # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) order_info = { @@ -569,12 +572,15 @@ class PaymentVerifyView(APIView): # 更新收支记录 - from jituan.services.szjilu_accounting import record_wechat_income_once - record_wechat_income_once( - dingdan.Amount, - getattr(dingdan, 'ClubID', None), - biz_ref=f'order:{dingdan.OrderID}', - ) + try: + from jituan.services.szjilu_accounting import record_wechat_income_once + record_wechat_income_once( + dingdan.Amount, + getattr(dingdan, 'ClubID', None), + biz_ref=f'order:{dingdan.OrderID}', + ) + except Exception as e: + logger.error(f'订单收支统计失败 {dingdan.OrderID}: {e}', exc_info=True) def _query_wechat_payment(self, out_trade_no): """