From 45123d5b6ff33af4e24e9fac52c7945eab028647 Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 24 Jun 2026 18:16:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=85=E5=80=BC=E5=B1=A5=E7=BA=A6?= =?UTF-8?q?=E5=B9=82=E7=AD=89=E5=8A=A0=E5=9B=BA(=E4=BC=9A=E5=91=98/?= =?UTF-8?q?=E6=8A=BC=E9=87=91/=E7=A7=AF=E5=88=86)=EF=BC=8C=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E5=8A=A0=E4=BC=9A=E5=91=98club=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/view.py | 5 +- jituan/services/czjilu_fulfill.py | 179 ++++++++++++++++++++++++++++ products/views.py | 192 ++++++------------------------ 3 files changed, 219 insertions(+), 157 deletions(-) create mode 100644 jituan/services/czjilu_fulfill.py diff --git a/backend/view.py b/backend/view.py index ef9fbd9..87c63c8 100644 --- a/backend/view.py +++ b/backend/view.py @@ -1256,7 +1256,7 @@ class KefuUpdateDashouView(APIView): # ---------- 会员操作 ---------- elif caozuo == 'jia_huiyuan': - if '001ff' not in permissions: + if '001ff' not in permissions and '000001' not in permissions: return Response({'code': 403, 'msg': '无权限给打手加会员'}) if not huiyuan_id: return Response({'code': 400, 'msg': '缺少会员ID'}) @@ -1265,6 +1265,8 @@ class KefuUpdateDashouView(APIView): huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 404, 'msg': '会员类型不存在'}) + from jituan.services.club_user import get_user_club_id + player_club = get_user_club_id(dashou_user) record, created = Huiyuangoumai.objects.select_for_update().get_or_create( yonghu_id=dashou_id, huiyuan_id=huiyuan_id, @@ -1272,6 +1274,7 @@ class KefuUpdateDashouView(APIView): 'huiyuan_zhuangtai': 1, 'jieshao': huiyuan.jieshao, 'daoqi_time': timezone.now() + timedelta(days=days), + 'club_id': player_club, } ) if not created: diff --git a/jituan/services/czjilu_fulfill.py b/jituan/services/czjilu_fulfill.py new file mode 100644 index 0000000..c363a7d --- /dev/null +++ b/jituan/services/czjilu_fulfill.py @@ -0,0 +1,179 @@ +"""充值订单 czjilu 履约(押金/积分),与 member_recharge 同一套幂等原则。""" +from __future__ import annotations + +import logging +from decimal import Decimal + +from django.db import transaction + +from backend.utils import update_guanshi_daily_by_action +from jituan.constants import CLUB_ID_DEFAULT +from jituan.services.club_penalty import resolve_gsfenhong_club_id +from orders.models import CommissionRate +from products.models import Czjilu, Gsfenhong +from users.models import UserDashou, UserGuanshi + +logger = logging.getLogger(__name__) + +INTEGRAL_PRICE = Decimal('5.00') +MAX_INTEGRAL = 10 +INTEGRAL_INCREMENT = 5 + + +class CzjiluFulfillError(Exception): + def __init__(self, message, code='error'): + super().__init__(message) + self.code = code + + +@transaction.atomic +def fulfill_yajin_recharge(dingdan_id, paid_amount_yuan=None): + """押金充值幂等履约:仅 zhuangtai==9 时入账。""" + order = Czjilu.query.filter(dingdan_id=dingdan_id).select_for_update().first() + if not order: + raise CzjiluFulfillError('订单不存在', 'not_found') + if order.leixing != 2: + raise CzjiluFulfillError('订单类型不是押金', 'type_error') + if order.zhuangtai != 9: + return {'already_done': True, 'dingdan_id': dingdan_id, 'zhuangtai': order.zhuangtai} + + amount = Decimal(str(order.jine)) + if paid_amount_yuan is not None: + paid = Decimal(str(paid_amount_yuan)) + if abs(paid - amount) > Decimal('0.01'): + raise CzjiluFulfillError(f'支付金额{paid}与订单{amount}不一致', 'amount_mismatch') + + try: + dashou = UserDashou.objects.select_for_update().get(user__UserUID=order.yonghuid) + except UserDashou.DoesNotExist: + raise CzjiluFulfillError('打手不存在,无法充值押金', 'dashou_not_found') + + dashou.yajin += amount + dashou.save(update_fields=['yajin']) + + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + + 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}') + except Exception as exc: + logger.error('押金收支统计失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True) + + try: + _apply_yajin_fenhong(order, dashou, amount) + except Exception as exc: + logger.error('押金分红失败但押金已到账 dingdan=%s: %s', dingdan_id, exc, exc_info=True) + + logger.info('押金履约完成 dingdan=%s amount=%s', dingdan_id, amount) + return {'already_done': False, 'dingdan_id': dingdan_id, 'zhuangtai': 3, 'yajin': dashou.yajin} + + +def _apply_yajin_fenhong(order, dashou, amount): + if Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists(): + return + guanshi_id = dashou.yaoqingren + if not guanshi_id: + return + lilu_obj = CommissionRate.query.filter(Platform='12').first() + rate = lilu_obj.Rate if lilu_obj and lilu_obj.Rate is not None else Decimal('0') + if rate <= 0: + return + fenhong_jine = (amount * rate).quantize(Decimal('0.01')) + if fenhong_jine <= 0: + return + guanshi = UserGuanshi.objects.select_for_update().get(user__UserUID=guanshi_id) + guanshi.yue += fenhong_jine + guanshi.chongzhifenrun += fenhong_jine + guanshi.save(update_fields=['yue', 'chongzhifenrun']) + user_main = dashou.user + Gsfenhong.query.create( + dingdan_id=order.dingdan_id, + guanshi=guanshi_id, + dashouid=order.yonghuid, + shuoming='押金分红', + fenhong=fenhong_jine, + avatar=user_main.Avatar, + nicheng=dashou.nicheng or '未知打手', + fenhong_leixing=2, + club_id=resolve_gsfenhong_club_id( + dingdan_id=order.dingdan_id, dashouid=order.yonghuid, czjilu=order, + ), + ) + try: + update_guanshi_daily_by_action(yonghuid=guanshi_id, action=4, amount=fenhong_jine) + except Exception as exc: + logger.error('押金分红日统计失败: %s', exc) + + +@transaction.atomic +def fulfill_jifen_recharge(dingdan_id, paid_amount_yuan=None): + """积分补充幂等履约:仅 zhuangtai==9 时入账。""" + order = Czjilu.query.filter(dingdan_id=dingdan_id).select_for_update().first() + if not order: + raise CzjiluFulfillError('订单不存在', 'not_found') + if order.leixing != 3: + raise CzjiluFulfillError('订单类型不是积分', 'type_error') + if order.zhuangtai != 9: + return {'already_done': True, 'dingdan_id': dingdan_id, 'zhuangtai': order.zhuangtai} + + amount = Decimal(str(order.jine)) + if paid_amount_yuan is not None: + paid = Decimal(str(paid_amount_yuan)) + if abs(paid - amount) > Decimal('0.01'): + raise CzjiluFulfillError(f'支付金额{paid}与订单{amount}不一致', 'amount_mismatch') + if abs(paid - INTEGRAL_PRICE) > Decimal('0.01'): + raise CzjiluFulfillError('积分订单金额必须为5元', 'amount_mismatch') + + try: + dashou = UserDashou.objects.select_for_update().get(user__UserUID=order.yonghuid) + except UserDashou.DoesNotExist: + raise CzjiluFulfillError('打手不存在,无法补充积分', 'dashou_not_found') + + if dashou.jifen < MAX_INTEGRAL: + dashou.jifen = min(dashou.jifen + INTEGRAL_INCREMENT, MAX_INTEGRAL) + dashou.save(update_fields=['jifen']) + + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + + 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}') + except Exception as exc: + logger.error('积分收支统计失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True) + + logger.info('积分履约完成 dingdan=%s jifen=%s', dingdan_id, dashou.jifen) + return {'already_done': False, 'dingdan_id': dingdan_id, 'zhuangtai': 3, 'jifen': dashou.jifen} + + +def confirm_czjilu_paid(dingdan_id, yonghuid, expected_leixing): + """轮询确认:查微信 SUCCESS 后幂等履约。""" + from jituan.services.member_recharge import query_wechat_v2_trade_state + + try: + order = Czjilu.query.get(dingdan_id=dingdan_id, yonghuid=yonghuid) + except Czjilu.DoesNotExist: + raise CzjiluFulfillError('订单不存在或不属于当前用户', 'not_found') + + if order.leixing != expected_leixing: + raise CzjiluFulfillError('订单类型不匹配', 'type_error') + + if order.zhuangtai == 3: + return {'already_done': True, 'zhuangtai': 3} + + if order.zhuangtai != 9: + raise CzjiluFulfillError(f'订单状态异常: {order.zhuangtai}', 'status_error') + + trade_state, _ = query_wechat_v2_trade_state(dingdan_id, getattr(order, 'club_id', None)) + if trade_state != 'SUCCESS': + raise CzjiluFulfillError('微信侧尚未支付成功,请稍后再试', 'not_paid') + + paid = Decimal(str(order.jine)) + if expected_leixing == 2: + return fulfill_yajin_recharge(dingdan_id, paid_amount_yuan=paid) + if expected_leixing == 3: + return fulfill_jifen_recharge(dingdan_id, paid_amount_yuan=paid) + raise CzjiluFulfillError('不支持的充值类型', 'type_error') diff --git a/products/views.py b/products/views.py index 4eec1d5..bf9e7e6 100644 --- a/products/views.py +++ b/products/views.py @@ -615,111 +615,13 @@ class YajinHuitiao(View): logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") return self.wechat_response('FAIL', '金额不一致') - # 7. 更新订单状态为已支付(3) - order.zhuangtai = 3 - order.save(update_fields=['zhuangtai']) - logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3") - - # 8. 更新打手押金 + from jituan.services.czjilu_fulfill import fulfill_yajin_recharge, CzjiluFulfillError try: - # 通过用户ID查找打手(高效查询) - dashou = UserDashou.query.get(user__UserUID=order.yonghuid) + fulfill_yajin_recharge(out_trade_no, paid_amount_yuan=wechat_amount_yuan) + except CzjiluFulfillError as exc: + logger.error('押金回调履约失败 %s: %s', out_trade_no, exc) + return self.wechat_response('FAIL', str(exc)) - # 增加押金(使用Decimal确保精度) - dashou.yajin += Decimal(str(order_amount_yuan)) - dashou.save(update_fields=['yajin']) - - logger.info(f"打手押金更新成功: 用户{order.yonghuid}, 增加{order_amount_yuan}元, 新押金{dashou.yajin}") - - # 8b. 押金分红(仅微信确认到账后;费率 CommissionRate Platform=12) - try: - - guanshi_id = dashou.yaoqingren - if not guanshi_id: - logger.info(f"押金分红跳过: 打手{order.yonghuid}无邀请管事") - elif Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists(): - logger.info(f"押金分红已处理: 订单{order.dingdan_id}") - else: - lilu_obj = CommissionRate.query.filter(Platform='12').first() - rate = lilu_obj.Rate if lilu_obj and lilu_obj.Rate is not None else Decimal('0') - if rate <= 0: - logger.info(f"押金分红跳过: 费率未配置或为0") - else: - recharge_amount = Decimal(str(order_amount_yuan)) - fenhong_jine = (recharge_amount * rate).quantize(Decimal('0.01')) - if fenhong_jine > 0: - with transaction.atomic(): - guanshi = UserGuanshi.objects.select_for_update().get( - user__UserUID=guanshi_id - ) - guanshi.yue += fenhong_jine - guanshi.chongzhifenrun += fenhong_jine - guanshi.save(update_fields=['yue', 'chongzhifenrun']) - - user_main = dashou.user - Gsfenhong.query.create( - dingdan_id=order.dingdan_id, - guanshi=guanshi_id, - dashouid=order.yonghuid, - shuoming='押金分红', - fenhong=fenhong_jine, - avatar=user_main.Avatar, - nicheng=dashou.nicheng or '未知打手', - fenhong_leixing=2, - club_id=resolve_gsfenhong_club_id( - dingdan_id=order.dingdan_id, - dashouid=order.yonghuid, - czjilu=order, - ), - ) - logger.info( - f"押金分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, " - f"打手{order.yonghuid}, 金额{fenhong_jine}元" - ) - try: - update_guanshi_daily_by_action( - yonghuid=guanshi_id, - action=4, - amount=fenhong_jine, - ) - except Exception as e: - logger.error(f"押金分红管事日统计更新失败: {e}") - except UserGuanshi.DoesNotExist: - logger.warning(f"押金分红跳过: 管事{guanshi_id}不存在") - except Exception as e: - logger.error(f"押金分红处理失败: {str(e)}", exc_info=True) - - except UserDashou.DoesNotExist: - # 如果找不到打手,记录错误,但依然返回成功(避免微信重复回调) - logger.error(f"打手信息不存在: 用户{order.yonghuid}") - return self.wechat_response('SUCCESS', 'OK') - except Exception as e: - logger.error(f"更新打手押金失败: {str(e)}", exc_info=True) - return self.wechat_response('FAIL', '更新押金失败') - - # ==== 在返回成功响应之前添加这段代码 ==== - - # 更新收支记录表 - try: - # 获取订单金额(确保是Decimal类型) - 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( - jine_decimal, - getattr(order, 'club_id', None), - biz_ref=f'cz:{order.dingdan_id}', - ) - - logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") - - except Exception as e: - # 收支记录更新失败不影响主流程,只记录日志 - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - - # ==== 添加结束 ==== - - # 9. 返回成功响应给微信 logger.info(f"押金支付回调处理完成: {out_trade_no}") return self.wechat_response('SUCCESS', 'OK') @@ -872,7 +774,19 @@ class YajinLunxun(APIView): 'message': '订单不存在或不属于当前用户' }, status=status.HTTP_400_BAD_REQUEST) - # 3. 检查订单状态是否为已支付(3) + # 3. 未支付则查微信并幂等履约 + if order.zhuangtai == 9: + from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError + try: + confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=2) + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID) + except CzjiluFulfillError as exc: + logger.info(f"押金确认未就绪 {dingdanid}: {exc}") + return Response({ + 'code': 400, + 'message': str(exc), + }, status=status.HTTP_400_BAD_REQUEST) + if order.zhuangtai != 3: logger.info(f"订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") return Response({ @@ -1205,59 +1119,13 @@ class JifenHuitiao(View): logger.error(f"积分金额错误: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") return self.wechat_response('FAIL', '金额错误') - # ==== 在返回成功响应之前添加这段代码 ==== - - # 更新收支记录表 + from jituan.services.czjilu_fulfill import fulfill_jifen_recharge, CzjiluFulfillError 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( - jine_decimal, - getattr(order, 'club_id', None), - biz_ref=f'cz:{order.dingdan_id}', - ) + fulfill_jifen_recharge(out_trade_no, paid_amount_yuan=wechat_amount_yuan) + except CzjiluFulfillError as exc: + logger.error('积分回调履约失败 %s: %s', out_trade_no, exc) + return self.wechat_response('FAIL', str(exc)) - logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") - - except Exception as e: - # 收支记录更新失败不影响主流程,只记录日志 - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - - # ==== 添加结束 ==== - - # 7. 更新订单状态为已支付(3) - order.zhuangtai = 3 - order.save(update_fields=['zhuangtai']) - logger.info(f"积分订单状态更新成功: {order.dingdan_id} -> 3") - - # 8. 更新打手积分 - try: - # 通过用户ID查找打手 - dashou = UserDashou.query.get(user__UserUID=order.yonghuid) - - # 检查积分是否小于10 - if dashou.jifen < 10: - # 增加5积分,但不超过10 - new_jifen = dashou.jifen + 5 - dashou.jifen = min(new_jifen, 10) # 确保不超过10 - dashou.save(update_fields=['jifen']) - - logger.info(f"打手积分更新成功: 用户{order.yonghuid}, 增加5积分, 新积分{dashou.jifen}") - - - - - else: - logger.warning(f"打手积分已满: 用户{order.yonghuid}, 当前积分{dashou.jifen}") - - except UserDashou.DoesNotExist: - logger.error(f"打手信息不存在: 用户{order.yonghuid}") - return self.wechat_response('SUCCESS', 'OK') - except Exception as e: - logger.error(f"更新打手积分失败: {str(e)}", exc_info=True) - return self.wechat_response('FAIL', '更新积分失败') - - # 9. 返回成功响应给微信 logger.info(f"积分支付回调处理完成: {out_trade_no}") return self.wechat_response('SUCCESS', 'OK') @@ -1421,7 +1289,19 @@ class JifenLunxun(APIView): 'message': '订单类型错误,不是积分订单' }, status=status.HTTP_400_BAD_REQUEST) - # 4. 检查订单状态是否为已支付(3) + # 4. 未支付则查微信并幂等履约 + if order.zhuangtai == 9: + from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError + try: + confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=3) + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID) + except CzjiluFulfillError as exc: + logger.info(f"积分确认未就绪 {dingdanid}: {exc}") + return Response({ + 'code': 400, + 'message': str(exc), + }, status=status.HTTP_400_BAD_REQUEST) + if order.zhuangtai != 3: logger.info(f"积分订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") return Response({