fix: 财务接口+恢复今日收入

This commit is contained in:
XingQue
2026-06-24 21:35:55 +08:00
parent 84665e0c5d
commit b1fbc3cd8f
5 changed files with 425 additions and 26 deletions

View File

@@ -0,0 +1,70 @@
"""
【终极恢复】清空全部收支脏数据,按真实微信支付逐笔重建。
只执行这一条,不要再用 repair / undo / fix
预览python manage.py rebuild_income_stats
执行python manage.py rebuild_income_stats --yes
"""
from datetime import date
from django.core.management.base import BaseCommand
from django.db.models import Sum
from config.models import DailyIncomeStat
from jituan.services.szjilu_accounting import rebuild_income_stats_from_scratch
class Command(BaseCommand):
help = '清空收支脏数据,按真实微信支付逐笔重建(终极恢复)'
def add_arguments(self, parser):
parser.add_argument('--since-year', type=int, default=0, help='从哪一年扫,默认去年')
parser.add_argument('--until-year', type=int, default=0, help='到哪一年,默认今年')
parser.add_argument('--yes', action='store_true', help='确认执行')
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.ERROR(
'【执行】删除全部收入日志和日统计,按真实支付重建...\n'
))
r = rebuild_income_stats_from_scratch(
since_year=since_year, until_year=until_year, dry_run=dry_run,
)
self.stdout.write(f'=== {since_year} ~ {until_year} ===')
self.stdout.write(f' 错乱前 daily 合计: {r["before_daily_total"]:.2f}')
self.stdout.write(f' 错乱前 log: {r["before_log_total"]:.2f} 元 / {r["before_log_count"]}')
self.stdout.write(self.style.SUCCESS(
f' 重建后合计: {r["after_total"]:.2f} 元 / {r["after_count"]}'
))
self.stdout.write(self.style.SUCCESS(
f' 重建后本月: {r["month_amount"]:.2f}'
))
self.stdout.write(self.style.SUCCESS(
f' 重建后今日: {r["today_amount"]:.2f} 元 / {r["today_count"]} 笔 ← 看这条'
))
if not dry_run:
self.stdout.write(self.style.SUCCESS(
f'\ndaily 表合计: {r.get("after_daily_total", 0):.2f}'
))
agg = DailyIncomeStat.objects.filter(date=date.today()).aggregate(
amt=Sum('total_amount'), cnt=Sum('total_count'),
)
self.stdout.write(
f'今日 daily 行合计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)}'
)
self.stdout.write(self.style.SUCCESS('\n完成。刷新后台财务页。'))
else:
self.stdout.write(self.style.WARNING(
f'\n今日 {r["today_amount"]:.2f} 元对的话:\n'
f' python manage.py rebuild_income_stats --yes'
))

View File

@@ -0,0 +1,48 @@
"""只恢复今日收入(修复后台四个板子显示 0"""
from datetime import date
from django.core.management.base import BaseCommand
from django.db.models import Sum
from config.models import DailyIncomeStat
from jituan.services.szjilu_accounting import repopulate_today_income_stats
class Command(BaseCommand):
help = '重建今日 daily_income_stat修复财务页今日收入为 0'
def add_arguments(self, parser):
parser.add_argument('--date', type=str, default='', help='日期 YYYY-MM-DD默认今天')
parser.add_argument('--yes', action='store_true', help='确认执行')
def handle(self, *args, **options):
stat_date = date.fromisoformat(options['date']) if options['date'] else date.today()
dry_run = not options['yes']
if dry_run:
self.stdout.write(self.style.WARNING('【预览】\n'))
else:
self.stdout.write(self.style.WARNING('【执行】重建今日收入...\n'))
r = repopulate_today_income_stats(stat_date, dry_run=dry_run)
self.stdout.write(f'日期: {r["date"]}')
self.stdout.write(f' 当前: {r["before_amount"]:.2f} 元 / {r["before_count"]}')
self.stdout.write(self.style.SUCCESS(
f' 重建后: {r["after_amount"]:.2f} 元 / {r["after_count"]}'
))
if r['items']:
for ref, cid, amt, kind in r['items']:
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
if not dry_run:
agg = DailyIncomeStat.objects.filter(date=stat_date).aggregate(
amt=Sum('total_amount'), cnt=Sum('total_count'),
)
self.stdout.write(self.style.SUCCESS(
f'\n完成。今日日统计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)}'
))
self.stdout.write('请刷新后台财务页。')
else:
self.stdout.write(self.style.WARNING(
'\n确认后: python manage.py repopulate_today_income --yes'
))

View File

@@ -90,26 +90,20 @@ def build_caiwu_payload(request):
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
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)
today_orders = order_qs.filter(
CreateTime__gte=today_start,
@@ -160,10 +154,17 @@ def build_caiwu_payload(request):
szjilu_payload = _build_szjilu_payload(request)
# 账户总收入/总支出:日收支统计表全历史 SUM与「每日收支详细」按年汇总一致
total_income = float(income_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00)
total_payout = float(payout_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00)
platform_profit = round(total_income - total_payout, 2)
# 账户总收入/总支出:始终全集团合计(切换俱乐部也不变少
total_income = float(
DailyIncomeStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00
)
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_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)

View File

@@ -738,3 +738,283 @@ def reset_all_daily_szjilu():
szjilu.save(update_fields=['DailyExpense', 'DailyFlow', 'UpdateTime'])
updated += 1
return updated
# 合法微信支付收入(不含退款 Status=5
_LEGIT_ORDER_STATUSES = [1, 2, 3, 4, 6, 7, 8]
_LEGIT_CZ_LEIXING = [1, 2, 3, 4, 5]
def collect_legitimate_wechat_payments(since_year, until_year):
"""
从业务单据重建:每笔 biz_ref 全局只出现一次。
返回 [(biz_ref, club_id, amount, stat_date, pay_datetime, kind), ...]
"""
from datetime import datetime
from orders.models import Order
from products.models import Czjilu
start = datetime(since_year, 1, 1)
end = datetime(until_year + 1, 1, 1)
seen = {}
items = []
def _add(ref, club_id, amount, pay_dt, kind):
ref = (ref or '').strip()
if not ref or ref in seen:
return
jine = Decimal(str(amount or 0))
if jine <= 0:
return
cid = normalize_szjilu_club_id(club_id)
if not pay_dt:
pay_dt = datetime.now()
stat_d = pay_dt.date() if hasattr(pay_dt, 'date') else pay_dt
seen[ref] = True
items.append((ref, cid, jine, stat_d, pay_dt, kind))
for od in Order.query.filter(
CreateTime__gte=start,
CreateTime__lt=end,
Status__in=_LEGIT_ORDER_STATUSES,
):
_add(
f'order:{od.OrderID}',
getattr(od, 'ClubID', None),
od.Amount,
od.CreateTime,
'order',
)
for cz in Czjilu.query.filter(
CreateTime__gte=start,
CreateTime__lt=end,
zhuangtai=3,
leixing__in=_LEGIT_CZ_LEIXING,
):
_add(
f'cz:{cz.dingdan_id}',
getattr(cz, 'club_id', None),
cz.jine,
cz.CreateTime,
f'cz_{cz.leixing}',
)
return items
def rebuild_income_stats_from_scratch(since_year=None, until_year=None, dry_run=True):
"""
终极恢复:清空全部收支脏数据 → 按真实微信支付逐笔重建(每笔唯一)。
"""
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)
before = {
'daily': float(DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0),
'logs': 0,
'log_cnt': 0,
}
try:
before['logs'] = float(PlatformIncomeLog.objects.aggregate(t=Sum('amount'))['t'] or 0)
before['log_cnt'] = PlatformIncomeLog.objects.count()
except _PLATFORM_LOG_ERRORS:
pass
items = collect_legitimate_wechat_payments(since_year, until_year)
total_amt = sum((x[2] for x in items), _ZERO)
today = date.today()
today_amt = sum((x[2] for x in items if x[3] == today), _ZERO)
today_cnt = sum(1 for x in items if x[3] == today)
month_amt = sum(
(x[2] for x in items if x[3].year == today.year and x[3].month == today.month),
_ZERO,
)
result = {
'since_year': since_year,
'until_year': until_year,
'before_daily_total': before['daily'],
'before_log_total': before['logs'],
'before_log_count': before['log_cnt'],
'after_total': float(total_amt),
'after_count': len(items),
'today_amount': float(today_amt),
'today_count': today_cnt,
'month_amount': float(month_amt),
'items': items,
'dry_run': dry_run,
}
if dry_run:
return result
with transaction.atomic():
try:
PlatformIncomeLog.objects.all().delete()
except _PLATFORM_LOG_ERRORS as exc:
raise RuntimeError(f'platform_income_log 不可用: {exc}') from exc
DailyIncomeStat.objects.all().delete()
by_day_club = defaultdict(lambda: {'amt': _ZERO, 'cnt': 0})
for ref, cid, jine, stat_d, pay_dt, _kind in items:
log = PlatformIncomeLog.objects.create(biz_ref=ref, club_id=cid, amount=jine)
if pay_dt:
PlatformIncomeLog.objects.filter(pk=log.pk).update(CreateTime=pay_dt)
key = (stat_d, cid)
by_day_club[key]['amt'] += jine
by_day_club[key]['cnt'] += 1
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'],
)
today_d = date.today()
club_ids = set(by_day_club.keys())
club_ids |= {(today_d, row.club_id) for row in Szjilu.objects.all()}
all_clubs = {cid for _, cid in club_ids}
for cid in all_clubs:
total = PlatformIncomeLog.objects.filter(club_id=cid).aggregate(
t=Sum('amount'),
)['t'] or _ZERO
today_flow = PlatformIncomeLog.objects.filter(
club_id=cid, CreateTime__date=today_d,
).aggregate(t=Sum('amount'))['t'] or _ZERO
szjilu, _ = Szjilu.objects.select_for_update().get_or_create(
club_id=cid,
defaults=dict(_SZJILU_DEFAULTS),
)
szjilu.TotalIncome = total
szjilu.TotalFlow = total
szjilu.DailyFlow = today_flow
szjilu.save(update_fields=['TotalIncome', 'TotalFlow', 'DailyFlow', 'UpdateTime'])
result['after_daily_total'] = float(
DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0
)
return result
def collect_today_live_wechat_payments(stat_date=None):
"""今日实时微信支付(含当日下单或当日完成支付的订单/充值)。"""
from datetime import datetime, timedelta
from django.db.models import Q
from orders.models import Order
from products.models import Czjilu
stat_date = stat_date or date.today()
day_start = datetime.combine(stat_date, datetime.min.time())
day_end = day_start + timedelta(days=1)
seen = {}
items = []
def _add(ref, club_id, amount, kind):
ref = (ref or '').strip()
if not ref or ref in seen:
return
jine = Decimal(str(amount or 0))
if jine <= 0:
return
cid = normalize_szjilu_club_id(club_id)
seen[ref] = True
items.append((ref, cid, jine, kind))
for od in Order.query.filter(Status__in=_LEGIT_ORDER_STATUSES).filter(
Q(CreateTime__gte=day_start, CreateTime__lt=day_end)
| Q(UpdateTime__gte=day_start, UpdateTime__lt=day_end),
):
_add(f'order:{od.OrderID}', getattr(od, 'ClubID', None), od.Amount, 'order')
for cz in Czjilu.query.filter(zhuangtai=3, leixing__in=_LEGIT_CZ_LEIXING).filter(
Q(CreateTime__gte=day_start, CreateTime__lt=day_end)
| Q(UpdateTime__gte=day_start, UpdateTime__lt=day_end),
):
_add(f'cz:{cz.dingdan_id}', getattr(cz, 'club_id', None), cz.jine, f'cz_{cz.leixing}')
return items
def repopulate_today_income_stats(stat_date=None, dry_run=True):
"""只重建指定日期的日收入(不删历史其它天),修复今日显示为 0。"""
from collections import defaultdict
from datetime import datetime, time as dt_time
from django.db.models import Sum
from django.utils import timezone
stat_date = stat_date or date.today()
before = DailyIncomeStat.objects.filter(date=stat_date).aggregate(
amt=Sum('total_amount'), cnt=Sum('total_count'),
)
items = collect_today_live_wechat_payments(stat_date)
total = sum((x[2] for x in items), _ZERO)
result = {
'date': str(stat_date),
'before_amount': float(before['amt'] or 0),
'before_count': int(before['cnt'] or 0),
'after_amount': float(total),
'after_count': len(items),
'items': items,
'dry_run': dry_run,
}
if dry_run:
return result
pay_ts = timezone.make_aware(datetime.combine(stat_date, dt_time(12, 0, 0)))
with transaction.atomic():
DailyIncomeStat.objects.filter(date=stat_date).delete()
by_club = defaultdict(lambda: {'amt': _ZERO, 'cnt': 0})
for ref, cid, jine, _kind in items:
if not income_log_exists(ref):
try:
log = PlatformIncomeLog.objects.create(biz_ref=ref, club_id=cid, amount=jine)
PlatformIncomeLog.objects.filter(pk=log.pk).update(CreateTime=pay_ts)
except IntegrityError:
pass
by_club[cid]['amt'] += jine
by_club[cid]['cnt'] += 1
for cid, agg in by_club.items():
DailyIncomeStat.objects.create(
date=stat_date,
club_id=cid,
year=stat_date.year,
month=stat_date.month,
day=stat_date.day,
total_amount=agg['amt'],
total_count=agg['cnt'],
)
today_d = date.today()
for cid, agg in by_club.items():
szjilu, _ = Szjilu.objects.select_for_update().get_or_create(
club_id=cid, defaults=dict(_SZJILU_DEFAULTS),
)
if stat_date == today_d:
szjilu.DailyFlow = agg['amt']
total_all = PlatformIncomeLog.objects.filter(club_id=cid).aggregate(
t=Sum('amount'),
)['t'] or _ZERO
szjilu.TotalIncome = total_all
szjilu.TotalFlow = total_all
szjilu.save(update_fields=['TotalIncome', 'TotalFlow', 'DailyFlow', 'UpdateTime'])
return result

View File

@@ -33,14 +33,14 @@ def get_order_user_id(order):
return None
def update_daily_income(amount, club_id=None):
def update_daily_income(amount, club_id=None, stat_date=None):
"""
原子更新当日收入统计金额累加笔数加1
用于微信支付成功回调。
"""
from jituan.constants import CLUB_ID_DEFAULT
cid = club_id or CLUB_ID_DEFAULT
today = date.today()
today = stat_date or date.today()
year, month, day = today.year, today.month, today.day
with transaction.atomic():