fix: undo_repair_wechat_income 反向撤销重复记账
This commit is contained in:
114
config/management/commands/fix_income_month.py
Normal file
114
config/management/commands/fix_income_month.py
Normal 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('请刷新后台财务页核对。')
|
||||
133
config/management/commands/undo_repair_wechat_income.py
Normal file
133
config/management/commands/undo_repair_wechat_income.py
Normal 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('已全部清零。请刷新后台。'))
|
||||
@@ -58,6 +58,150 @@ def apply_szjilu_income(amount, club_id=None):
|
||||
szjilu.save()
|
||||
|
||||
|
||||
def subtract_szjilu_income(amount, club_id=None, subtract_daily_flow=True):
|
||||
"""扣减 szjilu 收入(repair 反向操作用)。"""
|
||||
jine = Decimal(str(amount))
|
||||
with transaction.atomic():
|
||||
szjilu = _get_szjilu_for_update(club_id)
|
||||
szjilu.TotalIncome = max(_ZERO, (szjilu.TotalIncome or _ZERO) - jine)
|
||||
szjilu.TotalFlow = max(_ZERO, (szjilu.TotalFlow or _ZERO) - jine)
|
||||
if subtract_daily_flow:
|
||||
szjilu.DailyFlow = max(_ZERO, (szjilu.DailyFlow or _ZERO) - jine)
|
||||
szjilu.save()
|
||||
|
||||
|
||||
# repair_wechat_income 原始扫描逻辑(用于反向撤销,一字不改)
|
||||
_REPAIR_ORDER_STATUSES = [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
|
||||
|
||||
def collect_repair_wechat_income_scan(stat_date):
|
||||
"""
|
||||
与已禁用的 repair_wechat_income 命令完全相同的扫描列表。
|
||||
返回 [(biz_ref, club_id, amount, kind), ...]
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from orders.models import Order
|
||||
from products.models import Czjilu
|
||||
|
||||
day_start = datetime.combine(stat_date, datetime.min.time())
|
||||
day_end = day_start + timedelta(days=1)
|
||||
items = []
|
||||
|
||||
orders = Order.query.filter(
|
||||
CreateTime__gte=day_start,
|
||||
CreateTime__lt=day_end,
|
||||
Status__in=_REPAIR_ORDER_STATUSES,
|
||||
)
|
||||
for od in orders:
|
||||
cid = normalize_szjilu_club_id(getattr(od, 'ClubID', None))
|
||||
items.append((f'order:{od.OrderID}', cid, Decimal(str(od.Amount or 0)), 'order'))
|
||||
|
||||
cz_orders = Czjilu.query.filter(
|
||||
CreateTime__gte=day_start,
|
||||
CreateTime__lt=day_end,
|
||||
zhuangtai=3,
|
||||
).filter(
|
||||
Q(leixing__in=[1, 2, 3, 4]) | Q(leixing__isnull=False),
|
||||
)
|
||||
for cz in cz_orders:
|
||||
cid = normalize_szjilu_club_id(getattr(cz, 'club_id', None))
|
||||
items.append((f'cz:{cz.dingdan_id}', cid, Decimal(str(cz.jine or 0)), f'cz_{cz.leixing}'))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def undo_repair_wechat_income_one(amount, club_id, biz_ref, inflation_date):
|
||||
"""
|
||||
反向执行 repair 的 _repair_one:
|
||||
- 扣减当日 daily_income_stat(repair 当时 update_daily_income 用的是执行日=inflation_date)
|
||||
- 扣减 szjilu
|
||||
- 删除 platform_income_log(repair 若写过)
|
||||
若 repair 曾走「删日志再记」分支,会多记一次;用 had_log 判断扣两次。
|
||||
"""
|
||||
from orders.utils import subtract_daily_income
|
||||
|
||||
ref = (biz_ref or '').strip()
|
||||
jine = Decimal(str(amount))
|
||||
cid = normalize_szjilu_club_id(club_id)
|
||||
|
||||
had_log = income_log_exists(ref)
|
||||
subtract_daily_income(jine, cid, inflation_date)
|
||||
subtract_szjilu_income(jine, cid, subtract_daily_flow=(inflation_date == date.today()))
|
||||
|
||||
if had_log:
|
||||
subtract_daily_income(jine, cid, inflation_date)
|
||||
subtract_szjilu_income(jine, cid, subtract_daily_flow=(inflation_date == date.today()))
|
||||
|
||||
try:
|
||||
PlatformIncomeLog.objects.filter(biz_ref=ref).delete()
|
||||
except _PLATFORM_LOG_ERRORS:
|
||||
pass
|
||||
|
||||
|
||||
def undo_repair_wechat_income_period(start_date, end_date, inflation_date, dry_run=True):
|
||||
"""
|
||||
撤销 repair_wechat_income 在 [start_date, end_date] 扫描范围内造成的入账。
|
||||
inflation_date = 你执行 repair 那天(repair 把所有金额都记到执行日的 daily 里)。
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db.models import Sum
|
||||
|
||||
all_items = []
|
||||
d = start_date
|
||||
while d <= end_date:
|
||||
all_items.extend(collect_repair_wechat_income_scan(d))
|
||||
d += timedelta(days=1)
|
||||
|
||||
total_amt = sum((x[2] for x in all_items), _ZERO)
|
||||
before_daily = DailyIncomeStat.objects.filter(date=inflation_date).aggregate(
|
||||
amt=Sum('total_amount'), cnt=Sum('total_count'),
|
||||
)
|
||||
|
||||
result = {
|
||||
'start': str(start_date),
|
||||
'end': str(end_date),
|
||||
'inflation_date': str(inflation_date),
|
||||
'undo_count': len(all_items),
|
||||
'undo_amount': float(total_amt),
|
||||
'before_daily_on_inflation_date': float(before_daily['amt'] or 0),
|
||||
'items': all_items,
|
||||
'dry_run': dry_run,
|
||||
}
|
||||
|
||||
if dry_run:
|
||||
return result
|
||||
|
||||
with transaction.atomic():
|
||||
for ref, cid, jine, _kind in all_items:
|
||||
undo_repair_wechat_income_one(jine, cid, ref, inflation_date)
|
||||
|
||||
after_daily = DailyIncomeStat.objects.filter(date=inflation_date).aggregate(
|
||||
amt=Sum('total_amount'), cnt=Sum('total_count'),
|
||||
)
|
||||
result['after_daily_on_inflation_date'] = float(after_daily['amt'] or 0)
|
||||
return result
|
||||
|
||||
|
||||
def zero_all_income_statistics():
|
||||
"""核弹:收支统计全部清零(日收入表、幂等日志、szjilu 收入/流水)。"""
|
||||
with transaction.atomic():
|
||||
try:
|
||||
PlatformIncomeLog.objects.all().delete()
|
||||
except _PLATFORM_LOG_ERRORS:
|
||||
pass
|
||||
DailyIncomeStat.objects.all().delete()
|
||||
for szjilu in Szjilu.objects.select_for_update().all():
|
||||
szjilu.TotalIncome = _ZERO
|
||||
szjilu.TotalFlow = _ZERO
|
||||
szjilu.DailyFlow = _ZERO
|
||||
szjilu.save(update_fields=['TotalIncome', 'TotalFlow', 'DailyFlow', 'UpdateTime'])
|
||||
return True
|
||||
|
||||
|
||||
def _apply_income_stats(amount, club_id):
|
||||
"""写入 daily_income_stat + szjilu(仅在新 platform_income_log 落库后调用)。"""
|
||||
from orders.utils import update_daily_income
|
||||
@@ -206,15 +350,11 @@ _CZ_WECHAT_LEIXING = [1, 2, 3, 4, 5]
|
||||
|
||||
def collect_canonical_wechat_income_for_date(stat_date, existing_refs=None):
|
||||
"""
|
||||
收集当日真实微信收入(每笔 biz_ref 全局唯一,不重复计已入账订单)。
|
||||
- 订单:当日创建且已付款,或当日完成微信支付(有微信交易号)
|
||||
- 充值:当日创建且 zhuangtai=3
|
||||
- 不含退款单(Status=5)
|
||||
收集当日真实微信收入(严格:仅统计当日下单且已支付成功的单据,每笔唯一)。
|
||||
不含退款;不按 UpdateTime 计老单(repair 虚高的主要原因之一)。
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from orders.models import Order
|
||||
from products.models import Czjilu
|
||||
|
||||
@@ -235,17 +375,11 @@ def collect_canonical_wechat_income_for_date(stat_date, existing_refs=None):
|
||||
seen[ref] = True
|
||||
items.append((ref, cid, jine, kind))
|
||||
|
||||
order_qs = Order.query.filter(
|
||||
for od in Order.query.filter(
|
||||
CreateTime__gte=day_start,
|
||||
CreateTime__lt=day_end,
|
||||
Status__in=_ORDER_INCOME_STATUSES,
|
||||
).filter(
|
||||
Q(CreateTime__gte=day_start, CreateTime__lt=day_end)
|
||||
| Q(
|
||||
UpdateTime__gte=day_start,
|
||||
UpdateTime__lt=day_end,
|
||||
WechatTransactionID__isnull=False,
|
||||
),
|
||||
)
|
||||
for od in order_qs:
|
||||
):
|
||||
txn = (getattr(od, 'WechatTransactionID', None) or '').strip()
|
||||
if not txn:
|
||||
continue
|
||||
@@ -262,6 +396,141 @@ def collect_canonical_wechat_income_for_date(stat_date, existing_refs=None):
|
||||
return items
|
||||
|
||||
|
||||
def dedupe_platform_income_logs():
|
||||
"""删除 biz_ref 重复的幂等日志(保留最早一条)。"""
|
||||
try:
|
||||
seen = {}
|
||||
dup_ids = []
|
||||
for log in PlatformIncomeLog.objects.order_by('id'):
|
||||
key = (log.biz_ref or '').strip()
|
||||
if not key:
|
||||
dup_ids.append(log.id)
|
||||
continue
|
||||
if key in seen:
|
||||
dup_ids.append(log.id)
|
||||
else:
|
||||
seen[key] = log.id
|
||||
if dup_ids:
|
||||
PlatformIncomeLog.objects.filter(id__in=dup_ids).delete()
|
||||
return len(dup_ids)
|
||||
except _PLATFORM_LOG_ERRORS:
|
||||
return 0
|
||||
|
||||
|
||||
def fix_income_period(start_date, end_date, dry_run=True):
|
||||
"""
|
||||
核弹级修复:删掉区间内错误日志与日统计,按严格规则重建,再对齐 szjilu。
|
||||
daily_income_stat 只信 platform_income_log,不再累加脏数据。
|
||||
"""
|
||||
from datetime import datetime, time as dt_time, timedelta
|
||||
|
||||
from django.db.models import Sum
|
||||
from django.utils import timezone
|
||||
|
||||
before_daily = DailyIncomeStat.objects.filter(
|
||||
date__gte=start_date, date__lte=end_date,
|
||||
).aggregate(amt=Sum('total_amount'), cnt=Sum('total_count'))
|
||||
|
||||
before_log = _ZERO
|
||||
before_log_cnt = 0
|
||||
try:
|
||||
before_log_cnt = PlatformIncomeLog.objects.filter(
|
||||
CreateTime__date__gte=start_date,
|
||||
CreateTime__date__lte=end_date,
|
||||
).count()
|
||||
agg = PlatformIncomeLog.objects.filter(
|
||||
CreateTime__date__gte=start_date,
|
||||
CreateTime__date__lte=end_date,
|
||||
).aggregate(t=Sum('amount'))
|
||||
before_log = agg['t'] or _ZERO
|
||||
except _PLATFORM_LOG_ERRORS:
|
||||
pass
|
||||
|
||||
all_items = _collect_items_for_period(start_date, end_date)
|
||||
rebuild_amt = sum((x[3] for x in all_items), _ZERO)
|
||||
result = {
|
||||
'start': str(start_date),
|
||||
'end': str(end_date),
|
||||
'before_daily_amount': float(before_daily['amt'] or 0),
|
||||
'before_daily_count': int(before_daily['cnt'] or 0),
|
||||
'before_log_amount': float(before_log),
|
||||
'before_log_count': before_log_cnt,
|
||||
'after_amount': float(rebuild_amt),
|
||||
'after_count': len(all_items),
|
||||
'items': all_items,
|
||||
'dry_run': dry_run,
|
||||
}
|
||||
|
||||
if dry_run:
|
||||
return result
|
||||
|
||||
execute_fix_income_period(start_date, end_date)
|
||||
return result
|
||||
|
||||
|
||||
def _collect_items_for_period(start_date, end_date):
|
||||
from datetime import timedelta
|
||||
|
||||
items = []
|
||||
d = start_date
|
||||
while d <= end_date:
|
||||
for ref, cid, jine, kind in collect_canonical_wechat_income_for_date(d, existing_refs=set()):
|
||||
items.append((d, ref, cid, jine, kind))
|
||||
d += timedelta(days=1)
|
||||
return items
|
||||
|
||||
|
||||
def execute_fix_income_period(start_date, end_date):
|
||||
from datetime import datetime, time as dt_time, timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
dedupe_platform_income_logs()
|
||||
items = _collect_items_for_period(start_date, end_date)
|
||||
|
||||
with transaction.atomic():
|
||||
PlatformIncomeLog.objects.filter(
|
||||
CreateTime__date__gte=start_date,
|
||||
CreateTime__date__lte=end_date,
|
||||
).delete()
|
||||
DailyIncomeStat.objects.filter(
|
||||
date__gte=start_date, date__lte=end_date,
|
||||
).delete()
|
||||
|
||||
by_day_club = {}
|
||||
for stat_date, ref, cid, jine, _kind in items:
|
||||
pay_ts = timezone.make_aware(datetime.combine(stat_date, dt_time(12, 0, 0)))
|
||||
log = PlatformIncomeLog.objects.create(biz_ref=ref, club_id=cid, amount=jine)
|
||||
PlatformIncomeLog.objects.filter(pk=log.pk).update(CreateTime=pay_ts)
|
||||
key = (stat_date, cid)
|
||||
if key not in by_day_club:
|
||||
by_day_club[key] = {'amt': _ZERO, 'cnt': 0}
|
||||
by_day_club[key]['amt'] += jine
|
||||
by_day_club[key]['cnt'] += 1
|
||||
|
||||
for (stat_date, cid), agg in by_day_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'],
|
||||
)
|
||||
|
||||
sync_szjilu_income_from_logs()
|
||||
return items
|
||||
|
||||
|
||||
def align_daily_income_from_logs_only():
|
||||
"""不重扫订单:仅按现有 platform_income_log 重写 daily_income_stat + szjilu(日志对、日统计错时用)。"""
|
||||
dedupe_platform_income_logs()
|
||||
n = rebuild_all_daily_income_from_logs()
|
||||
sync_szjilu_income_from_logs()
|
||||
return n
|
||||
|
||||
|
||||
def _existing_refs_outside_date(stat_date):
|
||||
"""该日之外已入账的 biz_ref(删当日错误日志后仍保留的)。"""
|
||||
try:
|
||||
|
||||
@@ -65,6 +65,29 @@ def update_daily_income(amount, club_id=None):
|
||||
)
|
||||
|
||||
|
||||
def subtract_daily_income(amount, club_id=None, stat_date=None):
|
||||
"""扣减指定日期的收入统计(repair 反向操作用)。"""
|
||||
from jituan.constants import CLUB_ID_DEFAULT
|
||||
|
||||
cid = club_id or CLUB_ID_DEFAULT
|
||||
target = stat_date or date.today()
|
||||
|
||||
with transaction.atomic():
|
||||
stat = DailyIncomeStat.objects.select_for_update().filter(
|
||||
date=target, club_id=cid,
|
||||
).first()
|
||||
if not stat:
|
||||
return
|
||||
jine = Decimal(str(amount))
|
||||
zero = Decimal('0.00')
|
||||
new_amt = max(zero, Decimal(str(stat.total_amount or 0)) - jine)
|
||||
new_cnt = max(0, int(stat.total_count or 0) - 1)
|
||||
stat.total_amount = new_amt
|
||||
stat.total_count = new_cnt
|
||||
stat.save(update_fields=['total_amount', 'total_count'])
|
||||
logger.info(f"每日收入扣减[{cid}]: {target}, -{amount}元")
|
||||
|
||||
|
||||
def update_daily_payout(amount, club_id=None):
|
||||
"""
|
||||
原子更新当日出款统计(金额累加,笔数加1)。
|
||||
|
||||
@@ -211,7 +211,7 @@ class WechatPayNotifyView(APIView):
|
||||
try:
|
||||
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
|
||||
repair_wechat_income_if_missing(
|
||||
dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{out_trade_no}',
|
||||
dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{dingdan.OrderID}',
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'订单补记收支失败 {out_trade_no}: {e}', exc_info=True)
|
||||
@@ -318,7 +318,7 @@ class WechatPayNotifyView(APIView):
|
||||
try:
|
||||
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
|
||||
repair_wechat_income_if_missing(
|
||||
dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{out_trade_no}',
|
||||
dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{dingdan.OrderID}',
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'订单收支统计失败 {out_trade_no}: {e}', exc_info=True)
|
||||
|
||||
Reference in New Issue
Block a user