fix: 会员分红按购买次序与duoci_fenhong计,严格俱乐部配置
This commit is contained in:
1 parent
00d2939457
commit
1c48779ff0
3 files changed
+500
-331
No files matched your search
@@ -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')
|
||||
|
||||
|
||||
|
||||
@@ -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]}</{k}>' for k in params)
|
||||
xml_data = f'<xml>{xml_body}</xml>'
|
||||
|
||||
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
|
||||
+37
-314
@@ -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': '会员订单尚未支付完成'
|
||||
|
||||
Reference in new issue
Block a user