From 1c48779ff0241ebd75778fbeac78a246fe565f3f Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 24 Jun 2026 16:47:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=9A=E5=91=98=E5=88=86=E7=BA=A2?= =?UTF-8?q?=E6=8C=89=E8=B4=AD=E4=B9=B0=E6=AC=A1=E5=BA=8F=E4=B8=8Eduoci=5Ff?= =?UTF-8?q?enhong=E8=AE=A1=EF=BC=8C=E4=B8=A5=E6=A0=BC=E4=BF=B1=E4=B9=90?= =?UTF-8?q?=E9=83=A8=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jituan/services/club_config.py | 32 +-- jituan/services/member_recharge.py | 448 +++++++++++++++++++++++++++++ products/views.py | 351 +++------------------- 3 files changed, 500 insertions(+), 331 deletions(-) create mode 100644 jituan/services/member_recharge.py diff --git a/jituan/services/club_config.py b/jituan/services/club_config.py index ae882a7..31cab6c 100644 --- a/jituan/services/club_config.py +++ b/jituan/services/club_config.py @@ -1,34 +1,32 @@ -"""各俱乐部价格 / 利率覆盖(无覆盖时回落全局 huiyuan / lilubiao)。""" +"""各俱乐部价格 / 利率(严格按 club_id,不读全局 huiyuan 会员表)。""" from decimal import Decimal from jituan.constants import CLUB_ID_DEFAULT from jituan.models import ClubHuiyuanPrice, ClubLilubiao from orders.models import CommissionRate -from products.models import Huiyuan + + +def get_club_huiyuan_price_row(club_id, huiyuan_id, require_enabled=True): + club_id = club_id or CLUB_ID_DEFAULT + qs = ClubHuiyuanPrice.query.filter(club_id=club_id, huiyuan_id=huiyuan_id) + if require_enabled: + qs = qs.filter(is_enabled=True) + return qs.first() def get_huiyuan_price(club_id, huiyuan_id, huiyuan_obj=None): - club_id = club_id or CLUB_ID_DEFAULT - row = ClubHuiyuanPrice.query.filter( - club_id=club_id, huiyuan_id=huiyuan_id, is_enabled=True, - ).first() - if row: + """会员售价:仅 club_huiyuan_price。""" + row = get_club_huiyuan_price_row(club_id, huiyuan_id, require_enabled=True) + if row and row.jiage is not None: return Decimal(str(row.jiage)) - obj = huiyuan_obj or Huiyuan.query.filter(huiyuan_id=huiyuan_id).first() - if obj: - return Decimal(str(obj.jiage)) return Decimal('0') def get_huiyuan_fenchong(club_id, huiyuan_id, huiyuan_obj=None): - """返回 (guanshifc, zuzhangfc)。""" - club_id = club_id or CLUB_ID_DEFAULT - row = ClubHuiyuanPrice.query.filter(club_id=club_id, huiyuan_id=huiyuan_id).first() + """首次购买默认分成:仅 club_huiyuan_price(管事/组长各公司独立配置)。""" + row = get_club_huiyuan_price_row(club_id, huiyuan_id, require_enabled=False) if row: - return Decimal(str(row.guanshifc)), Decimal(str(row.zuzhangfc)) - obj = huiyuan_obj or Huiyuan.query.filter(huiyuan_id=huiyuan_id).first() - if obj: - return Decimal(str(obj.guanshifc)), Decimal(str(getattr(obj, 'zuzhangfc', 0) or 0)) + return Decimal(str(row.guanshifc or 0)), Decimal(str(row.zuzhangfc or 0)) return Decimal('0'), Decimal('0') diff --git a/jituan/services/member_recharge.py b/jituan/services/member_recharge.py new file mode 100644 index 0000000..0f5ae4b --- /dev/null +++ b/jituan/services/member_recharge.py @@ -0,0 +1,448 @@ +""" +会员微信充值履约(回调 / 前端确认共用,幂等)。 + +分红策略(定稿): +- 第几次购买 cishu:本俱乐部该打手该会员,已支付成功 czjilu 笔数(不含本单)+ 1。 + 与换管事无关,不看 gsfenhong,不看管事被分了几次。 +- cishu=1:先 duoci_fenhong(club_id+受益人+会员+cishu);无则 club_huiyuan_price 默认分成。 +- cishu>=2:仅 duoci_fenhong;无配置则不分(不读管事/组长用户表二次或永久分红字段)。 +- 价格与默认分成:仅 club_huiyuan_price + order.club_id,不读全局 huiyuan 表。 +""" +from __future__ import annotations + +import hashlib +import logging +import random +import string +from datetime import timedelta +from decimal import Decimal + +import defusedxml.ElementTree as ET +import requests +from django.db import transaction +from django.utils import timezone + +from backend.utils import ( + update_guanshi_daily_by_action, + update_guanshi_xufei_daily, + update_zuzhang_daily_by_action, +) +from jituan.constants import CLUB_ID_DEFAULT +from jituan.models import ClubHuiyuanPrice +from jituan.services.club_config import get_huiyuan_fenchong +from jituan.services.club_penalty import resolve_gsfenhong_club_id +from jituan.services.club_user import get_user_club_id +from jituan.services.wechat_pay import club_id_from_czjilu, get_wechat_v2_config +from orders.utils import update_daily_income +from products.models import Czjilu, DuociFenhong, Gsfenhong, Huiyuan, Huiyuangoumai +from users.business_models import User +from users.models import UserDashou, UserGuanshi, UserZuzhang + +logger = logging.getLogger(__name__) + +MEMBER_FENHONG_LEIXING = 1 + + +class MemberRechargeError(Exception): + def __init__(self, message, code='error'): + super().__init__(message) + self.code = code + + +def club_huiyuan_sellable(club_id, huiyuan_id): + """仅 club_huiyuan_price 可售;未配置则不可购买。""" + club_id = club_id or CLUB_ID_DEFAULT + row = ClubHuiyuanPrice.query.filter( + club_id=club_id, huiyuan_id=huiyuan_id, is_enabled=True, + ).first() + if row and row.jiage and Decimal(str(row.jiage)) > 0: + return True, Decimal(str(row.jiage)) + return False, Decimal('0') + + +def resolve_member_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id): + """ + 打手第几次购买该会员(= duoci_fenhong.cishu)。 + 同俱乐部 czjilu 已支付笔数(不含本单)+ 1。 + """ + club_id = club_id or CLUB_ID_DEFAULT + prior_paid = Czjilu.query.filter( + yonghuid=yonghuid, + huiyuan_id=huiyuan_id, + leixing=1, + zhuangtai=3, + club_id=club_id, + ).exclude(dingdan_id=current_dingdan_id).count() + return prior_paid + 1 + + +def _lookup_duoci_fenhong(club_id, huiyuan_id, beneficiary_id, cishu, field): + """有行则返回金额(含 0);无行返回 None 表示未配置。""" + try: + row = DuociFenhong.query.get( + club_id=club_id or CLUB_ID_DEFAULT, + huiyuan=huiyuan_id, + yonghuid=beneficiary_id, + cishu=cishu, + ) + return Decimal(str(getattr(row, field) or 0)) + except DuociFenhong.DoesNotExist: + return None + + +def calc_guanshi_member_fenhong( + huiyuan_id, guanshi_id, cishu, club_id, default_guanshi_fc, +): + """cishu>=2 只认 duoci_fenhong;cishu=1 可回落 club 默认分成。""" + club_id = club_id or CLUB_ID_DEFAULT + custom = _lookup_duoci_fenhong(club_id, huiyuan_id, guanshi_id, cishu, 'guanshi_fenhong') + if custom is not None: + return custom if custom > 0 else None + + if cishu == 1: + base = default_guanshi_fc if default_guanshi_fc is not None else Decimal('0') + return base if base > 0 else None + + return None + + +def calc_zuzhang_member_fenhong( + huiyuan_id, zuzhang_id, cishu, club_id, default_zuzhang_fc, +): + if not zuzhang_id: + return None + club_id = club_id or CLUB_ID_DEFAULT + custom = _lookup_duoci_fenhong(club_id, huiyuan_id, zuzhang_id, cishu, 'zuzhang_fenhong') + if custom is not None: + return custom if custom > 0 else None + + if cishu == 1: + base = default_zuzhang_fc if default_zuzhang_fc is not None else Decimal('0') + return base if base > 0 else None + + return None + + +def _apply_member_fenhong(order, huiyuan, purchase_cishu): + if Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists(): + logger.info('会员分红已处理,跳过: %s', order.dingdan_id) + return + + dashou = UserDashou.query.get(user__UserUID=order.yonghuid) + guanshi_id = dashou.yaoqingren + if not guanshi_id: + logger.info('打手%s无邀请管事,跳过会员分红', order.yonghuid) + return + + try: + guanshi = UserGuanshi.query.get(user__UserUID=guanshi_id) + except UserGuanshi.DoesNotExist: + logger.warning('管事不存在: %s', guanshi_id) + return + + zuzhang_id = guanshi.yaoqingren + zuzhang = None + if zuzhang_id: + try: + zuzhang = UserZuzhang.query.get(user__UserUID=zuzhang_id) + except UserZuzhang.DoesNotExist: + logger.warning('组长不存在: %s', zuzhang_id) + + club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT + huiyuan_id = huiyuan.huiyuan_id + cishu = purchase_cishu + guanshi_fc, zuzhang_fc = get_huiyuan_fenchong(club_id, huiyuan_id) + order_amount = Decimal(str(order.jine)) + + guanshi_fenhong = calc_guanshi_member_fenhong( + huiyuan_id, guanshi_id, cishu, club_id, guanshi_fc, + ) + guanshi_fenhong = guanshi_fenhong if guanshi_fenhong is not None else Decimal('0') + + zuzhang_fenhong = calc_zuzhang_member_fenhong( + huiyuan_id, zuzhang_id, cishu, club_id, zuzhang_fc, + ) + zuzhang_fenhong = zuzhang_fenhong if zuzhang_fenhong is not None else Decimal('0') + + total = guanshi_fenhong + zuzhang_fenhong + if total > order_amount: + logger.warning( + '分红总额%s超订单%s,组长置0', total, order_amount, + ) + zuzhang_fenhong = Decimal('0') + total = guanshi_fenhong + if total > order_amount: + logger.warning('管事分红%s超订单,置0', guanshi_fenhong) + guanshi_fenhong = Decimal('0') + + if guanshi_fenhong <= 0 and zuzhang_fenhong <= 0: + logger.info( + '本次无会员分红 dingdan=%s club=%s cishu=%s', + order.dingdan_id, club_id, cishu, + ) + return + + user_main = dashou.user + if guanshi_fenhong > 0: + guanshi.chongzhifenrun += guanshi_fenhong + guanshi.yue += guanshi_fenhong + guanshi.jinrichongzhi += 1 + guanshi.jinyuechongzhi += 1 + guanshi.save(update_fields=[ + 'chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi', + ]) + + if zuzhang_fenhong > 0 and zuzhang: + zuzhang.fenyong_zonge += zuzhang_fenhong + zuzhang.ketixian_jine += zuzhang_fenhong + zuzhang.jinri_fenyong += zuzhang_fenhong + zuzhang.jinyue_fenyong += zuzhang_fenhong + zuzhang.save(update_fields=[ + 'fenyong_zonge', 'ketixian_jine', 'jinri_fenyong', 'jinyue_fenyong', + ]) + + Gsfenhong.query.create( + dingdan_id=order.dingdan_id, + guanshi=guanshi_id, + dashouid=order.yonghuid, + shuoming=order.shuoming, + fenhong=guanshi_fenhong, + avatar=user_main.Avatar, + nicheng=dashou.nicheng or '未知打手', + zuzhang_id=zuzhang_id if zuzhang_fenhong > 0 else None, + zuzhang_fenhong=zuzhang_fenhong if zuzhang_fenhong > 0 else None, + huiyuan_id=huiyuan_id, + fenhong_leixing=MEMBER_FENHONG_LEIXING, + club_id=resolve_gsfenhong_club_id(dingdan_id=order.dingdan_id, czjilu=order), + ) + + if guanshi_fenhong > 0: + try: + if cishu == 1: + update_guanshi_daily_by_action( + yonghuid=guanshi_id, action=2, amount=guanshi_fenhong, + ) + else: + update_guanshi_xufei_daily( + yonghuid=guanshi_id, xufei_jine=guanshi_fenhong, + ) + except Exception as exc: + logger.error('管事日统计更新失败: %s', exc) + + if zuzhang_fenhong > 0 and zuzhang_id: + try: + update_zuzhang_daily_by_action( + yonghuid=zuzhang_id, action=2, amount=zuzhang_fenhong, + ) + except Exception as exc: + logger.error('组长日统计更新失败: %s', exc) + + logger.info( + '会员分红完成 dingdan=%s club=%s cishu=%s 管事=%s 组长=%s', + order.dingdan_id, club_id, cishu, guanshi_fenhong, zuzhang_fenhong, + ) + + +def _apply_huiyuan_goumai(yonghuid, huiyuan_id, huiyuan, club_id): + """返回 (record, is_first_purchase_of_member, is_first_buy_any_member)。""" + existing = Huiyuangoumai.query.filter( + yonghu_id=yonghuid, huiyuan_id=huiyuan_id, + ).first() + is_first_purchase = existing is None + is_first_buy_any = False + + if existing: + existing.daoqi_time += timedelta(days=30) + existing.huiyuan_zhuangtai = 1 + existing.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai', 'UpdateTime']) + return existing, False, False + + other = Huiyuangoumai.query.filter(yonghu_id=yonghuid).exclude( + huiyuan_id=huiyuan_id, + ).count() + if other == 0: + is_first_buy_any = True + + record = Huiyuangoumai.query.create( + yonghu_id=yonghuid, + huiyuan_id=huiyuan_id, + jieshao=huiyuan.jieshao, + huiyuan_zhuangtai=1, + daoqi_time=timezone.now() + timedelta(days=30), + club_id=club_id or get_user_club_id(User.query.filter(UserUID=yonghuid).first()), + ) + return record, is_first_purchase, is_first_buy_any + + +def _add_first_buy_jifen(yonghuid): + try: + dashou = UserDashou.query.get(user__UserUID=yonghuid) + dashou.jifen += 10 + dashou.save(update_fields=['jifen']) + except UserDashou.DoesNotExist: + logger.warning('首次购会员加积分失败,打手不存在: %s', yonghuid) + + +@transaction.atomic +def fulfill_member_recharge(dingdan_id, paid_amount_yuan=None, source='callback'): + """ + 幂等履约:仅当 czjilu.zhuangtai==9 时执行。 + 已处理 (zhuangtai!=9) 直接返回 already_done=True。 + """ + locked = Czjilu.query.filter(dingdan_id=dingdan_id).select_for_update() + order = locked.first() + if not order: + raise MemberRechargeError('订单不存在', 'not_found') + + if order.leixing != 1: + raise MemberRechargeError('订单类型不是会员充值', 'type_error') + + if order.zhuangtai != 9: + logger.info( + '会员订单已处理,跳过履约 dingdan=%s zhuangtai=%s source=%s', + dingdan_id, order.zhuangtai, source, + ) + return { + 'already_done': True, + 'dingdan_id': dingdan_id, + 'zhuangtai': order.zhuangtai, + } + + if paid_amount_yuan is not None: + paid = Decimal(str(paid_amount_yuan)) + order_amt = Decimal(str(order.jine)) + if abs(paid - order_amt) > Decimal('0.01'): + raise MemberRechargeError( + f'支付金额{paid}与订单{order_amt}不一致', 'amount_mismatch', + ) + + huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id) + club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT + + is_first_purchase_of_member = not Huiyuangoumai.query.filter( + yonghu_id=order.yonghuid, + huiyuan_id=order.huiyuan_id, + ).exists() + + # 必须在改 zhuangtai 之前算清第几次购买 + purchase_cishu = resolve_member_purchase_cishu( + order.yonghuid, order.huiyuan_id, order.dingdan_id, club_id, + ) + + jine_decimal = Decimal(str(order.jine)) + try: + update_daily_income(jine_decimal, club_id) + from jituan.services.szjilu_accounting import apply_szjilu_income + apply_szjilu_income(jine_decimal, club_id) + except Exception as exc: + logger.error('会员收支统计更新失败: %s', exc, exc_info=True) + + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + + huiyuan.goumai_cishu = (huiyuan.goumai_cishu or 0) + 1 + huiyuan.save(update_fields=['goumai_cishu']) + + goumai_record, _, is_first_buy_any = _apply_huiyuan_goumai( + order.yonghuid, order.huiyuan_id, huiyuan, club_id, + ) + if is_first_buy_any: + _add_first_buy_jifen(order.yonghuid) + + _apply_member_fenhong(order, huiyuan, purchase_cishu) + + logger.info( + '会员充值履约完成 dingdan=%s source=%s club=%s purchase_cishu=%s first_member=%s', + dingdan_id, source, club_id, purchase_cishu, is_first_purchase_of_member, + ) + return { + 'already_done': False, + 'dingdan_id': dingdan_id, + 'zhuangtai': 3, + 'purchase_cishu': purchase_cishu, + 'is_first_purchase_of_member': is_first_purchase_of_member, + 'daoqi_time': goumai_record.daoqi_time, + 'huiyuan_id': order.huiyuan_id, + } + + +def query_wechat_v2_trade_state(out_trade_no, club_id=None): + """微信 V2 查单,返回 (trade_state, transaction_id) 或 (None, None)。""" + club_id = club_id or club_id_from_czjilu(out_trade_no) + cfg = get_wechat_v2_config(club_id) + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + params = { + 'appid': cfg['appid'], + 'mch_id': cfg['mch_id'], + 'out_trade_no': out_trade_no, + 'nonce_str': nonce_str, + } + string_a = '&'.join(f'{k}={params[k]}' for k in sorted(params.keys())) + sign = hashlib.md5(f'{string_a}&key={cfg["key"]}'.encode()).hexdigest().upper() + params['sign'] = sign + xml_body = ''.join(f'<{k}>{params[k]}' for k in params) + xml_data = f'{xml_body}' + + try: + resp = requests.post( + 'https://api.mch.weixin.qq.com/pay/orderquery', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10, + ) + root = ET.fromstring(resp.content) + if root.find('return_code').text != 'SUCCESS': + return None, None + if root.find('result_code').text != 'SUCCESS': + return None, None + trade_state = root.find('trade_state').text + txn = root.find('transaction_id') + transaction_id = txn.text if txn is not None else None + return trade_state, transaction_id + except Exception as exc: + logger.error('微信查单失败 %s: %s', out_trade_no, exc) + return None, None + + +def confirm_member_recharge_paid(dingdan_id, yonghuid): + """ + 前端支付成功确认:先查微信 SUCCESS 再幂等履约。 + 已履约则直接返回成功,不与回调重复入账。 + """ + try: + order = Czjilu.query.get(dingdan_id=dingdan_id, yonghuid=yonghuid) + except Czjilu.DoesNotExist: + raise MemberRechargeError('订单不存在或不属于当前用户', 'not_found') + + if order.leixing != 1: + raise MemberRechargeError('不是会员订单', 'type_error') + + if order.zhuangtai == 3: + goumai = Huiyuangoumai.query.filter( + yonghu_id=yonghuid, huiyuan_id=order.huiyuan_id, + ).first() + if not goumai: + raise MemberRechargeError('订单已付但会员记录缺失,请联系客服', 'data_error') + return { + 'fulfilled': False, + 'already_done': True, + 'zhuangtai': 3, + 'daoqi_time': goumai.daoqi_time, + 'huiyuan_id': order.huiyuan_id, + } + + if order.zhuangtai != 9: + raise MemberRechargeError(f'订单状态异常: {order.zhuangtai}', 'status_error') + + trade_state, _ = query_wechat_v2_trade_state( + dingdan_id, getattr(order, 'club_id', None), + ) + if trade_state != 'SUCCESS': + raise MemberRechargeError('微信侧尚未支付成功,请稍后再试', 'not_paid') + + total_fee_yuan = Decimal(str(order.jine)) + result = fulfill_member_recharge( + dingdan_id, paid_amount_yuan=total_fee_yuan, source='confirm', + ) + result['fulfilled'] = not result.get('already_done') + return result diff --git a/products/views.py b/products/views.py index bc21f96..d74cff1 100644 --- a/products/views.py +++ b/products/views.py @@ -1503,6 +1503,15 @@ class HuiyuanGoumai(APIView): 'message': '会员信息不存在' }, status=status.HTTP_400_BAD_REQUEST) + club_id = resolve_club_id_from_request(request) + from jituan.services.member_recharge import club_huiyuan_sellable + sellable, club_price = club_huiyuan_sellable(club_id, huiyuanid) + if not sellable: + return Response({ + 'code': 400, + 'message': '该俱乐部未配置此会员价格,请联系管理员' + }, status=status.HTTP_400_BAD_REQUEST) + # 5. 生成订单ID(前缀CzHY表示会员充值) timestamp_ms = int(time.time() * 1000) timestamp_ns = int(time.time_ns() // 1000) % 1000000 @@ -1510,9 +1519,8 @@ class HuiyuanGoumai(APIView): dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" - # 6. 创建充值记录订单 - club_id = resolve_club_id_from_request(request) - jine = float(get_huiyuan_price(club_id, huiyuanid, huiyuan)) + # 6. 创建充值记录订单(金额仅用本俱乐部配置) + jine = float(club_price) # 组合订单说明:会员介绍 + 配置描述 shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}" @@ -1760,51 +1768,22 @@ class HuiyuanHuitiao(View): logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") return self.wechat_response('SUCCESS', 'OK') - # 验证金额 wechat_amount_yuan = total_fee / 100 order_amount_yuan = float(order.jine) if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") return self.wechat_response('FAIL', '金额不一致') - # 查询会员信息 + from jituan.services.member_recharge import fulfill_member_recharge, MemberRechargeError try: - huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id) - except Huiyuan.DoesNotExist: - logger.error(f"会员信息不存在: {order.huiyuan_id}") - return self.wechat_response('FAIL', '会员信息不存在') - - # ===== 更新收支记录 ===== - try: - jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - - update_daily_income(jine_decimal, getattr(order, 'club_id', None)) - from jituan.services.szjilu_accounting import apply_szjilu_income - apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None)) - logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") - except Exception as e: - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - - # 更新订单状态 - order.zhuangtai = 3 - order.save(update_fields=['zhuangtai']) - logger.info(f"会员订单状态更新成功: {order.dingdan_id} -> 3") - - # 更新会员购买次数 - huiyuan.goumai_cishu += 1 - huiyuan.save(update_fields=['goumai_cishu']) - - # 处理会员购买记录 - goumai_record, is_new, is_first_buy = self.process_huiyuan_goumai( - order.yonghuid, order.huiyuan_id, huiyuan - ) - - if is_first_buy: - self.add_first_buy_jifen(order.yonghuid) - - # 🆕 新增:处理分红(包含管事和组长) - with transaction.atomic(): - self.process_fenhong(order, huiyuan) + fulfill_member_recharge( + out_trade_no, + paid_amount_yuan=wechat_amount_yuan, + source='callback', + ) + except MemberRechargeError as exc: + logger.error('会员回调履约失败 %s: %s', out_trade_no, exc) + return self.wechat_response('FAIL', str(exc)) logger.info(f"会员支付回调处理完成: {out_trade_no}") return self.wechat_response('SUCCESS', 'OK') @@ -1816,278 +1795,8 @@ class HuiyuanHuitiao(View): logger.error(f"会员支付回调处理异常: {str(e)}", exc_info=True) return self.wechat_response('FAIL', '处理异常') - # ---------- 以下为原方法,基本不变,但需要导入新增模型 ---------- + # 履约逻辑见 jituan.services.member_recharge(回调与 /huiyuanlx 确认共用) - def process_huiyuan_goumai(self, yonghuid, huiyuan_id, huiyuan): - """处理会员购买记录(与原代码一致)""" - try: - existing_record = Huiyuangoumai.query.filter( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id - ).first() - - is_first_buy_any = False - - if existing_record: - existing_record.daoqi_time += timedelta(days=30) - existing_record.huiyuan_zhuangtai = 1 - existing_record.save() - logger.info(f"会员续费: 用户{yonghuid}, 会员{huiyuan_id}") - return existing_record, False, False - else: - other_huiyuan_count = Huiyuangoumai.query.filter( - yonghu_id=yonghuid - ).exclude(huiyuan_id=huiyuan_id).count() - if other_huiyuan_count == 0: - is_first_buy_any = True - logger.info(f"用户{yonghuid}首次购买任何会员,将获得10积分") - - goumai_record = Huiyuangoumai.query.create( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id, - jieshao=huiyuan.jieshao, - huiyuan_zhuangtai=1, - daoqi_time=timezone.now() + timedelta(days=30), - club_id=get_user_club_id(User.query.filter(UserUID=yonghuid).first()), - ) - logger.info(f"新购会员: 用户{yonghuid}, 会员{huiyuan_id}") - return goumai_record, True, is_first_buy_any - except Exception as e: - logger.error(f"处理会员购买记录失败: {str(e)}", exc_info=True) - if "unique" in str(e).lower() or "duplicate" in str(e).lower(): - logger.warning(f"检测到重复创建尝试,已由数据库拦截: 用户{yonghuid}, 会员{huiyuan_id}") - return self.process_huiyuan_goumai(yonghuid, huiyuan_id, huiyuan) - raise - - def add_first_buy_jifen(self, yonghuid): - """首次购买积分奖励(与原代码一致)""" - try: - dashou = UserDashou.query.get(user__UserUID=yonghuid) - dashou.jifen += 10 - dashou.save(update_fields=['jifen']) - logger.info(f"用户{yonghuid}首次购买会员,积分+10,当前积分: {dashou.jifen}") - except UserDashou.DoesNotExist: - logger.error(f"找不到打手记录,无法增加积分: {yonghuid}") - except Exception as e: - logger.error(f"增加积分失败: {str(e)}") - - # 🆕 新增:统一分红处理函数 - def process_fenhong(self, order, huiyuan): - """ - 处理管事和组长分红(支持多次分红配置) - """ - try: - # 获取打手信息 - dashou = UserDashou.query.get(user__UserUID=order.yonghuid) - guanshi_id = dashou.yaoqingren - if not guanshi_id: - logger.info(f"打手{order.yonghuid}没有邀请人,无分红") - return - - # 获取管事信息 - try: - guanshi = UserGuanshi.query.get(user__UserUID=guanshi_id) - except UserGuanshi.DoesNotExist: - logger.warning(f"管事信息不存在: {guanshi_id}") - return - - # 获取组长ID(从管事扩展表的邀请人) - zuzhang_id = guanshi.yaoqingren - - # 获取打手头像和昵称 - user_main = dashou.user - avatar = user_main.Avatar - nicheng = dashou.nicheng or "未知打手" - - # 查询该打手针对该管事的已分红次数 - existing_count = Gsfenhong.query.filter( - dashouid=order.yonghuid, - guanshi=guanshi_id - ).count() - cishu = existing_count + 1 # 本次是第几次 - - # 获取订单金额(用于限制分红总额) - order_amount = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - club_id = getattr(order, 'club_id', None) or resolve_club_id_for_write(user=dashou.user) - guanshi_fc, zuzhang_fc = get_huiyuan_fenchong(club_id, huiyuan.huiyuan_id, huiyuan) - - # 计算管事分红金额 - guanshi_fenhong = self._calc_guanshi_fenhong( - guanshi, huiyuan, guanshi_id, cishu, default_fc=guanshi_fc, - ) - if guanshi_fenhong is None: - logger.info(f"管事{guanshi_id}本次无分红") - guanshi_fenhong = Decimal('0.00') - - # 计算组长分红金额(如果存在组长) - zuzhang_fenhong = Decimal('0.00') - if zuzhang_id: - # 获取组长信息 - try: - zuzhang = UserZuzhang.query.get(user__UserUID=zuzhang_id) - except UserZuzhang.DoesNotExist: - logger.warning(f"组长信息不存在: {zuzhang_id}") - zuzhang = None - - zuzhang_fenhong = self._calc_zuzhang_fenhong( - zuzhang, huiyuan, zuzhang_id, cishu, default_fc=zuzhang_fc, - ) - if zuzhang_fenhong is None: - logger.info(f"组长{zuzhang_id}本次无分红") - zuzhang_fenhong = Decimal('0.00') - else: - logger.info(f"管事{guanshi_id}没有组长,无组长分红") - - # 检查分红总额是否超过订单金额(优先保证管事分红) - total_fenhong = guanshi_fenhong + zuzhang_fenhong - if total_fenhong > order_amount: - logger.warning(f"分红总额{total_fenhong}超过订单金额{order_amount},组长分红设为0") - zuzhang_fenhong = Decimal('0.00') - total_fenhong = guanshi_fenhong - if total_fenhong > order_amount: - logger.warning(f"管事分红{guanshi_fenhong}也超过订单金额,强制设为0") - guanshi_fenhong = Decimal('0.00') - total_fenhong = Decimal('0.00') - - # 创建分红记录(先创建主记录,即使金额为0也记录?但为0无意义,所以只有大于0才创建) - if guanshi_fenhong > 0 or zuzhang_fenhong > 0: - # 更新管事扩展表 - if guanshi_fenhong > 0: - guanshi.chongzhifenrun += guanshi_fenhong - guanshi.yue += guanshi_fenhong - guanshi.jinrichongzhi += 1 - guanshi.jinyuechongzhi += 1 - guanshi.save(update_fields=['chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi']) - logger.info(f"管事分红成功: 管事{guanshi_id}, 金额{guanshi_fenhong}") - - # 更新组长扩展表 - if zuzhang_fenhong > 0 and zuzhang: - zuzhang.fenyong_zonge += zuzhang_fenhong - zuzhang.ketixian_jine += zuzhang_fenhong - zuzhang.jinri_fenyong += zuzhang_fenhong - zuzhang.jinyue_fenyong += zuzhang_fenhong - zuzhang.save(update_fields=['fenyong_zonge', 'ketixian_jine', 'jinri_fenyong', 'jinyue_fenyong']) - logger.info(f"组长分红成功: 组长{zuzhang_id}, 金额{zuzhang_fenhong}") - - # 创建分红记录 - Gsfenhong.query.create( - dingdan_id=order.dingdan_id, - guanshi=guanshi_id, - dashouid=order.yonghuid, - shuoming=order.shuoming, - fenhong=guanshi_fenhong, - avatar=avatar, - nicheng=nicheng, - zuzhang_id=zuzhang_id if zuzhang_fenhong > 0 else None, - zuzhang_fenhong=zuzhang_fenhong if zuzhang_fenhong > 0 else None, - huiyuan_id=huiyuan.huiyuan_id, - fenhong_leixing=1, - club_id=resolve_gsfenhong_club_id( - dingdan_id=order.dingdan_id, - dashouid=order.yonghuid, - czjilu=order, - ), - ) - - - - # ========== 管事每日统计(新购) ========== - if guanshi_fenhong > 0 and cishu == 1: - try: - update_guanshi_daily_by_action( - yonghuid=guanshi_id, - action=2, - amount=guanshi_fenhong - ) - logger.info(f"管事每日统计更新成功(新购):管事{guanshi_id},分红{guanshi_fenhong}") - except Exception as e: - logger.error(f"管事每日统计更新失败(新购):{str(e)}") - - # ========== 管事每日统计(续费) ========== - if guanshi_fenhong > 0 and cishu > 1: - try: - update_guanshi_xufei_daily( - yonghuid=guanshi_id, - xufei_jine=guanshi_fenhong - ) - logger.info(f"管事续费统计更新成功:管事{guanshi_id},续费分红{guanshi_fenhong}") - except Exception as e: - logger.error(f"管事续费统计更新失败:{str(e)}") - - # ========== 组长每日统计(分佣收入) ========== - if zuzhang_fenhong > 0 and zuzhang_id: - try: - update_zuzhang_daily_by_action( - yonghuid=zuzhang_id, - action=2, - amount=zuzhang_fenhong - ) - logger.info(f"组长每日统计更新成功:组长{zuzhang_id},分佣{zuzhang_fenhong}") - except Exception as e: - logger.error(f"组长每日统计更新失败:{str(e)}") - - - logger.info(f"分红记录创建成功: 订单{order.dingdan_id}") - else: - logger.info(f"分红金额均为0,不创建记录") - - except Exception as e: - logger.error(f"处理分红失败: {str(e)}", exc_info=True) - # 不抛出异常,避免影响主流程 - - def _calc_guanshi_fenhong(self, guanshi, huiyuan, guanshi_id, cishu, default_fc=None): - """计算管事分红金额""" - # 先查多次分红配置表 - try: - cid = resolve_gsfenhong_club_id(dashouid=guanshi_id) - config = DuociFenhong.query.get( - club_id=cid, - huiyuan=huiyuan, - yonghuid=guanshi_id, - cishu=cishu, - ) - return Decimal(str(config.guanshi_fenhong)) - except DuociFenhong.DoesNotExist: - pass - - if cishu == 1: - base = default_fc if default_fc is not None else huiyuan.guanshifc - return Decimal(str(base)) - else: - # 第二次及以上,检查是否开启二次分红 - if guanshi.fenghong_erci_enabled: - return Decimal(str(guanshi.fenghong_erci)) - else: - return None # 无分红 - - def _calc_zuzhang_fenhong(self, zuzhang, huiyuan, zuzhang_id, cishu, default_fc=None): - """计算组长分红金额""" - if zuzhang is None: - return None - # 查多次分红配置表 - try: - cid = resolve_gsfenhong_club_id(dashouid=zuzhang_id) - config = DuociFenhong.query.get( - club_id=cid, - huiyuan=huiyuan, - yonghuid=zuzhang_id, - cishu=cishu, - ) - return Decimal(str(config.zuzhang_fenhong)) - except DuociFenhong.DoesNotExist: - pass - - if cishu == 1: - base = default_fc if default_fc is not None else huiyuan.zuzhangfc - return Decimal(str(base)) - else: - # 第二次及以上,检查组长是否开启额外分红 - if zuzhang.kaioi_ewai_fenhong: - return Decimal(str(zuzhang.ewai_fenhong_jine)) - else: - return None - - # 以下为原辅助方法,保持原样 def verify_signature(self, xml_data): """验证微信支付签名(与原代码一致)""" try: @@ -2236,9 +1945,23 @@ class HuiyuanLunxun(APIView): 'message': '订单类型错误,不是会员订单' }, status=status.HTTP_400_BAD_REQUEST) - # 4. 检查订单状态是否为已支付(3) + # 4. 未支付:查微信并幂等履约(与回调互斥,zhuangtai!=9 则跳过) + if order.zhuangtai == 9: + from jituan.services.member_recharge import ( + confirm_member_recharge_paid, + MemberRechargeError, + ) + try: + confirm_member_recharge_paid(dingdanid, request.user.UserUID) + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID) + except MemberRechargeError 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({ 'code': 400, 'message': '会员订单尚未支付完成'