fix: 会员充值链路加固(体验资格、积分分红、czjilu审计、余额抵扣)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-08 00:07:54 +08:00
parent 1e3d65e0e4
commit 7dc78acb73
6 changed files with 428 additions and 131 deletions

View 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}',
))

View File

@@ -2,11 +2,12 @@
会员微信充值履约(回调 / 前端确认共用,幂等)。
分红策略(定稿 v2
- formal_cishu本俱乐部 + 打手 + 会员,czjilu 已支付正式单(is_trial=false)笔数 + 1
与换管事无关;体验单不计入。
- formal_cishu本俱乐部 + 打手 + 会员,已支付正式单笔数 + 1体验单及历史脏数据不计入
- formal_cishu=1duoci_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

View File

@@ -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),

View File

@@ -879,6 +879,7 @@ class DsqrgmdhView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
from jituan.services.member_recharge import MemberRechargeError
try:
leixing = request.data.get('leixing')
shenfen_id = request.data.get('shenfen_id')
@@ -969,6 +970,7 @@ class DsqrgmdhView(APIView):
return Response({'code': 400, 'msg': f'{identity["name"]}余额不足,需要{required}元,当前{balance}'}, status=status.HTTP_400_BAD_REQUEST)
try:
fulfill_error = None
with transaction.atomic():
locked_profile = identity['model'].objects.select_for_update().get(id=profile.id)
current_balance = getattr(locked_profile, identity['balance_field'])
@@ -995,7 +997,8 @@ class DsqrgmdhView(APIView):
shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}"
cz_leixing = leixing
cz_club_id = club_id
cz_zhuangtai = 1
from jituan.services.member_recharge import MEMBER_PENDING_STATUS, MEMBER_FAILED_STATUS, MEMBER_PAID_STATUS
cz_zhuangtai = MEMBER_PENDING_STATUS
if leixing == 4:
from jituan.services.club_penalty import penalty_cz_club_id
from products.czjilu_types import (
@@ -1031,35 +1034,52 @@ class DsqrgmdhView(APIView):
czjilu = Czjilu.query.create(**czjilu_kwargs)
response_data = {}
response_data = {'dingdan_id': dingdan_id}
fulfill_error = None
if leixing == 1:
from jituan.services.member_recharge import fulfill_member_balance_purchase
huiyuan_record, is_first_buy = fulfill_member_balance_purchase(
czjilu, huiyuan, club_id=club_id,
from jituan.services.member_recharge import (
fulfill_member_balance_purchase,
validate_member_purchase,
)
response_data['huiyuan'] = {
'huiyuanid': huiyuan_id,
'huiyuanming': huiyuan.jieshao,
'daoqi': huiyuan_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(huiyuan_record, 'daoqi_time') else '',
}
if is_first_buy:
try:
dashou = UserDashou.query.get(user=user)
dashou.jifen += 10
dashou.save(update_fields=['jifen'])
response_data['jifen'] = dashou.jifen
except UserDashou.DoesNotExist:
pass
try:
ok, err_msg, _, _ = validate_member_purchase(
yonghuid, huiyuan_id, club_id, is_trial=False,
)
if not ok:
raise MemberRechargeError(err_msg or '会员不可购买', 'purchase_not_allowed')
huiyuan_record, new_jifen = fulfill_member_balance_purchase(
czjilu, huiyuan, club_id=club_id,
)
response_data['huiyuan'] = {
'huiyuanid': huiyuan_id,
'huiyuanming': huiyuan.jieshao,
'daoqi': huiyuan_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(huiyuan_record, 'daoqi_time') else '',
}
response_data['jifen'] = new_jifen
except MemberRechargeError as exc:
setattr(locked_profile, identity['balance_field'], current_balance)
locked_profile.save(update_fields=[identity['balance_field']])
czjilu.zhuangtai = MEMBER_FAILED_STATUS
czjilu.shuoming = f"{shuoming} | 履约失败:{str(exc)[:40]}"[:100]
czjilu.save(update_fields=['zhuangtai', 'shuoming'])
fulfill_error = exc
elif leixing == 2:
try:
dashou = UserDashou.objects.select_for_update().get(user=user)
dashou.yajin += yajin_jine
dashou.save(update_fields=['yajin'])
czjilu.zhuangtai = MEMBER_PAID_STATUS
czjilu.save(update_fields=['zhuangtai'])
response_data['yajin'] = dashou.yajin
except UserDashou.DoesNotExist:
return Response({'code': 500, 'msg': '打手信息不存在,无法充值押金'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
setattr(locked_profile, identity['balance_field'], current_balance)
locked_profile.save(update_fields=[identity['balance_field']])
czjilu.zhuangtai = MEMBER_FAILED_STATUS
czjilu.shuoming = f"{shuoming} | 履约失败:打手不存在"[:100]
czjilu.save(update_fields=['zhuangtai', 'shuoming'])
fulfill_error = MemberRechargeError('打手信息不存在,无法充值押金', 'dashou_not_found')
elif leixing == 4:
if fadan_obj.Status not in [1, 3]:
@@ -1082,14 +1102,29 @@ class DsqrgmdhView(APIView):
new_jifen = dashou.jifen + INTEGRAL_INCREMENT
dashou.jifen = min(new_jifen, MAX_INTEGRAL)
dashou.save(update_fields=['jifen'])
czjilu.zhuangtai = MEMBER_PAID_STATUS
czjilu.save(update_fields=['zhuangtai'])
response_data['jifen'] = dashou.jifen
except UserDashou.DoesNotExist:
return Response({'code': 500, 'msg': '打手信息不存在,无法补充积分'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
setattr(locked_profile, identity['balance_field'], current_balance)
locked_profile.save(update_fields=[identity['balance_field']])
czjilu.zhuangtai = MEMBER_FAILED_STATUS
czjilu.shuoming = f"{shuoming} | 履约失败:打手不存在"[:100]
czjilu.save(update_fields=['zhuangtai', 'shuoming'])
fulfill_error = MemberRechargeError('打手信息不存在,无法补充积分', 'dashou_not_found')
except Exception as e:
logger.error(f"事务处理异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': f'支付处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if fulfill_error:
logger.warning('余额抵扣履约失败 dingdan=%s err=%s', dingdan_id, fulfill_error)
return Response({
'code': 400,
'msg': str(fulfill_error),
'data': response_data,
}, status=status.HTTP_400_BAD_REQUEST)
final_profile = identity['model'].query.get(user=user)
response_data['balance'] = str(getattr(final_profile, identity['balance_field']))

View File

@@ -168,8 +168,15 @@ class HuiyuanGoumai(APIView):
try:
pay_openid, _ = get_payment_openid(request)
if not pay_openid:
chongzhi_jilu.delete()
return Response({'code': 400, 'message': '用户openid不存在'}, status=status.HTTP_400_BAD_REQUEST)
from jituan.services.member_recharge import MEMBER_CANCELLED_STATUS
chongzhi_jilu.zhuangtai = MEMBER_CANCELLED_STATUS
chongzhi_jilu.shuoming = f"{shuoming} | 下单失败:openid不存在"[:100]
chongzhi_jilu.save(update_fields=['zhuangtai', 'shuoming'])
return Response({
'code': 400,
'message': '用户openid不存在',
'dingdanid': dingdanid,
}, status=status.HTTP_400_BAD_REQUEST)
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
@@ -177,8 +184,10 @@ class HuiyuanGoumai(APIView):
pay_type='huiyuan'
)
except Exception as e:
# 支付参数生成失败,删除订单记录
chongzhi_jilu.delete()
from jituan.services.member_recharge import MEMBER_CANCELLED_STATUS
chongzhi_jilu.zhuangtai = MEMBER_CANCELLED_STATUS
chongzhi_jilu.shuoming = f"{shuoming} | 下单失败:{str(e)[:40]}"[:100]
chongzhi_jilu.save(update_fields=['zhuangtai', 'shuoming'])
logger.error(f"生成会员支付参数失败: {str(e)}")
return Response({
'code': 500,

View File

@@ -290,26 +290,7 @@ class DashouHuiyuanList(APIView):
)
} if _huiyuan_ids else {}
# 批量预取 2当前用户已购 Huiyuangoumai按 huiyuan_id 索引
_goumai_map = {
gm.huiyuan_id: gm
for gm in Huiyuangoumai.query.filter(
yonghu_id=yonghuid,
huiyuan_id__in=_huiyuan_ids,
)
} if _huiyuan_ids else {}
# 批量预取 3当前用户在本俱乐部已支付正式会员的 huiyuan_id 集合
_formal_paid_set = set(
Czjilu.query.filter(
yonghuid=yonghuid,
huiyuan_id__in=_huiyuan_ids,
leixing=1,
zhuangtai=3,
club_id=club_id,
is_trial=False,
).values_list('huiyuan_id', flat=True)
) if _huiyuan_ids else set()
from jituan.services.member_recharge import can_purchase_trial
def _local_sellable(cp_row, is_trial):
"""单行 ClubHuiyuanPrice 计算 sellable/price/days避免重复查询"""
@@ -330,17 +311,9 @@ class DashouHuiyuanList(APIView):
return True, price, days
def _local_can_trial(huiyuan_id):
"""基于已预取的数据判断体验会员是否可购"""
cp_row = _club_price_map.get(huiyuan_id)
trial_sellable, _, _ = _local_sellable(cp_row, is_trial=True)
if not trial_sellable:
return False
goumai = _goumai_map.get(huiyuan_id)
if goumai and goumai.has_used_trial:
return False
if huiyuan_id in _formal_paid_set:
return False
return True
"""与下单/履约共用 can_purchase_trial"""
ok, _ = can_purchase_trial(yonghuid, huiyuan_id, club_id)
return ok
huiyuan_list = []
for huiyuan in huiyuan_queryset: