财务恢复:只读日统计表,废弃szjilu,新增restore_finance_stats

This commit is contained in:
XingQue
2026-06-24 21:50:40 +08:00
parent 8c271f639a
commit 79f2d4f020
8 changed files with 191 additions and 425 deletions

View File

@@ -6939,14 +6939,13 @@ class CaiwuView(APIView):
today_start = datetime.combine(today, datetime.min.time())
today_end = today_start + timedelta(days=1)
from jituan.services.live_income_stats import (
income_daily_stats_last_days,
income_for_date,
income_total_all_time,
# 今日收入 / 支出(全平台各俱乐部合计,只读 daily_income_stat / daily_payout_stat
today_inc = DailyIncomeStat.query.filter(date=today).aggregate(
total_amount=Sum('total_amount'),
total_count=Sum('total_count'),
)
# 今日收入:从真实微信支付汇总,不读 daily_income_stat
today_income_amount, today_income_count = income_for_date(today)
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'),
@@ -7017,20 +7016,23 @@ 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, _ = income_total_all_time()
# 平台总收支及利润(只读日统计表
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)
platform_profit = round(total_income - total_payout, 2)
# 近30天每日收支明细
# 近30天每日收支明细(按日期合并各俱乐部)
daily_stats = []
since = today - timedelta(days=30)
income_by_date = {
row['date']: {
'income': row['income'],
'income_count': row['income_count'],
str(row['date']): {
'income': float(row['total_amount'] or 0),
'income_count': int(row['total_count'] or 0),
}
for row in income_daily_stats_last_days(30)
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)
@@ -7406,15 +7408,14 @@ class SzxxView(APIView):
else:
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
# ---------- 收入:从真实支付汇总;支出仍读 daily_payout_stat ----------
from jituan.services.live_income_stats import (
income_summary_filter,
income_time_series,
)
# ---------- 收入分组查询daily_income_stat----------
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)
income_map = income_time_series(granularity, year, month, request)
# ---------- 支出分组查询 ----------
# ---------- 支出分组查询daily_payout_stat----------
payout_qs = filter_queryset_by_club(
DailyPayoutStat.query.filter(**payout_filter), request,
).values(group_field) \
@@ -7423,12 +7424,13 @@ class SzxxView(APIView):
# ---------- 合并数据 ----------
data_map = {}
for key, vals in income_map.items():
for item in income_qs:
key = item[group_field]
data_map[key] = {
'income': vals['income'],
'income_count': vals.get('income_count', 0),
'income': float(item['total_income'] or 0),
'income_count': item['total_count'] or 0,
'payout': 0.0,
'payout_count': 0,
'payout_count': 0
}
for item in payout_qs:
@@ -7457,25 +7459,17 @@ class SzxxView(APIView):
time_series.sort(key=lambda x: x['date'])
# ---------- 汇总数据 ----------
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,
)
income_summary = filter_queryset_by_club(
DailyIncomeStat.query.filter(**summary_filter), request,
).aggregate(
total_income=Sum('total_amount'), total_count=Sum('total_count')
)
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)
@@ -7486,7 +7480,7 @@ class SzxxView(APIView):
'time_series': time_series,
'summary': {
'total_income': total_income,
'total_income_count': total_income_count,
'total_income_count': income_summary['total_count'] or 0,
'total_payout': total_payout,
'total_payout_count': payout_summary['total_count'] or 0,
'total_profit': total_profit

View File

@@ -0,0 +1,64 @@
"""
一键恢复财务「每日收入」统计repair_wechat_income 搞乱后执行)。
只重建:
- daily_income_stat每日收入统计表财务看板读这个
- platform_income_log支付幂等日志
绝不碰:
- daily_payout_stat每日支出repair 没动过)
- szjilu收支记录表已废弃
- 订单 / 充值业务表
用法:
预览python manage.py restore_finance_stats
执行python manage.py restore_finance_stats --yes
"""
from datetime import date
from django.core.management.base import BaseCommand
from jituan.services.szjilu_accounting import restore_finance_daily_stats
class Command(BaseCommand):
help = '从订单/充值重建 daily_income_stat恢复 repair 搞乱的每日收入)'
def add_arguments(self, parser):
parser.add_argument('--since-year', type=int, default=2020, 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 2020
dry_run = not options['yes']
if dry_run:
self.stdout.write(self.style.WARNING(
'【预览】未改数据库。下面「恢复后」应是正确每日收入合计。\n'
))
else:
self.stdout.write(self.style.WARNING(
'【执行】正在从订单/充值重建 daily_income_stat...\n'
))
r = restore_finance_daily_stats(since_year, until_year, dry_run=dry_run)
self.stdout.write(f'范围: {r["since_year"]} ~ {r["until_year"]}')
self.stdout.write(f' 支付笔数: {r["after_count"]}')
self.stdout.write(f' 恢复前 daily 合计: {r["before_daily_total"]:.2f}')
self.stdout.write(self.style.SUCCESS(
f' 恢复后 daily 合计: {r.get("after_daily_total", r["after_total"]):.2f}'
))
self.stdout.write(f' 其中今日收入: {r["today_amount"]:.2f} 元 / {r["today_count"]}')
if dry_run:
self.stdout.write(self.style.WARNING(
'\n数字对的话执行:\n'
' python manage.py restore_finance_stats --yes'
))
else:
self.stdout.write(self.style.SUCCESS(
'\n完成。daily_payout_stat 未动。请 push 代码后刷新财务页。'
))

View File

@@ -1,12 +0,0 @@
"""按真实支付重算 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 记录'))

View File

@@ -59,6 +59,13 @@ class Tupianpeizhi(QModel):
class Szjilu(QModel):
"""
【已废弃 · 禁止使用】
本表与财务看板「每日收入 / 每日支出」无关。
财务四个板子只读 daily_income_stat、daily_payout_stat不得读写本表。
repair_wechat_income 曾误写本表导致数据膨胀,后续入账也不再更新本表。
"""
id = models.AutoField(primary_key=True, verbose_name='记录ID')
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
TotalIncome = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总收益')
@@ -287,8 +294,8 @@ class AccountPermission(QModel):
class DailyIncomeStat(QModel):
"""
每日收入统计表
记录平台每天通过微信支付获得的总收入和总笔数
每日收入统计表 —— 财务看板「今日收入 / 累计收入 / 每日明细」唯一数据源。
微信支付成功时由 update_daily_income 写入;勿读 szjilu 表。
"""
# 日期相关字段(便于快速筛选和分组)
date = models.DateField(verbose_name='统计日期', db_index=True)
@@ -328,8 +335,7 @@ class DailyIncomeStat(QModel):
class DailyPayoutStat(QModel):
"""
每日出款统计表
记录平台每天通过提现/结算等方式的出款总额和总笔数
每日出款统计表 —— 财务看板「今日支出 / 累计支出」唯一数据源。
"""
date = models.DateField(verbose_name='统计日期', db_index=True)
club_id = models.CharField(
@@ -363,7 +369,7 @@ class DailyPayoutStat(QModel):
class PlatformIncomeLog(QModel):
"""微信入账幂等日志:同一 biz_ref 只记一次日收入与 szjilu"""
"""微信入账幂等日志:同一 biz_ref 只记一次 daily_income_stat"""
biz_ref = models.CharField(max_length=64, unique=True, db_index=True, verbose_name='业务单号')
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
amount = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='入账金额')

View File

@@ -1,7 +1,5 @@
"""俱乐部维度财务统计(供 /jituan/houtai/caiwu旧 /houtai/caiwu 不变)。"""
from calendar import monthrange
from datetime import date, datetime, timedelta
from decimal import Decimal
from django.db.models import Sum
@@ -12,13 +10,8 @@ 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 DailyPayoutStat, Szjilu
from config.models import DailyIncomeStat, DailyPayoutStat
from orders.models import Order
from products.models import Czjilu, Huiyuangoumai
from users.models import UserDashou, UserGuanshi, UserShangjia, UserZuzhang
@@ -38,27 +31,6 @@ def _sum_role_balance(profile_model, balance_field, request):
)
def _build_szjilu_payload(request):
"""全局收支流水表 szjilu按俱乐部或集团汇总"""
if resolve_club_scope(request) == DATA_SCOPE_ALL:
agg = Szjilu.query.aggregate(
zongshouyi=Sum('TotalIncome'),
zongliushui=Sum('TotalFlow'),
zongzhichu=Sum('TotalExpense'),
jrls=Sum('DailyFlow'),
jrzc=Sum('DailyExpense'),
)
return {
'zongliushui': float(agg['zongliushui'] or 0),
'zongshouyi': float(agg['zongshouyi'] or 0),
'zongzhichu': float(agg['zongzhichu'] or 0),
'jrls': float(agg['jrls'] or 0),
'jrzc': float(agg['jrzc'] or 0),
}
from jituan.services.szjilu_accounting import get_szjilu_snapshot
return get_szjilu_snapshot(resolve_club_id_from_request(request))
def _order_qs(request):
return filter_queryset_by_club(Order.query.all(), request, club_field='ClubID')
@@ -71,12 +43,16 @@ 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')
def build_caiwu_payload(request):
"""与 CaiwuView 返回结构一致,但按俱乐部过滤订单/充值流水"""
"""与 CaiwuView 返回结构一致;收入/支出只读 daily_income_stat、daily_payout_stat"""
today = date.today()
today_start = datetime.combine(today, datetime.min.time())
today_end = today_start + timedelta(days=1)
@@ -85,11 +61,19 @@ 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)
today_income_amount, today_income_count = income_for_date(today, request)
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'),
@@ -145,50 +129,69 @@ def build_caiwu_payload(request):
all_zuzhang_yue = _sum_role_balance(UserZuzhang, 'ketixian_jine', request)
all_shangjia_yue = _sum_role_balance(UserShangjia, 'yue', request)
szjilu_payload = _build_szjilu_payload(request)
total_income, _ = income_total_all_time()
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, _ = income_total_all_time(request)
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)
income_by_date = {
row['date']: {
'income': row['income'],
'income_count': row['income_count'],
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'),
)
}
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),
})
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,
'scope': resolve_club_scope(request),
'szjilu': szjilu_payload,
'today_income': round(today_income_amount, 2),
'today_income_count': today_income_count,
'today_payout': round(today_payout_amount, 2),

View File

@@ -1,264 +0,0 @@
"""
每日收入:从真实微信支付订单/充值单实时汇总。
不读 daily_income_stat该表可被 repair 搞乱,与展示解耦)。
每日支出:仍读 daily_payout_statrepair 未动支出)。
收支记录 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

View File

@@ -1,4 +1,4 @@
"""各俱乐部全局收支流水szjilu统一记账"""
"""【szjilu 已废弃】财务入账只写 daily_income_stat本模块保留幂等日志与恢复命令"""
import logging
from datetime import date
from decimal import Decimal
@@ -203,13 +203,12 @@ def zero_all_income_statistics():
def _apply_income_stats(amount, club_id):
""" daily_income_stat + szjilu仅在新 platform_income_log 落库后调用)"""
"""写 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):
@@ -783,7 +782,7 @@ def collect_legitimate_wechat_payments(since_year, until_year):
f'order:{od.OrderID}',
getattr(od, 'ClubID', None),
od.Amount,
od.CreateTime,
od.UpdateTime or od.CreateTime,
'order',
)
@@ -797,7 +796,7 @@ def collect_legitimate_wechat_payments(since_year, until_year):
f'cz:{cz.dingdan_id}',
getattr(cz, 'club_id', None),
cz.jine,
cz.CreateTime,
cz.UpdateTime or cz.CreateTime,
f'cz_{cz.leixing}',
)
@@ -882,33 +881,20 @@ def rebuild_income_stats_from_scratch(since_year=None, until_year=None, dry_run=
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 restore_finance_daily_stats(since_year=None, until_year=None, dry_run=True):
"""
一键恢复财务每日收入:从订单/充值重建 daily_income_stat + platform_income_log。
不碰 daily_payout_stat不碰 szjilu收支记录表已废弃
"""
return rebuild_income_stats_from_scratch(since_year, until_year, dry_run=dry_run)
def collect_today_live_wechat_payments(stat_date=None):
"""今日实时微信支付(含当日下单或当日完成支付的订单/充值)。"""
from datetime import datetime, timedelta
@@ -998,7 +984,6 @@ def restore_daily_income_from_orders(since_year=None, until_year=None, dry_run=T
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

View File

@@ -362,13 +362,8 @@ class WechatPayNotifyView(APIView):
logger.error(traceback.format_exc())
def _update_szjilu(self, jine, club_id=None):
"""更新收支记录表(按俱乐部)"""
from jituan.services.szjilu_accounting import apply_szjilu_income
try:
apply_szjilu_income(jine, club_id)
logger.info(f"收支记录更新成功,增加金额: {jine}, club={club_id}")
except Exception as e:
logger.error(f"更新收支记录失败: {e}")
"""szjilu 已废弃,财务只写 daily_income_stat此处不再操作。"""
pass
def _broadcast_async(self, dingdan):
"""异步广播通知"""
@@ -632,13 +627,8 @@ class PaymentVerifyView(APIView):
raise Exception(f"微信订单查询失败: {err_msg}")
def _update_szjilu(self, jine, club_id=None):
"""更新收支记录表(按俱乐部)"""
from jituan.services.szjilu_accounting import apply_szjilu_income
try:
apply_szjilu_income(jine, club_id)
logger.info(f"收支记录更新成功,增加金额: {jine}, club={club_id}")
except Exception as e:
logger.error(f"更新收支记录失败: {e}")
"""szjilu 已废弃,财务只写 daily_income_stat此处不再操作。"""
pass
# 不抛出异常,因为收支记录不是核心流程,但记录错误供排查
def _generate_nonce_str(self):