fix: 会员充值链路加固(体验资格、积分分红、czjilu审计、余额抵扣)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
52
jituan/management/commands/backfill_member_trial_flags.py
Normal file
52
jituan/management/commands/backfill_member_trial_flags.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""回填会员体验标记:czjilu.is_trial + huiyuangoumai.has_used_trial。"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from jituan.services.member_recharge import (
|
||||
_order_is_trial_like,
|
||||
has_paid_trial_purchase,
|
||||
)
|
||||
from products.models import Czjilu, Huiyuangoumai
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '回填体验会员订单/权益标记(修复重复购买体验、正式计次错误)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='仅统计,不写库',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = bool(options.get('dry_run'))
|
||||
czjilu_fixed = 0
|
||||
goumai_fixed = 0
|
||||
|
||||
member_orders = Czjilu.query.filter(leixing=1, zhuangtai=3)
|
||||
with transaction.atomic():
|
||||
for order in member_orders.iterator():
|
||||
club_id = getattr(order, 'club_id', None)
|
||||
if _order_is_trial_like(order, club_id) and not order.is_trial:
|
||||
czjilu_fixed += 1
|
||||
if not dry_run:
|
||||
Czjilu.query.filter(dingdan_id=order.dingdan_id).update(is_trial=True)
|
||||
|
||||
goumai_rows = Huiyuangoumai.query.all()
|
||||
for row in goumai_rows.iterator():
|
||||
if row.has_used_trial:
|
||||
continue
|
||||
if has_paid_trial_purchase(row.yonghu_id, row.huiyuan_id, row.club_id):
|
||||
goumai_fixed += 1
|
||||
if not dry_run:
|
||||
Huiyuangoumai.query.filter(id=row.id).update(has_used_trial=True)
|
||||
|
||||
if dry_run:
|
||||
transaction.set_rollback(True)
|
||||
|
||||
mode = 'DRY-RUN' if dry_run else 'APPLIED'
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'[{mode}] czjilu.is_trial 修复 {czjilu_fixed} 条,'
|
||||
f'huiyuangoumai.has_used_trial 修复 {goumai_fixed} 条',
|
||||
))
|
||||
@@ -2,11 +2,12 @@
|
||||
会员微信充值履约(回调 / 前端确认共用,幂等)。
|
||||
|
||||
分红策略(定稿 v2):
|
||||
- formal_cishu:本俱乐部 + 打手 + 会员,czjilu 已支付正式单(is_trial=false)笔数 + 1。
|
||||
与换管事无关;体验单不计入。
|
||||
- formal_cishu:本俱乐部 + 打手 + 会员,已支付正式单笔数 + 1(体验单及历史脏数据不计入)。
|
||||
- formal_cishu=1:duoci_fenhong 或 club 默认正式分成。
|
||||
- formal_cishu>=2:仅 duoci_fenhong;无配置则不分。
|
||||
- 体验单:固定 trial_guanshifc / trial_zuzhangfc;不计入 formal_cishu。
|
||||
- 体验单:固定 trial_guanshifc / trial_zuzhangfc;终身仅可购 1 次。
|
||||
- 积分:体验 +5,正式 +5(体验后)或直接至 10(未买过体验),封顶 10。
|
||||
- 履约(回调/轮询/余额)均二次校验体验资格;分红失败回滚事务。
|
||||
|
||||
财务:每笔成功履约必须 czjilu.zhuangtai=3 + 收支入账(record_wechat_income_once)。
|
||||
"""
|
||||
@@ -45,6 +46,11 @@ MEMBER_FENHONG_LEIXING = 1
|
||||
MEMBER_LEIXING = 1
|
||||
MEMBER_PAID_STATUS = 3
|
||||
MEMBER_PENDING_STATUS = 9
|
||||
MEMBER_FAILED_STATUS = 8
|
||||
MEMBER_CANCELLED_STATUS = 2
|
||||
MEMBER_JIFEN_CAP = 10
|
||||
MEMBER_JIFEN_TRIAL_GRANT = 5
|
||||
MEMBER_JIFEN_FORMAL_GRANT = 5
|
||||
|
||||
|
||||
class MemberRechargeError(Exception):
|
||||
@@ -84,17 +90,70 @@ def club_huiyuan_sellable(club_id, huiyuan_id, is_trial=False):
|
||||
return True, price, days
|
||||
|
||||
|
||||
def has_formal_member_purchase(yonghuid, huiyuan_id, club_id):
|
||||
"""是否已有成功支付的正式会员单(用于禁止再买体验)。"""
|
||||
def _order_is_trial_like(order, club_id=None):
|
||||
"""识别体验单(含历史 is_trial 未回填的脏数据)。"""
|
||||
if bool(getattr(order, 'is_trial', False)):
|
||||
return True
|
||||
club_id = club_id or getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
||||
huiyuan_id = getattr(order, 'huiyuan_id', None)
|
||||
if not huiyuan_id:
|
||||
return False
|
||||
row = get_club_huiyuan_row(club_id, huiyuan_id, require_enabled=False)
|
||||
if not row or not row.trial_enabled:
|
||||
return False
|
||||
trial_price = Decimal(str(row.trial_price or 0))
|
||||
trial_days = int(row.trial_days or 0)
|
||||
if trial_price <= 0 or trial_days <= 0:
|
||||
return False
|
||||
order_jine = Decimal(str(order.jine or 0))
|
||||
if abs(order_jine - trial_price) > Decimal('0.01'):
|
||||
return False
|
||||
order_days = int(getattr(order, 'purchase_days', None) or 0)
|
||||
return order_days in (0, trial_days)
|
||||
|
||||
|
||||
def _iter_paid_member_orders(yonghuid, huiyuan_id, club_id, exclude_dingdan_id=None):
|
||||
club_id = club_id or CLUB_ID_DEFAULT
|
||||
return Czjilu.query.filter(
|
||||
qs = Czjilu.query.filter(
|
||||
yonghuid=yonghuid,
|
||||
huiyuan_id=huiyuan_id,
|
||||
leixing=MEMBER_LEIXING,
|
||||
zhuangtai=MEMBER_PAID_STATUS,
|
||||
club_id=club_id,
|
||||
is_trial=False,
|
||||
).exists()
|
||||
)
|
||||
if exclude_dingdan_id:
|
||||
qs = qs.exclude(dingdan_id=exclude_dingdan_id)
|
||||
return qs
|
||||
|
||||
|
||||
def has_paid_trial_purchase(yonghuid, huiyuan_id, club_id, exclude_dingdan_id=None):
|
||||
"""是否已有成功支付体验单(含历史脏数据)。"""
|
||||
club_id = club_id or CLUB_ID_DEFAULT
|
||||
for order in _iter_paid_member_orders(yonghuid, huiyuan_id, club_id, exclude_dingdan_id):
|
||||
if _order_is_trial_like(order, club_id):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def has_formal_member_purchase(yonghuid, huiyuan_id, club_id, exclude_dingdan_id=None):
|
||||
"""是否已有成功支付的正式会员单(用于禁止再买体验)。"""
|
||||
club_id = club_id or CLUB_ID_DEFAULT
|
||||
for order in _iter_paid_member_orders(yonghuid, huiyuan_id, club_id, exclude_dingdan_id):
|
||||
if not _order_is_trial_like(order, club_id):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _sync_trial_usage_flags(yonghuid, huiyuan_id, club_id):
|
||||
"""根据 czjilu 历史回填 huiyuangoumai.has_used_trial。"""
|
||||
goumai = Huiyuangoumai.query.filter(
|
||||
yonghu_id=yonghuid, huiyuan_id=huiyuan_id,
|
||||
).first()
|
||||
if not goumai:
|
||||
return
|
||||
if has_paid_trial_purchase(yonghuid, huiyuan_id, club_id) and not goumai.has_used_trial:
|
||||
goumai.has_used_trial = True
|
||||
goumai.save(update_fields=['has_used_trial', 'UpdateTime'])
|
||||
|
||||
|
||||
def can_purchase_trial(yonghuid, huiyuan_id, club_id):
|
||||
@@ -104,6 +163,11 @@ def can_purchase_trial(yonghuid, huiyuan_id, club_id):
|
||||
if not sellable:
|
||||
return False, '本俱乐部未开启体验会员'
|
||||
|
||||
_sync_trial_usage_flags(yonghuid, huiyuan_id, club_id)
|
||||
|
||||
if has_paid_trial_purchase(yonghuid, huiyuan_id, club_id):
|
||||
return False, '体验会员仅可购买一次'
|
||||
|
||||
goumai = Huiyuangoumai.query.filter(
|
||||
yonghu_id=yonghuid, huiyuan_id=huiyuan_id,
|
||||
).first()
|
||||
@@ -137,20 +201,61 @@ def validate_member_purchase(yonghuid, huiyuan_id, club_id, is_trial):
|
||||
def resolve_formal_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id):
|
||||
"""
|
||||
打手第几次正式购买该会员(duoci_fenhong.cishu)。
|
||||
仅统计 is_trial=false 的已支付 czjilu。
|
||||
体验单(含历史脏数据)不计入。
|
||||
"""
|
||||
club_id = club_id or CLUB_ID_DEFAULT
|
||||
prior_paid = Czjilu.query.filter(
|
||||
yonghuid=yonghuid,
|
||||
huiyuan_id=huiyuan_id,
|
||||
leixing=MEMBER_LEIXING,
|
||||
zhuangtai=MEMBER_PAID_STATUS,
|
||||
club_id=club_id,
|
||||
is_trial=False,
|
||||
).exclude(dingdan_id=current_dingdan_id).count()
|
||||
prior_paid = 0
|
||||
for order in _iter_paid_member_orders(yonghuid, huiyuan_id, club_id, current_dingdan_id):
|
||||
if not _order_is_trial_like(order, club_id):
|
||||
prior_paid += 1
|
||||
return prior_paid + 1
|
||||
|
||||
|
||||
def _truncate_shuoming(text, max_len=100):
|
||||
text = (text or '').strip()
|
||||
return text[:max_len] if text else ''
|
||||
|
||||
|
||||
def _mark_member_order_failed(order, reason, *, record_income=False, club_id=None):
|
||||
"""已收款或已扣款但履约失败:写失败状态,不发会员/积分/分红。"""
|
||||
club_id = club_id or getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
||||
base = (order.shuoming or '').strip()
|
||||
order.zhuangtai = MEMBER_FAILED_STATUS
|
||||
order.shuoming = _truncate_shuoming(f'{base} | 履约失败:{reason}')
|
||||
order.save(update_fields=['zhuangtai', 'shuoming', 'UpdateTime'])
|
||||
if record_income:
|
||||
try:
|
||||
_record_member_income(order, club_id)
|
||||
except MemberRechargeError:
|
||||
logger.error('失败单记收入异常 dingdan=%s', order.dingdan_id, exc_info=True)
|
||||
logger.error('会员订单履约失败 dingdan=%s reason=%s', order.dingdan_id, reason)
|
||||
|
||||
|
||||
def _require_paid_order_for_benefits(order):
|
||||
"""仅已支付(3)订单可发会员权益/积分/分红。"""
|
||||
if int(getattr(order, 'zhuangtai', 0) or 0) != MEMBER_PAID_STATUS:
|
||||
raise MemberRechargeError(
|
||||
f'订单未支付成功(zhuangtai={order.zhuangtai}),禁止发放权益',
|
||||
'not_paid',
|
||||
)
|
||||
|
||||
|
||||
def _validate_member_fulfillment(order, club_id):
|
||||
"""履约前二次校验(防止待支付旧单绕过下单校验)。"""
|
||||
club_id = club_id or CLUB_ID_DEFAULT
|
||||
is_trial = bool(getattr(order, 'is_trial', False))
|
||||
if is_trial or _order_is_trial_like(order, club_id):
|
||||
ok, msg = can_purchase_trial(order.yonghuid, order.huiyuan_id, club_id)
|
||||
if not ok:
|
||||
raise MemberRechargeError(msg or '体验会员不可购买', 'trial_not_allowed')
|
||||
return
|
||||
ok, msg, _, _ = validate_member_purchase(
|
||||
order.yonghuid, order.huiyuan_id, club_id, is_trial=False,
|
||||
)
|
||||
if not ok:
|
||||
raise MemberRechargeError(msg or '会员不可购买', 'purchase_not_allowed')
|
||||
|
||||
|
||||
# 兼容旧名
|
||||
def resolve_member_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id):
|
||||
return resolve_formal_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id)
|
||||
@@ -160,50 +265,54 @@ def fulfill_member_balance_purchase(czjilu, huiyuan, club_id=None):
|
||||
"""余额抵扣购买会员:按俱乐部配置天数履约,并计入购买次数/分红。"""
|
||||
club_id = club_id or getattr(czjilu, 'club_id', None) or CLUB_ID_DEFAULT
|
||||
is_trial = bool(getattr(czjilu, 'is_trial', False))
|
||||
purchase_days = int(getattr(czjilu, 'purchase_days', None) or 0)
|
||||
if purchase_days <= 0:
|
||||
_, _, purchase_days = club_huiyuan_sellable(club_id, czjilu.huiyuan_id, is_trial=is_trial)
|
||||
|
||||
formal_cishu = None
|
||||
trial_guanshi_fc = Decimal('0')
|
||||
trial_zuzhangfc = Decimal('0')
|
||||
if is_trial:
|
||||
row = get_club_huiyuan_row(club_id, czjilu.huiyuan_id)
|
||||
if row:
|
||||
trial_guanshi_fc = Decimal(str(row.trial_guanshifc or 0))
|
||||
trial_zuzhangfc = Decimal(str(row.trial_zuzhangfc or 0))
|
||||
else:
|
||||
formal_cishu = resolve_formal_purchase_cishu(
|
||||
czjilu.yonghuid, czjilu.huiyuan_id, czjilu.dingdan_id, club_id,
|
||||
raise MemberRechargeError(
|
||||
'体验会员仅支持微信支付,不可用佣金、分红或押金抵扣',
|
||||
'trial_balance_forbidden',
|
||||
)
|
||||
|
||||
goumai_record, is_first_buy_any = _apply_huiyuan_entitlement(
|
||||
czjilu.yonghuid, czjilu.huiyuan_id, huiyuan, club_id, is_trial, purchase_days,
|
||||
ok, err_msg, _, _ = validate_member_purchase(
|
||||
czjilu.yonghuid, czjilu.huiyuan_id, club_id, is_trial=False,
|
||||
)
|
||||
if is_first_buy_any:
|
||||
_add_first_buy_jifen(czjilu.yonghuid)
|
||||
if not ok:
|
||||
raise MemberRechargeError(err_msg or '会员不可购买', 'purchase_not_allowed')
|
||||
|
||||
purchase_days = int(getattr(czjilu, 'purchase_days', None) or 0)
|
||||
if purchase_days <= 0:
|
||||
_, _, purchase_days = club_huiyuan_sellable(club_id, czjilu.huiyuan_id, is_trial=False)
|
||||
|
||||
formal_cishu = resolve_formal_purchase_cishu(
|
||||
czjilu.yonghuid, czjilu.huiyuan_id, czjilu.dingdan_id, club_id,
|
||||
)
|
||||
trial_guanshi_fc = Decimal('0')
|
||||
trial_zuzhangfc = Decimal('0')
|
||||
|
||||
czjilu.zhuangtai = MEMBER_PAID_STATUS
|
||||
if not czjilu.purchase_days:
|
||||
czjilu.purchase_days = purchase_days
|
||||
czjilu.save(update_fields=['zhuangtai', 'purchase_days'])
|
||||
czjilu.is_trial = False
|
||||
czjilu.save(update_fields=['zhuangtai', 'purchase_days', 'is_trial'])
|
||||
_require_paid_order_for_benefits(czjilu)
|
||||
|
||||
goumai_record, _ = _apply_huiyuan_entitlement(
|
||||
czjilu.yonghuid, czjilu.huiyuan_id, huiyuan, club_id, False, purchase_days,
|
||||
)
|
||||
new_jifen = _apply_member_purchase_jifen(
|
||||
czjilu.yonghuid, is_trial=False, huiyuan_id=czjilu.huiyuan_id,
|
||||
club_id=club_id, current_dingdan_id=czjilu.dingdan_id,
|
||||
)
|
||||
|
||||
huiyuan.goumai_cishu = (huiyuan.goumai_cishu or 0) + 1
|
||||
huiyuan.save(update_fields=['goumai_cishu'])
|
||||
|
||||
try:
|
||||
_apply_member_fenhong(
|
||||
czjilu, huiyuan,
|
||||
formal_cishu=formal_cishu,
|
||||
trial_guanshi_fc=trial_guanshi_fc,
|
||||
trial_zuzhangfc=trial_zuzhangfc,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
'余额购会员分红失败 dingdan=%s: %s', czjilu.dingdan_id, exc, exc_info=True,
|
||||
)
|
||||
_apply_member_fenhong(
|
||||
czjilu, huiyuan,
|
||||
formal_cishu=formal_cishu,
|
||||
trial_guanshi_fc=trial_guanshi_fc,
|
||||
trial_zuzhangfc=trial_zuzhangfc,
|
||||
)
|
||||
|
||||
return goumai_record, is_first_buy_any
|
||||
return goumai_record, new_jifen
|
||||
|
||||
|
||||
def _lookup_duoci_fenhong(club_id, huiyuan_id, beneficiary_id, cishu, field):
|
||||
@@ -313,6 +422,8 @@ def _apply_huiyuan_entitlement(yonghuid, huiyuan_id, huiyuan, club_id, is_trial,
|
||||
existing.has_used_trial = True
|
||||
else:
|
||||
existing.is_trial = False
|
||||
if has_paid_trial_purchase(yonghuid, huiyuan_id, club_id):
|
||||
existing.has_used_trial = True
|
||||
existing.save(update_fields=[
|
||||
'daoqi_time', 'huiyuan_zhuangtai', 'jieshao', 'is_trial', 'has_used_trial', 'UpdateTime',
|
||||
])
|
||||
@@ -321,6 +432,7 @@ def _apply_huiyuan_entitlement(yonghuid, huiyuan_id, huiyuan, club_id, is_trial,
|
||||
other = Huiyuangoumai.query.filter(yonghu_id=yonghuid).exclude(huiyuan_id=huiyuan_id).count()
|
||||
if other == 0:
|
||||
is_first_buy_any = True
|
||||
used_trial_before = has_paid_trial_purchase(yonghuid, huiyuan_id, club_id)
|
||||
resolved_club = club_id or get_user_club_id(User.query.filter(UserUID=yonghuid).first())
|
||||
record = Huiyuangoumai.query.create(
|
||||
yonghu_id=yonghuid,
|
||||
@@ -330,12 +442,18 @@ def _apply_huiyuan_entitlement(yonghuid, huiyuan_id, huiyuan, club_id, is_trial,
|
||||
daoqi_time=new_daoqi,
|
||||
club_id=resolved_club,
|
||||
is_trial=is_trial,
|
||||
has_used_trial=is_trial,
|
||||
has_used_trial=is_trial or used_trial_before,
|
||||
)
|
||||
return record, is_first_buy_any
|
||||
|
||||
|
||||
def _apply_member_fenhong(order, huiyuan, formal_cishu=None, trial_guanshi_fc=None, trial_zuzhang_fc=None):
|
||||
if int(getattr(order, 'zhuangtai', 0) or 0) != MEMBER_PAID_STATUS:
|
||||
logger.warning(
|
||||
'订单未支付成功,禁止分红 dingdan=%s zhuangtai=%s',
|
||||
order.dingdan_id, order.zhuangtai,
|
||||
)
|
||||
return
|
||||
if Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists():
|
||||
logger.info('会员分红已处理,跳过: %s', order.dingdan_id)
|
||||
return
|
||||
@@ -368,7 +486,7 @@ def _apply_member_fenhong(order, huiyuan, formal_cishu=None, trial_guanshi_fc=No
|
||||
club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
||||
huiyuan_id = huiyuan.huiyuan_id
|
||||
order_amount = Decimal(str(order.jine))
|
||||
is_trial_order = bool(getattr(order, 'is_trial', False))
|
||||
is_trial_order = _order_is_trial_like(order, club_id)
|
||||
|
||||
if is_trial_order:
|
||||
guanshi_fenhong = Decimal(str(trial_guanshi_fc or 0))
|
||||
@@ -460,13 +578,55 @@ def _apply_member_fenhong(order, huiyuan, formal_cishu=None, trial_guanshi_fc=No
|
||||
)
|
||||
|
||||
|
||||
def _add_first_buy_jifen(yonghuid):
|
||||
def _apply_member_purchase_jifen(yonghuid, is_trial, huiyuan_id, club_id, current_dingdan_id=None):
|
||||
"""
|
||||
会员购积分:体验 +5,正式 +5(体验后)或直接到 10(未买过体验),封顶 10。
|
||||
"""
|
||||
try:
|
||||
dashou = UserDashou.query.get(user__UserUID=yonghuid)
|
||||
dashou.jifen += 10
|
||||
dashou.save(update_fields=['jifen'])
|
||||
except UserDashou.DoesNotExist:
|
||||
logger.warning('首次购会员加积分失败,打手不存在: %s', yonghuid)
|
||||
logger.warning('会员购积分失败,打手不存在: %s', yonghuid)
|
||||
return 0
|
||||
|
||||
current = int(dashou.jifen or 0)
|
||||
if current >= MEMBER_JIFEN_CAP:
|
||||
return current
|
||||
|
||||
if is_trial:
|
||||
delta = min(MEMBER_JIFEN_TRIAL_GRANT, MEMBER_JIFEN_CAP - current)
|
||||
else:
|
||||
had_trial = has_paid_trial_purchase(
|
||||
yonghuid, huiyuan_id, club_id, exclude_dingdan_id=current_dingdan_id,
|
||||
)
|
||||
if had_trial:
|
||||
delta = min(MEMBER_JIFEN_FORMAL_GRANT, MEMBER_JIFEN_CAP - current)
|
||||
else:
|
||||
delta = MEMBER_JIFEN_CAP - current
|
||||
|
||||
if delta <= 0:
|
||||
return current
|
||||
|
||||
dashou.jifen = min(current + delta, MEMBER_JIFEN_CAP)
|
||||
dashou.save(update_fields=['jifen'])
|
||||
logger.info(
|
||||
'会员购积分 yonghu=%s is_trial=%s huiyuan=%s +%s -> %s',
|
||||
yonghuid, is_trial, huiyuan_id, delta, dashou.jifen,
|
||||
)
|
||||
return dashou.jifen
|
||||
|
||||
|
||||
def _add_first_buy_jifen(yonghuid):
|
||||
"""兼容旧调用:等价于直接买正式且从未购会员积分。"""
|
||||
logger.warning('_add_first_buy_jifen 已废弃,请改用 _apply_member_purchase_jifen')
|
||||
try:
|
||||
dashou = UserDashou.query.get(user__UserUID=yonghuid)
|
||||
except UserDashou.DoesNotExist:
|
||||
return
|
||||
current = int(dashou.jifen or 0)
|
||||
if current >= MEMBER_JIFEN_CAP:
|
||||
return
|
||||
dashou.jifen = MEMBER_JIFEN_CAP
|
||||
dashou.save(update_fields=['jifen'])
|
||||
|
||||
|
||||
def repair_member_entitlement_if_paid(dingdan_id):
|
||||
@@ -488,11 +648,34 @@ def repair_member_entitlement_if_paid(dingdan_id):
|
||||
|
||||
club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
||||
days = order.purchase_days or 30
|
||||
is_trial = _order_is_trial_like(order, club_id)
|
||||
with transaction.atomic():
|
||||
_apply_huiyuan_entitlement(
|
||||
order.yonghuid, order.huiyuan_id, huiyuan, club_id,
|
||||
bool(getattr(order, 'is_trial', False)), days,
|
||||
order.yonghuid, order.huiyuan_id, huiyuan, club_id, is_trial, days,
|
||||
)
|
||||
_apply_member_purchase_jifen(
|
||||
order.yonghuid, is_trial=is_trial, huiyuan_id=order.huiyuan_id,
|
||||
club_id=club_id, current_dingdan_id=order.dingdan_id,
|
||||
)
|
||||
if not Gsfenhong.query.filter(dingdan_id=dingdan_id, fenhong_leixing=MEMBER_FENHONG_LEIXING).exists():
|
||||
trial_guanshi_fc = Decimal('0')
|
||||
trial_zuzhang_fc = Decimal('0')
|
||||
formal_cishu = None
|
||||
if is_trial:
|
||||
row = get_club_huiyuan_row(club_id, order.huiyuan_id)
|
||||
if row:
|
||||
trial_guanshi_fc = Decimal(str(row.trial_guanshifc or 0))
|
||||
trial_zuzhang_fc = Decimal(str(row.trial_zuzhangfc or 0))
|
||||
else:
|
||||
formal_cishu = resolve_formal_purchase_cishu(
|
||||
order.yonghuid, order.huiyuan_id, order.dingdan_id, club_id,
|
||||
)
|
||||
_apply_member_fenhong(
|
||||
order, huiyuan,
|
||||
formal_cishu=formal_cishu,
|
||||
trial_guanshi_fc=trial_guanshi_fc,
|
||||
trial_zuzhang_fc=trial_zuzhang_fc,
|
||||
)
|
||||
logger.warning('已补开通会员 dingdan=%s', dingdan_id)
|
||||
return True
|
||||
|
||||
@@ -536,7 +719,21 @@ def fulfill_member_recharge(dingdan_id, paid_amount_yuan=None, source='callback'
|
||||
raise MemberRechargeError(f'会员不存在: {order.huiyuan_id}', 'huiyuan_not_found')
|
||||
|
||||
club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
||||
is_trial = bool(getattr(order, 'is_trial', False))
|
||||
try:
|
||||
_validate_member_fulfillment(order, club_id)
|
||||
except MemberRechargeError as exc:
|
||||
_mark_member_order_failed(
|
||||
order, str(exc),
|
||||
record_income=paid_amount_yuan is not None,
|
||||
club_id=club_id,
|
||||
)
|
||||
raise
|
||||
|
||||
is_trial = _order_is_trial_like(order, club_id)
|
||||
if is_trial and not order.is_trial:
|
||||
order.is_trial = True
|
||||
order.save(update_fields=['is_trial'])
|
||||
|
||||
purchase_days = order.purchase_days or (30 if not is_trial else 0)
|
||||
if purchase_days <= 0:
|
||||
_, _, purchase_days = club_huiyuan_sellable(club_id, order.huiyuan_id, is_trial=is_trial)
|
||||
@@ -554,31 +751,33 @@ def fulfill_member_recharge(dingdan_id, paid_amount_yuan=None, source='callback'
|
||||
order.yonghuid, order.huiyuan_id, order.dingdan_id, club_id,
|
||||
)
|
||||
|
||||
goumai_record, is_first_buy_any = _apply_huiyuan_entitlement(
|
||||
order.zhuangtai = MEMBER_PAID_STATUS
|
||||
if not order.purchase_days:
|
||||
order.purchase_days = purchase_days
|
||||
if is_trial:
|
||||
order.is_trial = True
|
||||
order.save(update_fields=['zhuangtai', 'purchase_days', 'is_trial'])
|
||||
_require_paid_order_for_benefits(order)
|
||||
|
||||
goumai_record, _ = _apply_huiyuan_entitlement(
|
||||
order.yonghuid, order.huiyuan_id, huiyuan, club_id, is_trial, purchase_days,
|
||||
)
|
||||
if is_first_buy_any:
|
||||
_add_first_buy_jifen(order.yonghuid)
|
||||
|
||||
order.zhuangtai = MEMBER_PAID_STATUS
|
||||
order.save(update_fields=['zhuangtai'])
|
||||
_apply_member_purchase_jifen(
|
||||
order.yonghuid, is_trial=is_trial, huiyuan_id=order.huiyuan_id,
|
||||
club_id=club_id, current_dingdan_id=order.dingdan_id,
|
||||
)
|
||||
|
||||
huiyuan.goumai_cishu = (huiyuan.goumai_cishu or 0) + 1
|
||||
huiyuan.save(update_fields=['goumai_cishu'])
|
||||
|
||||
_record_member_income(order, club_id)
|
||||
|
||||
try:
|
||||
_apply_member_fenhong(
|
||||
order, huiyuan,
|
||||
formal_cishu=formal_cishu,
|
||||
trial_guanshi_fc=trial_guanshi_fc,
|
||||
trial_zuzhang_fc=trial_zuzhang_fc,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
'会员分红失败但会员已开通 dingdan=%s err=%s', dingdan_id, exc, exc_info=True,
|
||||
)
|
||||
_apply_member_fenhong(
|
||||
order, huiyuan,
|
||||
formal_cishu=formal_cishu,
|
||||
trial_guanshi_fc=trial_guanshi_fc,
|
||||
trial_zuzhang_fc=trial_zuzhang_fc,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
'会员充值履约完成 dingdan=%s source=%s club=%s is_trial=%s formal_cishu=%s jine=%s',
|
||||
@@ -689,14 +888,37 @@ def dashou_in_active_trial_period(yonghuid):
|
||||
).exists()
|
||||
|
||||
|
||||
def extend_member_entitlement_admin(yonghuid, huiyuan_id, days, club_id=None):
|
||||
"""后台赠送正式会员(无 czjilu、无分红)。"""
|
||||
def extend_member_entitlement_admin(yonghuid, huiyuan_id, days, club_id=None, operator_note=''):
|
||||
"""后台赠送正式会员:写 czjilu 审计记录,无支付、无分红、无积分。"""
|
||||
try:
|
||||
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
|
||||
except Huiyuan.DoesNotExist:
|
||||
return None, '会员类型不存在'
|
||||
days = max(1, int(days))
|
||||
club_id = club_id or CLUB_ID_DEFAULT
|
||||
record, _ = _apply_huiyuan_entitlement(
|
||||
yonghuid, huiyuan_id, huiyuan, club_id, is_trial=False, purchase_days=days,
|
||||
)
|
||||
|
||||
timestamp = str(int(timezone.now().timestamp() * 1000))[-12:]
|
||||
rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
|
||||
dingdan_id = f'ADM{timestamp}{rand_str}'
|
||||
note = (operator_note or '').strip()
|
||||
shuoming = f'后台赠送会员 {huiyuan.jieshao} {days}天'
|
||||
if note:
|
||||
shuoming = _truncate_shuoming(f'{shuoming} | {note}')
|
||||
|
||||
Czjilu.query.create(
|
||||
dingdan_id=dingdan_id,
|
||||
zhuangtai=MEMBER_PAID_STATUS,
|
||||
yonghuid=yonghuid,
|
||||
jine=Decimal('0'),
|
||||
leixing=MEMBER_LEIXING,
|
||||
shuoming=shuoming,
|
||||
huiyuan_id=huiyuan_id,
|
||||
club_id=club_id,
|
||||
is_trial=False,
|
||||
purchase_days=days,
|
||||
)
|
||||
logger.info('后台赠送会员 yonghu=%s huiyuan=%s days=%s dingdan=%s', yonghuid, huiyuan_id, days, dingdan_id)
|
||||
return record, None
|
||||
|
||||
@@ -13,6 +13,8 @@ from jituan.services.member_recharge import (
|
||||
MEMBER_LEIXING,
|
||||
MEMBER_PAID_STATUS,
|
||||
MEMBER_PENDING_STATUS,
|
||||
MEMBER_FAILED_STATUS,
|
||||
MEMBER_CANCELLED_STATUS,
|
||||
resolve_formal_purchase_cishu,
|
||||
)
|
||||
from jituan.constants import DATA_SCOPE_ALL
|
||||
@@ -22,6 +24,8 @@ from users.models import UserDashou, UserBoss
|
||||
MEMBER_STATUS_LABELS = {
|
||||
MEMBER_PAID_STATUS: '已支付',
|
||||
MEMBER_PENDING_STATUS: '待支付',
|
||||
MEMBER_FAILED_STATUS: '履约失败',
|
||||
MEMBER_CANCELLED_STATUS: '已取消',
|
||||
}
|
||||
|
||||
|
||||
@@ -300,6 +304,8 @@ def build_member_recharge_records_payload(request, filters=None, page=1, page_si
|
||||
'zhuangtai_options': [
|
||||
{'value': MEMBER_PAID_STATUS, 'label': '已支付'},
|
||||
{'value': MEMBER_PENDING_STATUS, 'label': '待支付'},
|
||||
{'value': MEMBER_FAILED_STATUS, 'label': '履约失败'},
|
||||
{'value': MEMBER_CANCELLED_STATUS, 'label': '已取消'},
|
||||
],
|
||||
},
|
||||
'scope': resolve_club_scope(request),
|
||||
|
||||
Reference in New Issue
Block a user