财务接口按俱乐部过滤,日收支只读daily_income_stat与daily_payout_stat

This commit is contained in:
XingQue
2026-06-24 22:22:12 +08:00
parent 8e5db24bad
commit 7f2f0caaa1
6 changed files with 260 additions and 133 deletions

View File

@@ -6939,23 +6939,27 @@ class CaiwuView(APIView):
today_start = datetime.combine(today, datetime.min.time()) today_start = datetime.combine(today, datetime.min.time())
today_end = today_start + timedelta(days=1) today_end = today_start + timedelta(days=1)
# 四个板子 + 累计:全平台 daily_income_stat / daily_payout_stat 按 date 汇总(不按俱乐部缩范围) from jituan.services.club_context import (
today_inc = DailyIncomeStat.query.filter(date=today).aggregate( filter_club_char_field,
total_amount=Sum('total_amount'), filter_queryset_by_club,
total_count=Sum('total_count'), resolve_club_id_from_request,
resolve_club_scope,
)
from jituan.services.daily_finance_display import (
cumulative_income_payout,
daily_stats_for_request,
sum_payout_stat_for_date,
today_income_for_dashboard,
) )
today_income_amount = float(today_inc['total_amount'] or 0.00)
today_income_count = int(today_inc['total_count'] or 0)
today_pay = DailyPayoutStat.query.filter(date=today).aggregate( order_qs = filter_queryset_by_club(Order.query.all(), request, club_field='ClubID')
total_amount=Sum('total_amount'),
total_count=Sum('total_count'), # 四个板子:按 Header 俱乐部范围读 daily_income_stat / daily_payout_stat
) today_income_amount, today_income_count = today_income_for_dashboard(today, request)
today_payout_amount = float(today_pay['total_amount'] or 0.00) today_payout_amount, today_payout_count = sum_payout_stat_for_date(today, request)
today_payout_count = int(today_pay['total_count'] or 0)
# 今日订单(已付款及之后的状态) # 今日订单(已付款及之后的状态)
today_orders = Order.query.filter( today_orders = order_qs.filter(
CreateTime__gte=today_start, CreateTime__gte=today_start,
CreateTime__lt=today_end, CreateTime__lt=today_end,
Status__in=[1,2,3,4,5,6,7,8] Status__in=[1,2,3,4,5,6,7,8]
@@ -6982,7 +6986,8 @@ class CaiwuView(APIView):
today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('Amount'))['total'] or 0.00) today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('Amount'))['total'] or 0.00)
# 今日充值会员充值leixing=1, zhuangtai=3 # 今日充值会员充值leixing=1, zhuangtai=3
today_chongzhi = Czjilu.query.filter( cz_qs = filter_club_char_field(Czjilu.query.all(), request, field='club_id')
today_chongzhi = cz_qs.filter(
CreateTime__gte=today_start, CreateTime__gte=today_start,
CreateTime__lt=today_end, CreateTime__lt=today_end,
leixing=1, leixing=1,
@@ -6991,8 +6996,9 @@ class CaiwuView(APIView):
today_chongzhi_count = today_chongzhi.count() today_chongzhi_count = today_chongzhi.count()
today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00) today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00)
# 今日新增会员:从购买记录表统计今天创建的用户(去重) # 今日新增会员
new_huiyuan_user_ids = Huiyuangoumai.query.filter( hy_qs = filter_club_char_field(Huiyuangoumai.query.all(), request, field='club_id')
new_huiyuan_user_ids = hy_qs.filter(
CreateTime__gte=today_start, CreateTime__gte=today_start,
CreateTime__lt=today_end CreateTime__lt=today_end
).values_list('yonghu_id', flat=True).distinct() ).values_list('yonghu_id', flat=True).distinct()
@@ -7001,7 +7007,7 @@ class CaiwuView(APIView):
# 新增会员充值总额(这些用户今天的会员充值金额) # 新增会员充值总额(这些用户今天的会员充值金额)
today_new_huiyuan_amount = 0.0 today_new_huiyuan_amount = 0.0
if new_huiyuan_user_ids: if new_huiyuan_user_ids:
amount_sum = Czjilu.query.filter( amount_sum = cz_qs.filter(
CreateTime__gte=today_start, CreateTime__gte=today_start,
CreateTime__lt=today_end, CreateTime__lt=today_end,
yonghuid__in=new_huiyuan_user_ids, yonghuid__in=new_huiyuan_user_ids,
@@ -7016,49 +7022,17 @@ class CaiwuView(APIView):
all_zuzhang_yue = float(UserZuzhang.query.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00) 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) 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, total_payout, platform_profit = cumulative_income_payout(request)
total_payout = float(DailyPayoutStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00)
platform_profit = round(total_income - total_payout, 2)
# 近30天每日收支明细全平台 daily_stats = daily_stats_for_request(request, days=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),
}
for row in DailyIncomeStat.query.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 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': inc['income'],
'income_count': inc['income_count'],
'payout': payout_amt,
'profit': round(inc['income'] - payout_amt, 2),
})
return Response({ return Response({
'code': 0, 'code': 0,
'msg': 'ok', 'msg': 'ok',
'data': { 'data': {
'club_id': resolve_club_id_from_request(request),
'scope': resolve_club_scope(request),
'today_income': round(today_income_amount, 2), 'today_income': round(today_income_amount, 2),
'today_income_count': today_income_count, 'today_income_count': today_income_count,
'today_payout': round(today_payout_amount, 2), 'today_payout': round(today_payout_amount, 2),

View File

@@ -0,0 +1,29 @@
"""只修正今日 daily_income_stat.total_count不动金额、不动其它天"""
from datetime import date
from django.core.management.base import BaseCommand
from jituan.services.szjilu_accounting import fix_income_stat_count_for_date
class Command(BaseCommand):
help = '修正今日收入笔数repair 会把 total_count 虚增,本命令只改笔数列)'
def add_arguments(self, parser):
parser.add_argument('--date', type=str, default='', help='YYYY-MM-DD默认今天')
parser.add_argument('--yes', action='store_true')
def handle(self, *args, **options):
stat_date = date.fromisoformat(options['date']) if options['date'] else date.today()
dry_run = not options['yes']
r = fix_income_stat_count_for_date(stat_date, dry_run=dry_run)
self.stdout.write(f'日期: {r["date"]}')
self.stdout.write(f' 表内笔数合计(错): {r["before_count"]}')
self.stdout.write(self.style.SUCCESS(f' 修正后笔数: {r["after_count"]}'))
if r['clubs']:
for cid, cnt in r['clubs'].items():
self.stdout.write(f' club={cid}: {cnt}')
if dry_run:
self.stdout.write(self.style.WARNING('\n确认: python manage.py fix_today_income_count --yes'))
else:
self.stdout.write(self.style.SUCCESS('\n已写入 daily_income_stat.total_count请刷新财务页。'))

View File

@@ -9,16 +9,19 @@ from jituan.services.club_context import (
resolve_club_id_from_request, resolve_club_id_from_request,
resolve_club_scope, resolve_club_scope,
) )
from jituan.constants import DATA_SCOPE_ALL from jituan.services.daily_finance_display import (
cumulative_income_payout,
daily_stats_for_request,
sum_payout_stat_for_date,
today_income_for_dashboard,
)
from config.models import DailyIncomeStat, DailyPayoutStat
from orders.models import Order from orders.models import Order
from products.models import Czjilu, Huiyuangoumai from products.models import Czjilu, Huiyuangoumai
from users.models import UserDashou, UserGuanshi, UserShangjia, UserZuzhang from users.models import UserDashou, UserGuanshi, UserShangjia, UserZuzhang
def _role_profile_qs(profile_model, request): def _role_profile_qs(profile_model, request):
"""按俱乐部过滤角色扩展表(通过 user.ClubID"""
qs = profile_model.query.all() qs = profile_model.query.all()
if resolve_club_scope(request) == DATA_SCOPE_ALL: if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs return qs
@@ -43,16 +46,8 @@ def _huiyuangoumai_qs(request):
return filter_club_char_field(Huiyuangoumai.query.all(), request, field='club_id') 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')
def build_caiwu_payload(request): def build_caiwu_payload(request):
"""与 CaiwuView 返回结构一致;收入/支出只读 daily_income_stat、daily_payout_stat。""" """与 CaiwuView 一致;按 Header 俱乐部范围读 daily_income_stat、daily_payout_stat。"""
today = date.today() today = date.today()
today_start = datetime.combine(today, datetime.min.time()) today_start = datetime.combine(today, datetime.min.time())
today_end = today_start + timedelta(days=1) today_end = today_start + timedelta(days=1)
@@ -61,24 +56,9 @@ def build_caiwu_payload(request):
order_qs = _order_qs(request) order_qs = _order_qs(request)
cz_qs = _czjilu_qs(request) cz_qs = _czjilu_qs(request)
hy_qs = _huiyuangoumai_qs(request) hy_qs = _huiyuangoumai_qs(request)
payout_qs = _daily_payout_qs(request)
scope = resolve_club_scope(request) today_income_amount, today_income_count = today_income_for_dashboard(today, request)
today_payout_amount, today_payout_count = sum_payout_stat_for_date(today, request)
# 四个板子:全平台合计(与 /houtai/caiwu 一致,读 date 字段)
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_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_qs.filter( today_orders = order_qs.filter(
CreateTime__gte=today_start, CreateTime__gte=today_start,
@@ -127,50 +107,8 @@ def build_caiwu_payload(request):
all_zuzhang_yue = _sum_role_balance(UserZuzhang, 'ketixian_jine', request) all_zuzhang_yue = _sum_role_balance(UserZuzhang, 'ketixian_jine', request)
all_shangjia_yue = _sum_role_balance(UserShangjia, 'yue', request) all_shangjia_yue = _sum_role_balance(UserShangjia, 'yue', request)
total_income = float( total_income, total_payout, platform_profit = cumulative_income_payout(request)
DailyIncomeStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00 daily_stats = daily_stats_for_request(request, days=30)
)
total_payout = float(
DailyPayoutStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00
)
scoped_income_qs = _daily_income_qs(request)
scoped_payout_qs = _daily_payout_qs(request)
scoped_total_income = float(scoped_income_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00)
scoped_total_payout = float(scoped_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)
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'),
)
}
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': inc['income'],
'income_count': inc['income_count'],
'payout': payout_amt,
'profit': round(inc['income'] - payout_amt, 2),
})
return { return {
'club_id': club_id, 'club_id': club_id,

View File

@@ -0,0 +1,137 @@
"""财务看板:只读 daily_income_stat / daily_payout_stat支持按俱乐部 Header 过滤。"""
from datetime import date, timedelta
from decimal import Decimal
from django.db.models import Sum
from config.models import DailyIncomeStat, DailyPayoutStat
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
from jituan.services.club_context import filter_club_char_field, resolve_club_scope
from jituan.services.szjilu_accounting import (
collect_today_live_wechat_payments,
normalize_szjilu_club_id,
)
def _income_qs(request):
if request is None:
return DailyIncomeStat.objects.all()
return filter_club_char_field(DailyIncomeStat.objects.all(), request, field='club_id')
def _payout_qs(request):
if request is None:
return DailyPayoutStat.objects.all()
return filter_club_char_field(DailyPayoutStat.objects.all(), request, field='club_id')
def _payment_matches_request(club_id, request):
if request is None or resolve_club_scope(request) == DATA_SCOPE_ALL:
return True
from jituan.services.club_context import resolve_club_id_from_request
cid = resolve_club_id_from_request(request)
ic = normalize_szjilu_club_id(club_id)
if cid == CLUB_ID_DEFAULT:
return ic == CLUB_ID_DEFAULT
return ic == cid
def sum_income_stat_for_date(stat_date, request=None):
amount = Decimal('0')
count = 0
for row in _income_qs(request).filter(date=stat_date).iterator():
amount += Decimal(str(row.total_amount or 0))
count += int(row.total_count or 0)
return float(amount), count
def sum_payout_stat_for_date(stat_date, request=None):
amount = Decimal('0')
count = 0
for row in _payout_qs(request).filter(date=stat_date).iterator():
amount += Decimal(str(row.total_amount or 0))
count += int(row.total_count or 0)
return float(amount), count
def count_live_income_payments(stat_date, request=None):
"""当日真实微信支付笔数(去重),可按俱乐部过滤。"""
items = collect_today_live_wechat_payments(stat_date)
if request is None:
return len(items)
return sum(
1 for _ref, cid, _jine, _kind in items
if _payment_matches_request(cid, request)
)
def today_income_for_dashboard(stat_date=None, request=None):
"""
今日收入:金额与笔数均来自 daily_income_stat按俱乐部过滤
笔数若被 repair 虚增,用当日真实支付笔数校正。
"""
stat_date = stat_date or date.today()
amount, table_count = sum_income_stat_for_date(stat_date, request)
live_count = count_live_income_payments(stat_date, request)
if live_count > 0 and table_count > live_count:
display_count = live_count
else:
display_count = table_count
return amount, display_count
def cumulative_income_payout(request=None):
"""累计收入/支出/利润(按当前俱乐部视图范围)。"""
total_income = float(
_income_qs(request).aggregate(t=Sum('total_amount'))['t'] or 0
)
total_payout = float(
_payout_qs(request).aggregate(t=Sum('total_amount'))['t'] or 0
)
return total_income, total_payout, round(total_income - total_payout, 2)
def daily_stats_for_request(request=None, days=30):
"""近 N 天每日收支(按俱乐部视图)。"""
since = date.today() - timedelta(days=days)
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(request).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(request).filter(date__gte=since).values('date').annotate(
total_amount=Sum('total_amount'),
)
}
daily_stats = []
all_dates = sorted(
set(income_by_date.keys()) | set(payout_by_date.keys()),
reverse=True,
)[:days]
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 daily_stats
def income_count_for_summary_date(stat_date, request=None):
if stat_date == date.today():
_, count = today_income_for_dashboard(stat_date, request)
return count
_, count = sum_income_stat_for_date(stat_date, request)
return count

View File

@@ -1120,3 +1120,46 @@ def repopulate_today_income_stats(stat_date=None, dry_run=True):
) )
return result return result
def fix_income_stat_count_for_date(stat_date=None, dry_run=True):
"""
只修正 daily_income_stat.total_count不动 total_amount、不动其它天
repair 会把笔数虚增到 100+;按当日真实支付笔数写回。
"""
from collections import defaultdict
stat_date = stat_date or date.today()
items = collect_today_live_wechat_payments(stat_date)
by_club = defaultdict(int)
for _ref, cid, _jine, _kind in items:
by_club[cid] += 1
before_rows = list(DailyIncomeStat.objects.filter(date=stat_date))
before_count = sum(int(r.total_count or 0) for r in before_rows)
after_count = sum(by_club.values())
result = {
'date': str(stat_date),
'before_count': before_count,
'after_count': after_count,
'clubs': dict(by_club),
'dry_run': dry_run,
}
if dry_run:
return result
with transaction.atomic():
seen_clubs = set()
for cid, cnt in by_club.items():
seen_clubs.add(cid)
row = DailyIncomeStat.objects.filter(date=stat_date, club_id=cid).first()
if row:
row.total_count = cnt
row.save(update_fields=['total_count'])
for row in before_rows:
if row.club_id not in seen_clubs:
row.total_count = 0
row.save(update_fields=['total_count'])
return result

View File

@@ -10,6 +10,11 @@ from jituan.services.club_context import (
resolve_club_id_from_request, resolve_club_id_from_request,
resolve_club_scope, resolve_club_scope,
) )
from jituan.services.daily_finance_display import (
sum_income_stat_for_date,
sum_payout_stat_for_date,
today_income_for_dashboard,
)
def _month_bounds(y, m): def _month_bounds(y, m):
@@ -59,12 +64,13 @@ def build_szxx_payload(request, granularity, year, month, summary_date):
if not summary_date: if not summary_date:
summary_date = now.strftime('%Y-%m-%d') summary_date = now.strftime('%Y-%m-%d')
summary_d = date.fromisoformat(str(summary_date)[:10]) summary_d = date.fromisoformat(str(summary_date)[:10])
income_summary = income_base.filter(date=summary_d).aggregate( if summary_d == now:
total_income=Sum('total_amount'), total_count=Sum('total_count'), inc_amount, inc_count = today_income_for_dashboard(summary_d, request)
) else:
payout_summary = payout_base.filter(date=summary_d).aggregate( inc_amount, inc_count = sum_income_stat_for_date(summary_d, request)
total_payout=Sum('total_amount'), total_count=Sum('total_count'), pay_amount, pay_count = sum_payout_stat_for_date(summary_d, request)
) income_summary = {'total_income': inc_amount, 'total_count': inc_count}
payout_summary = {'total_payout': pay_amount, 'total_count': pay_count}
def _fmt_day(key): def _fmt_day(key):
return f'{year}-{month:02d}-{int(key):02d}' return f'{year}-{month:02d}-{int(key):02d}'