fix: undo_repair_wechat_income 反向撤销重复记账

This commit is contained in:
XingQue
2026-06-24 21:29:19 +08:00
parent 3b7c539a56
commit 84665e0c5d
5 changed files with 558 additions and 19 deletions

View File

@@ -0,0 +1,114 @@
"""
修复 repair_wechat_income 搞乱的收入200 显示成 600+)。
两种模式(先预览,再加 --yes
A. 默认:重扫本月真实微信支付,删掉脏数据后重建
python manage.py fix_income_month
python manage.py fix_income_month --yes
B. 若日志金额已对、只是日统计表脏了:
python manage.py fix_income_month --logs-only --yes
"""
from calendar import monthrange
from datetime import date
from django.core.management.base import BaseCommand
from django.db.models import Sum
from config.models import DailyIncomeStat, PlatformIncomeLog
from jituan.services.szjilu_accounting import (
align_daily_income_from_logs_only,
dedupe_platform_income_logs,
fix_income_period,
)
class Command(BaseCommand):
help = '修复本月收入统计错乱repair 重复记账)'
def add_arguments(self, parser):
parser.add_argument('--year', type=int, default=0, help='年,默认今年')
parser.add_argument('--month', type=int, default=0, help='月,默认本月')
parser.add_argument('--today-only', action='store_true', help='只修今天')
parser.add_argument('--logs-only', action='store_true', help='只按现有日志重写日统计,不重扫订单')
parser.add_argument('--yes', action='store_true', help='确认执行')
def handle(self, *args, **options):
today = date.today()
year = options['year'] or today.year
month = options['month'] or today.month
dry_run = not options['yes']
if options['today_only']:
start = end = today
label = f'今日 {today}'
else:
_, last = monthrange(year, month)
start = date(year, month, 1)
end = date(year, month, last)
label = f'{year}-{month:02d} 全月'
if options['logs_only']:
self._run_logs_only(dry_run, label)
return
self.stdout.write(self.style.WARNING(f'{label}】收入修复\n'))
if dry_run:
self.stdout.write('预览模式(未改库)\n')
r = fix_income_period(start, end, dry_run=dry_run)
self.stdout.write(f' 错乱日统计合计: {r["before_daily_amount"]:.2f} 元 / {r["before_daily_count"]}')
self.stdout.write(f' 错乱日志合计: {r["before_log_amount"]:.2f} 元 / {r["before_log_count"]}')
self.stdout.write(self.style.SUCCESS(
f' 重建后(严格): {r["after_amount"]:.2f} 元 / {r["after_count"]}'
))
if r['items']:
self.stdout.write(' 明细:')
for row in r['items']:
if len(row) == 5:
d, ref, cid, amt, kind = row
self.stdout.write(f' {d} {ref} {amt}元 club={cid} ({kind})')
else:
ref, cid, amt, kind = row
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
if not dry_run:
self._print_today_total(today)
else:
self.stdout.write(self.style.WARNING(
'\n重建后金额对 → python manage.py fix_income_month --yes\n'
'只修今天 → python manage.py fix_income_month --today-only --yes\n'
'日志已对、日统计脏 → python manage.py fix_income_month --logs-only --yes'
))
def _run_logs_only(self, dry_run, label):
try:
log_sum = PlatformIncomeLog.objects.aggregate(t=Sum('amount'))['t'] or 0
log_cnt = PlatformIncomeLog.objects.count()
daily_sum = DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0
except Exception as exc:
self.stderr.write(f'读取失败: {exc}')
return
self.stdout.write(f'{label}】仅按日志对齐日统计\n')
self.stdout.write(f' platform_income_log 合计: {float(log_sum):.2f} 元 / {log_cnt}')
self.stdout.write(f' daily_income_stat 现合计: {float(daily_sum):.2f}')
if dry_run:
self.stdout.write(self.style.WARNING('\n预览。确认后: python manage.py fix_income_month --logs-only --yes'))
return
n = align_daily_income_from_logs_only()
self.stdout.write(self.style.SUCCESS(f'已重写 daily_income_stat {n} 行,并重算 szjilu'))
self._print_today_total(date.today())
def _print_today_total(self, today):
agg = DailyIncomeStat.objects.filter(date=today).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('请刷新后台财务页核对。')

View File

@@ -0,0 +1,133 @@
"""
【反向撤销】repair_wechat_income 造成的重复记账。
你当时执行的指令是:
python manage.py repair_wechat_income
或:
python manage.py repair_wechat_income --date 2026-06-24 --days 7
repair 逻辑:按「当日创建的已支付订单/充值单」逐条 +金额、+笔数、写 platform_income_log、加 szjilu。
本命令用完全相同扫描列表,逐条反向 -金额、-笔数、删日志、减 szjilu。
用法:
1. 预览(不改库):
python manage.py undo_repair_wechat_income
2. 确认后撤销(加多少减多少):
python manage.py undo_repair_wechat_income --yes
3. 若当时带了参数,必须一致:
python manage.py undo_repair_wechat_income --date 2026-06-01 --days 30 --run-date 2026-06-24 --yes
--run-date = 你执行 repair 那天,默认今天)
4. 全部清零(恢复到 0
python manage.py undo_repair_wechat_income --zero-all --yes
"""
from datetime import date, timedelta
from django.core.management.base import BaseCommand
from django.db.models import Sum
from config.models import DailyIncomeStat
from jituan.services.szjilu_accounting import (
undo_repair_wechat_income_period,
zero_all_income_statistics,
)
class Command(BaseCommand):
help = '反向撤销 repair_wechat_income 造成的收支重复记账'
def add_arguments(self, parser):
parser.add_argument(
'--date', type=str, default='',
help='与 repair 相同的 --date扫描区间结束日默认今天',
)
parser.add_argument(
'--days', type=int, default=1,
help='与 repair 相同的 --days默认 1',
)
parser.add_argument(
'--run-date', type=str, default='',
help='你执行 repair 的日期(金额被记到哪天的 daily默认今天',
)
parser.add_argument('--yes', action='store_true', help='确认执行')
parser.add_argument(
'--zero-all', action='store_true',
help='收支统计全部清零日收入、日志、szjilu 收入流水归零)',
)
def handle(self, *args, **options):
if options['zero_all']:
self._zero_all(not options['yes'])
return
if options['date']:
end_day = date.fromisoformat(options['date'])
else:
end_day = date.today()
days = max(1, int(options['days'] or 1))
start_day = end_day - timedelta(days=days - 1)
if options['run_date']:
inflation_date = date.fromisoformat(options['run_date'])
else:
inflation_date = date.today()
dry_run = not options['yes']
self.stdout.write(self.style.WARNING(
'========== 反向撤销 repair_wechat_income ==========\n'
f'原 repair 扫描区间: {start_day} ~ {end_day} --days {days}\n'
f'金额被记到日统计日期: {inflation_date} --run-date\n'
))
if dry_run:
self.stdout.write('【预览】未改数据库\n')
else:
self.stdout.write('【执行】正在反向扣减...\n')
r = undo_repair_wechat_income_period(
start_day, end_day, inflation_date, dry_run=dry_run,
)
self.stdout.write(f' 将撤销条数: {r["undo_count"]}')
self.stdout.write(f' 将扣减金额: {r["undo_amount"]:.2f}')
self.stdout.write(
f' 执行前 {inflation_date} 日统计: {r["before_daily_on_inflation_date"]:.2f}'
)
if r['items']:
self.stdout.write(' 明细:')
for ref, cid, amt, kind in r['items']:
self.stdout.write(f' - {ref} {amt}元 club={cid} ({kind})')
if not dry_run:
self.stdout.write(self.style.SUCCESS(
f'\n执行后 {inflation_date} 日统计: {r.get("after_daily_on_inflation_date", 0):.2f}'
))
self.stdout.write(self.style.SUCCESS('反向撤销完成,请刷新后台财务页。'))
else:
self.stdout.write(self.style.WARNING(
'\n预览完成。确认后执行(参数与 repair 保持一致):\n'
f' python manage.py undo_repair_wechat_income --date {end_day} --days {days} '
f'--run-date {inflation_date} --yes\n'
'全部清零:\n'
' python manage.py undo_repair_wechat_income --zero-all --yes'
))
def _zero_all(self, dry_run):
agg = DailyIncomeStat.objects.aggregate(amt=Sum('total_amount'), cnt=Sum('total_count'))
self.stdout.write(self.style.ERROR(
'========== 收支统计全部清零 ==========\n'
f'当前 daily_income_stat 合计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)}\n'
'将删除: platform_income_log 全部、daily_income_stat 全部\n'
'将归零: szjilu.TotalIncome / TotalFlow / DailyFlow\n'
))
if dry_run:
self.stdout.write(self.style.WARNING(
'预览。确认清零:\n'
' python manage.py undo_repair_wechat_income --zero-all --yes'
))
return
zero_all_income_statistics()
self.stdout.write(self.style.SUCCESS('已全部清零。请刷新后台。'))