From a6068f7e2b34897073c22f3ce91276082f7964d9 Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 24 Jun 2026 21:10:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E5=85=A5=E8=B4=A6=E8=A1=A5=E8=AE=B0=20+=20=E8=B4=A2=E5=8A=A1?= =?UTF-8?q?=E6=8C=89=E4=BF=B1=E4=B9=90=E9=83=A8=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/repair_wechat_income.py | 102 ++++++++++++++++++ jituan/services/czjilu_fulfill.py | 35 +++++- jituan/services/member_recharge.py | 13 ++- jituan/services/szjilu_accounting.py | 22 ++-- orders/views.py | 30 ++++-- products/views.py | 43 +++++++- users/views.py | 30 ++++-- 7 files changed, 240 insertions(+), 35 deletions(-) create mode 100644 config/management/commands/repair_wechat_income.py diff --git a/config/management/commands/repair_wechat_income.py b/config/management/commands/repair_wechat_income.py new file mode 100644 index 0000000..6daf177 --- /dev/null +++ b/config/management/commands/repair_wechat_income.py @@ -0,0 +1,102 @@ +"""补记漏掉的微信收入统计(今日或指定日期已支付但未入账的单据)。""" +from datetime import date, datetime, timedelta + +from django.core.management.base import BaseCommand +from django.db.models import Q + +from config.models import PlatformIncomeLog +from jituan.constants import CLUB_ID_DEFAULT +from jituan.services.szjilu_accounting import repair_wechat_income_if_missing +from orders.models import Order +from products.models import Czjilu + + +class Command(BaseCommand): + help = '扫描已支付订单/充值单,补记 daily_income_stat(幂等,可重复执行)' + + def add_arguments(self, parser): + parser.add_argument( + '--date', + type=str, + default='', + help='补记日期 YYYY-MM-DD,默认今天', + ) + parser.add_argument( + '--days', + type=int, + default=1, + help='从指定日期起向前补几天,默认 1(仅当天)', + ) + + def handle(self, *args, **options): + 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) + + repaired = 0 + skipped = 0 + errors = 0 + + for day in range(days): + d = start_day + timedelta(days=day) + day_start = datetime.combine(d, datetime.min.time()) + day_end = day_start + timedelta(days=1) + self.stdout.write(f'扫描 {d} ...') + + orders = Order.query.filter( + CreateTime__gte=day_start, + CreateTime__lt=day_end, + Status__in=[1, 2, 3, 4, 5, 6, 7, 8], + ) + for od in orders: + club_id = getattr(od, 'ClubID', None) or CLUB_ID_DEFAULT + ref = f'order:{od.OrderID}' + try: + ok = self._repair_one(od.Amount, club_id, biz_ref=ref) + if ok: + repaired += 1 + self.stdout.write(f' + 订单 {od.OrderID} {od.Amount}元 club={club_id}') + else: + skipped += 1 + except Exception as exc: + errors += 1 + self.stderr.write(f' ! 订单 {od.OrderID} 失败: {exc}') + + 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: + club_id = getattr(cz, 'club_id', None) or CLUB_ID_DEFAULT + ref = f'cz:{cz.dingdan_id}' + try: + ok = self._repair_one(cz.jine, club_id, biz_ref=ref) + if ok: + repaired += 1 + self.stdout.write(f' + 充值 {cz.dingdan_id} {cz.jine}元 leixing={cz.leixing} club={club_id}') + else: + skipped += 1 + except Exception as exc: + errors += 1 + self.stderr.write(f' ! 充值 {cz.dingdan_id} 失败: {exc}') + + self.stdout.write(self.style.SUCCESS( + f'完成:新补记 {repaired} 笔,已存在跳过 {skipped} 笔,失败 {errors} 笔', + )) + + def _repair_one(self, amount, club_id, biz_ref): + """幂等补记;若仅有孤儿幂等日志无日统计,删日志后重试一次。""" + ref = (biz_ref or '').strip() + ok = repair_wechat_income_if_missing(amount, club_id, biz_ref=ref) + if ok: + return True + if PlatformIncomeLog.objects.filter(biz_ref=ref).exists(): + PlatformIncomeLog.objects.filter(biz_ref=ref).delete() + return repair_wechat_income_if_missing(amount, club_id, biz_ref=ref) + return False diff --git a/jituan/services/czjilu_fulfill.py b/jituan/services/czjilu_fulfill.py index c363a7d..25fb20d 100644 --- a/jituan/services/czjilu_fulfill.py +++ b/jituan/services/czjilu_fulfill.py @@ -35,6 +35,15 @@ def fulfill_yajin_recharge(dingdan_id, paid_amount_yuan=None): if order.leixing != 2: raise CzjiluFulfillError('订单类型不是押金', 'type_error') if order.zhuangtai != 9: + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None) or CLUB_ID_DEFAULT, + biz_ref=f'cz:{dingdan_id}', + ) + except Exception as exc: + logger.error('押金补记收支失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True) return {'already_done': True, 'dingdan_id': dingdan_id, 'zhuangtai': order.zhuangtai} amount = Decimal(str(order.jine)) @@ -56,8 +65,8 @@ def fulfill_yajin_recharge(dingdan_id, paid_amount_yuan=None): club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT try: - from jituan.services.szjilu_accounting import record_wechat_income_once - record_wechat_income_once(amount, club_id, biz_ref=f'cz:{dingdan_id}') + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing(amount, club_id, biz_ref=f'cz:{dingdan_id}') except Exception as exc: logger.error('押金收支统计失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True) @@ -116,6 +125,15 @@ def fulfill_jifen_recharge(dingdan_id, paid_amount_yuan=None): if order.leixing != 3: raise CzjiluFulfillError('订单类型不是积分', 'type_error') if order.zhuangtai != 9: + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None) or CLUB_ID_DEFAULT, + biz_ref=f'cz:{dingdan_id}', + ) + except Exception as exc: + logger.error('积分补记收支失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True) return {'already_done': True, 'dingdan_id': dingdan_id, 'zhuangtai': order.zhuangtai} amount = Decimal(str(order.jine)) @@ -140,8 +158,8 @@ def fulfill_jifen_recharge(dingdan_id, paid_amount_yuan=None): club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT try: - from jituan.services.szjilu_accounting import record_wechat_income_once - record_wechat_income_once(amount, club_id, biz_ref=f'cz:{dingdan_id}') + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing(amount, club_id, biz_ref=f'cz:{dingdan_id}') except Exception as exc: logger.error('积分收支统计失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True) @@ -162,6 +180,15 @@ def confirm_czjilu_paid(dingdan_id, yonghuid, expected_leixing): raise CzjiluFulfillError('订单类型不匹配', 'type_error') if order.zhuangtai == 3: + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None) or CLUB_ID_DEFAULT, + biz_ref=f'cz:{dingdan_id}', + ) + except Exception as exc: + logger.error('充值单补记收支失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True) return {'already_done': True, 'zhuangtai': 3} if order.zhuangtai != 9: diff --git a/jituan/services/member_recharge.py b/jituan/services/member_recharge.py index 84c5f51..e620e78 100644 --- a/jituan/services/member_recharge.py +++ b/jituan/services/member_recharge.py @@ -335,6 +335,15 @@ def fulfill_member_recharge(dingdan_id, paid_amount_yuan=None, source='callback' '会员订单已处理,跳过履约 dingdan=%s zhuangtai=%s source=%s', dingdan_id, order.zhuangtai, source, ) + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None) or CLUB_ID_DEFAULT, + biz_ref=f'cz:{dingdan_id}', + ) + except Exception as exc: + logger.error('会员补记收支失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True) return { 'already_done': True, 'dingdan_id': dingdan_id, @@ -380,8 +389,8 @@ def fulfill_member_recharge(dingdan_id, paid_amount_yuan=None, source='callback' jine_decimal = Decimal(str(order.jine)) try: - from jituan.services.szjilu_accounting import record_wechat_income_once - record_wechat_income_once(jine_decimal, club_id, biz_ref=f'cz:{dingdan_id}') + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing(jine_decimal, club_id, biz_ref=f'cz:{dingdan_id}') except Exception as exc: logger.error('会员收支统计更新失败: %s', exc, exc_info=True) diff --git a/jituan/services/szjilu_accounting.py b/jituan/services/szjilu_accounting.py index 6de87e6..2b0367e 100644 --- a/jituan/services/szjilu_accounting.py +++ b/jituan/services/szjilu_accounting.py @@ -60,7 +60,7 @@ def record_wechat_income_once(amount, club_id=None, biz_ref=None): """ 微信入账:同一 biz_ref 只记一次 daily_income_stat + szjilu。 返回 True=本次新入账;False=已记过(重复回调)。 - 若 platform_income_log 表不可用,仍记入收入(与改造前一致,不能因幂等表故障丢统计)。 + 幂等日志与日统计在同一事务内提交,避免「已支付但未入账」。 """ ref = (biz_ref or '').strip() if not ref: @@ -75,6 +75,7 @@ def record_wechat_income_once(amount, club_id=None, biz_ref=None): club_id=cid, amount=jine, ) + _apply_income_stats(jine, cid) except IntegrityError: logger.info('平台入账已存在,跳过重复记账 biz_ref=%s', ref) return False @@ -87,24 +88,25 @@ def record_wechat_income_once(amount, club_id=None, biz_ref=None): return True except Exception as exc: logger.error( - 'platform_income_log 写入异常,仍记入收入 biz_ref=%s err=%s', + 'platform_income_log 入账异常,仍尝试记入收入 biz_ref=%s err=%s', ref, exc, exc_info=True, ) _apply_income_stats(jine, cid) return True - try: - _apply_income_stats(jine, cid) - except Exception: - try: - PlatformIncomeLog.objects.filter(biz_ref=ref).delete() - except Exception: - pass - raise logger.info('平台入账记账成功 biz_ref=%s club=%s amount=%s', ref, cid, jine) return True +def repair_wechat_income_if_missing(amount, club_id=None, biz_ref=None): + """已支付订单/充值单重复回调时安全补记收支(幂等)。""" + try: + return record_wechat_income_once(amount, club_id, biz_ref) + except Exception as exc: + logger.error('补记收支失败 biz_ref=%s err=%s', biz_ref, exc, exc_info=True) + return False + + def apply_szjilu_expense(amount, club_id=None): """平台出款/支出:总收益减少,总支出、今日支出增加。""" jine = Decimal(str(amount)) diff --git a/orders/views.py b/orders/views.py index 50f37e5..633604b 100644 --- a/orders/views.py +++ b/orders/views.py @@ -205,9 +205,16 @@ class WechatPayNotifyView(APIView): logger.info(f"订单当前状态: {dingdan.Status}") - # 幂等处理:只有状态为9(未支付)才处理,已处理则跳过 + # 幂等:已支付则补试收支记账(防止首次入账失败后再也无法统计) if dingdan.Status != 9: - logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.Status},跳过") + logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.Status},补试收支记账") + 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}', + ) + except Exception as e: + logger.error(f'订单补记收支失败 {out_trade_no}: {e}', exc_info=True) return # 保存微信交易号 @@ -309,8 +316,8 @@ class WechatPayNotifyView(APIView): # 更新收支记录(总累计);统计失败不得回滚订单支付 try: - from jituan.services.szjilu_accounting import record_wechat_income_once - record_wechat_income_once( + 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}', ) except Exception as e: @@ -498,7 +505,15 @@ class PaymentVerifyView(APIView): # 重新锁定订单,防止并发更新 dingdan = Order.query.select_for_update().get(pk=dingdan.pk) if dingdan.Status != 9: - # 订单状态已被其他进程更新,直接返回(幂等) + 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:{dingdan.OrderID}', + ) + except Exception as e: + logger.error(f'订单复查补记收支失败 {dingdan.OrderID}: {e}', exc_info=True) return # 更新订单状态 @@ -571,10 +586,9 @@ class PaymentVerifyView(APIView): # ============================================= - # 更新收支记录 try: - from jituan.services.szjilu_accounting import record_wechat_income_once - record_wechat_income_once( + 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:{dingdan.OrderID}', diff --git a/products/views.py b/products/views.py index e4cc257..efe672f 100644 --- a/products/views.py +++ b/products/views.py @@ -608,7 +608,15 @@ class YajinHuitiao(View): # 5. 检查订单状态是否为未支付(9) if order.zhuangtai != 9: logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") - # 已处理过的订单直接返回成功,避免微信重复回调 + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None), + biz_ref=f'cz:{out_trade_no}', + ) + except Exception as exc: + logger.error('押金回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) return self.wechat_response('SUCCESS', 'OK') # 6. 验证金额(微信返回的是分,需要转换) @@ -1117,6 +1125,15 @@ class JifenHuitiao(View): # 5. 检查订单状态是否为未支付(9) if order.zhuangtai != 9: logger.warning(f"积分订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None), + biz_ref=f'cz:{out_trade_no}', + ) + except Exception as exc: + logger.error('积分回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) return self.wechat_response('SUCCESS', 'OK') # 6. 验证金额(固定5元) @@ -1663,6 +1680,16 @@ class HuiyuanHuitiao(View): if order.zhuangtai == 3: from jituan.services.member_recharge import repair_member_entitlement_if_paid repair_member_entitlement_if_paid(out_trade_no) + try: + from decimal import Decimal + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None), + biz_ref=f'cz:{out_trade_no}', + ) + except Exception as exc: + logger.error('会员回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) return self.wechat_response('SUCCESS', 'OK') wechat_amount_yuan = total_fee / 100 @@ -2232,7 +2259,15 @@ class ShangjiaHuitiao(View): # 5. 检查订单状态是否为未支付(9) if order.zhuangtai != 9: logger.warning(f"商家订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") - # 已处理过的订单直接返回成功,避免微信重复回调 + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None), + biz_ref=f'cz:{order.dingdan_id}', + ) + except Exception as exc: + logger.error('商家回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) return self.wechat_response('SUCCESS', 'OK') # 6. 验证金额(微信返回的是分,需要转换) @@ -2248,8 +2283,8 @@ class ShangjiaHuitiao(View): # 更新收支记录表 try: jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - from jituan.services.szjilu_accounting import record_wechat_income_once - record_wechat_income_once( + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( jine_decimal, getattr(order, 'club_id', None), biz_ref=f'cz:{order.dingdan_id}', diff --git a/users/views.py b/users/views.py index 50505f0..4b51846 100644 --- a/users/views.py +++ b/users/views.py @@ -11928,7 +11928,15 @@ class FaKuanHuitiaoView(View): # 5. 检查订单状态是否为未支付(9) if order.zhuangtai != 9: logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") - # 已处理过的订单直接返回成功,避免微信重复回调 + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None), + biz_ref=f'cz:{out_trade_no}', + ) + except Exception as exc: + logger.error('罚款回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) return self.wechat_response('SUCCESS', 'OK') # 6. 验证金额(微信返回的是分,需要转换) @@ -11970,8 +11978,8 @@ class FaKuanHuitiaoView(View): # 9. 更新收支记录表 try: jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - from jituan.services.szjilu_accounting import record_wechat_income_once - record_wechat_income_once( + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( jine_decimal, getattr(order, 'club_id', None), biz_ref=f'cz:{order.dingdan_id}', @@ -12475,9 +12483,17 @@ class KaohePayCallbackView(View): if order.zhuangtai != 9: logger.warning(f"订单状态非未支付: {order.zhuangtai}, 订单号: {out_trade_no}") - # 已支付的订单直接返回成功,避免微信重复回调 if order.zhuangtai == 3: - logger.info(f"订单已支付,直接返回成功: {out_trade_no}") + logger.info(f"订单已支付,补试收支记账: {out_trade_no}") + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None), + biz_ref=f'cz:{out_trade_no}', + ) + except Exception as exc: + logger.error('考核回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) return self.wechat_response('SUCCESS', 'OK') return self.wechat_response('FAIL', '订单状态异常') @@ -12532,8 +12548,8 @@ class KaohePayCallbackView(View): # 更新收支记录 try: jine_decimal = Decimal(str(order.jine)) - from jituan.services.szjilu_accounting import record_wechat_income_once - record_wechat_income_once( + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( jine_decimal, getattr(order, 'club_id', None), biz_ref=f'cz:{order.dingdan_id}',