fix: 会员迁移不盲信club_id,目标必落库并撤销源Czjilu权益
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -284,18 +284,41 @@ def user_has_migratable_assets(plan: ClubMigratePlan, from_uid: str) -> bool:
|
||||
return any(asset_item_has_value(it) for it in items)
|
||||
|
||||
|
||||
def _preview_huiyuan(uid: str, club_id: str, role: str) -> List[dict]:
|
||||
from products.models import Huiyuan, Huiyuangoumai
|
||||
def _list_active_huiyuan_rows(uid: str, from_club: str, *, for_update: bool = False):
|
||||
"""
|
||||
源店可迁会员:按用户取有效会员,不盲信 Huiyuangoumai.club_id。
|
||||
历史数据常把 club_id 写成默认 xq/空,严格按 from_club 过滤会漏迁。
|
||||
仅排除「明确属于其他店」的记录。
|
||||
"""
|
||||
from jituan.constants import CLUB_ID_DEFAULT
|
||||
from products.models import Huiyuangoumai
|
||||
|
||||
now = _now()
|
||||
rows = list(
|
||||
Huiyuangoumai.query.filter(
|
||||
now = _now_aware()
|
||||
from_club = (from_club or '').strip()
|
||||
qs = Huiyuangoumai.objects.filter(
|
||||
yonghu_id=str(uid),
|
||||
club_id=club_id,
|
||||
huiyuan_zhuangtai=1,
|
||||
daoqi_time__gt=now,
|
||||
)
|
||||
)
|
||||
if for_update:
|
||||
qs = qs.select_for_update()
|
||||
rows = list(qs)
|
||||
out = []
|
||||
for r in rows:
|
||||
exp = _aware(r.daoqi_time)
|
||||
if not exp or exp <= now:
|
||||
continue
|
||||
rec_cid = (getattr(r, 'club_id', None) or '').strip()
|
||||
if rec_cid and rec_cid not in (from_club, CLUB_ID_DEFAULT, ''):
|
||||
if from_club and rec_cid != from_club:
|
||||
continue
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
def _preview_huiyuan(uid: str, club_id: str, role: str) -> List[dict]:
|
||||
from products.models import Huiyuan
|
||||
|
||||
rows = _list_active_huiyuan_rows(uid, club_id)
|
||||
if not rows:
|
||||
return []
|
||||
ids = [r.huiyuan_id for r in rows]
|
||||
@@ -325,36 +348,49 @@ def _preview_huiyuan(uid: str, club_id: str, role: str) -> List[dict]:
|
||||
'daoqi_time': exp.isoformat() if exp else '',
|
||||
'jieshao': name,
|
||||
'has_profile': True,
|
||||
'record_club_id': (r.club_id or ''),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def _club_huiyuan_enabled(club_id: str, huiyuan_id: str) -> bool:
|
||||
from jituan.models import ClubHuiyuanPrice
|
||||
row = ClubHuiyuanPrice.query.filter(
|
||||
club_id=str(club_id), huiyuan_id=str(huiyuan_id), is_enabled=True,
|
||||
).first()
|
||||
return bool(row)
|
||||
from jituan.services.huiyuan_bundle import expand_huiyuan_id_variants
|
||||
|
||||
for vid in expand_huiyuan_id_variants(huiyuan_id):
|
||||
if ClubHuiyuanPrice.query.filter(
|
||||
club_id=str(club_id), huiyuan_id=str(vid), is_enabled=True,
|
||||
).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _bundle_child_ids(club_id: str, bundle_huiyuan_id: str) -> List[str]:
|
||||
from jituan.models import ClubHuiyuanBundleInclude
|
||||
from jituan.services.huiyuan_bundle import expand_huiyuan_id_variants
|
||||
|
||||
kids = []
|
||||
seen = set()
|
||||
for bid in expand_huiyuan_id_variants(bundle_huiyuan_id):
|
||||
qs = ClubHuiyuanBundleInclude.query.filter(
|
||||
club_id=str(club_id), bundle_huiyuan_id=str(bundle_huiyuan_id),
|
||||
club_id=str(club_id), bundle_huiyuan_id=str(bid),
|
||||
)
|
||||
kids = [str(x.included_huiyuan_id) for x in qs if x.included_huiyuan_id]
|
||||
for x in qs:
|
||||
cid = str(x.included_huiyuan_id or '')
|
||||
if cid and cid not in seen:
|
||||
seen.add(cid)
|
||||
kids.append(cid)
|
||||
if kids:
|
||||
return kids
|
||||
# 源店未配包含关系时,回退任意俱乐部同总会员配置
|
||||
qs2 = ClubHuiyuanBundleInclude.query.filter(bundle_huiyuan_id=str(bundle_huiyuan_id))
|
||||
seen = set()
|
||||
out = []
|
||||
for bid in expand_huiyuan_id_variants(bundle_huiyuan_id):
|
||||
qs2 = ClubHuiyuanBundleInclude.query.filter(bundle_huiyuan_id=str(bid))
|
||||
for x in qs2:
|
||||
cid = str(x.included_huiyuan_id or '')
|
||||
if cid and cid not in seen:
|
||||
seen.add(cid)
|
||||
out.append(cid)
|
||||
return out
|
||||
kids.append(cid)
|
||||
return kids
|
||||
|
||||
|
||||
def _resolve_migrate_target_huiyuan_ids(
|
||||
@@ -362,9 +398,11 @@ def _resolve_migrate_target_huiyuan_ids(
|
||||
) -> List[str]:
|
||||
"""
|
||||
目标店开通了同 ID → 迁该 ID;
|
||||
总会员目标店无对应 ID → 按源店子会员映射到目标店已开通的子会员。
|
||||
总会员目标店无对应 ID → 按源店子会员映射到目标店已开通的子会员;
|
||||
仍无映射 → 仍迁同 ID(打手端 clumber 按开通记录展示,不依赖目标店上架)。
|
||||
"""
|
||||
from products.models import Huiyuan
|
||||
from jituan.services.huiyuan_bundle import is_bundle_huiyuan
|
||||
|
||||
src_id = str(src_huiyuan_id or '').strip()
|
||||
if not src_id:
|
||||
@@ -372,17 +410,62 @@ def _resolve_migrate_target_huiyuan_ids(
|
||||
if _club_huiyuan_enabled(to_club, src_id):
|
||||
return [src_id]
|
||||
|
||||
hy = Huiyuan.query.filter(huiyuan_id=src_id).first()
|
||||
if not hy or not bool(getattr(hy, 'is_bundle', False)):
|
||||
return []
|
||||
|
||||
# 总会员:尽量映射到目标店已开通子会员
|
||||
if is_bundle_huiyuan(src_id):
|
||||
children = _bundle_child_ids(from_club, src_id)
|
||||
out = []
|
||||
for cid in children:
|
||||
if _club_huiyuan_enabled(to_club, cid):
|
||||
out.append(cid)
|
||||
if out:
|
||||
return out
|
||||
|
||||
# 兜底:原样迁入同 ID,保证目标账号有开通记录可展示
|
||||
return [src_id]
|
||||
|
||||
|
||||
def _revoke_source_member_czjilu(from_uid: str, huiyuan_id: str):
|
||||
"""撤销源店充值单权益,避免 Huiyuangoumai 失效后仍被 clumber 从 Czjilu 合成展示。"""
|
||||
from jituan.services.huiyuan_bundle import huiyuan_ids_match
|
||||
from jituan.services.member_recharge import (
|
||||
ENTITLEMENT_REVOKED_MARKER,
|
||||
MEMBER_LEIXING,
|
||||
MEMBER_PAID_STATUS,
|
||||
_czjilu_entitlement_revoked,
|
||||
_truncate_shuoming,
|
||||
)
|
||||
from products.models import Czjilu
|
||||
|
||||
for order in Czjilu.query.filter(
|
||||
yonghuid=str(from_uid),
|
||||
leixing=MEMBER_LEIXING,
|
||||
zhuangtai=MEMBER_PAID_STATUS,
|
||||
):
|
||||
if not huiyuan_ids_match(order.huiyuan_id, huiyuan_id):
|
||||
continue
|
||||
if _czjilu_entitlement_revoked(order):
|
||||
continue
|
||||
note = (order.shuoming or '').strip()
|
||||
order.shuoming = _truncate_shuoming(
|
||||
f'{note} |跨店迁移|{ENTITLEMENT_REVOKED_MARKER}|'
|
||||
if note else f'跨店迁移|{ENTITLEMENT_REVOKED_MARKER}'
|
||||
)
|
||||
order.save(update_fields=['shuoming'])
|
||||
|
||||
|
||||
def _expire_source_huiyuan(row, now):
|
||||
"""源会员置失效。"""
|
||||
row.huiyuan_zhuangtai = 0
|
||||
row.daoqi_time = now
|
||||
row.save(update_fields=['huiyuan_zhuangtai', 'daoqi_time', 'UpdateTime'])
|
||||
try:
|
||||
_revoke_source_member_czjilu(row.yonghu_id, row.huiyuan_id)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
'migrate revoke czjilu fail uid=%s hy=%s: %s',
|
||||
row.yonghu_id, row.huiyuan_id, e,
|
||||
)
|
||||
|
||||
|
||||
def _add_huiyuan_duration(*, to_uid: str, to_club: str, huiyuan_id: str, remain, jieshao: str, is_trial: bool, has_used_trial: bool):
|
||||
"""给目标用户对应会员加时长(已有则累加剩余,否则从现在起算)。"""
|
||||
@@ -430,17 +513,10 @@ def _add_huiyuan_duration(*, to_uid: str, to_club: str, huiyuan_id: str, remain,
|
||||
|
||||
|
||||
def _transfer_huiyuan(*, from_uid, to_uid, from_club, to_club, role) -> List[dict]:
|
||||
from products.models import Huiyuan, Huiyuangoumai
|
||||
from products.models import Huiyuan
|
||||
|
||||
now = _now_aware()
|
||||
rows = list(
|
||||
Huiyuangoumai.objects.select_for_update().filter(
|
||||
yonghu_id=str(from_uid),
|
||||
club_id=from_club,
|
||||
huiyuan_zhuangtai=1,
|
||||
daoqi_time__gt=_now(),
|
||||
)
|
||||
)
|
||||
rows = _list_active_huiyuan_rows(from_uid, from_club, for_update=True)
|
||||
out = []
|
||||
for r in rows:
|
||||
src_exp = _aware(r.daoqi_time)
|
||||
@@ -454,41 +530,38 @@ def _transfer_huiyuan(*, from_uid, to_uid, from_club, to_club, role) -> List[dic
|
||||
'zhuangtai': r.huiyuan_zhuangtai,
|
||||
'is_trial': bool(r.is_trial),
|
||||
'jieshao': name,
|
||||
'record_club_id': (r.club_id or ''),
|
||||
}
|
||||
remain = (src_exp - now) if src_exp and src_exp > now else None
|
||||
# 源失效(保留原到期进流水 before)
|
||||
r.huiyuan_zhuangtai = 0
|
||||
r.daoqi_time = now
|
||||
r.save(update_fields=['huiyuan_zhuangtai', 'daoqi_time', 'UpdateTime'])
|
||||
|
||||
target_ids = _resolve_migrate_target_huiyuan_ids(
|
||||
src_huiyuan_id=r.huiyuan_id, from_club=from_club, to_club=to_club,
|
||||
)
|
||||
if not target_ids or remain is None:
|
||||
if remain is None or not target_ids:
|
||||
out.append({
|
||||
'item_type': ClubMigrateLedgerItem.ITEM_HUIYUAN,
|
||||
'role': role,
|
||||
'field': 'huiyuan',
|
||||
'amount': Decimal('0'),
|
||||
'before_src': before.get('daoqi_time', ''),
|
||||
'after_src': 'expired',
|
||||
'after_src': 'kept',
|
||||
'before_dst': '',
|
||||
'after_dst': 'skipped',
|
||||
'huiyuan_id': r.huiyuan_id,
|
||||
'meta_json': {
|
||||
'before_src': before,
|
||||
'skip_reason': 'target_not_enabled' if not target_ids else 'no_remain',
|
||||
'skip_reason': 'no_remain' if remain is None else 'no_target',
|
||||
'name': name,
|
||||
},
|
||||
})
|
||||
continue
|
||||
|
||||
# 先写入目标,再失效源 + 撤销 Czjilu(避免目标写失败却已抹源)
|
||||
transferred = False
|
||||
for tid in target_ids:
|
||||
child_name = name
|
||||
if tid != r.huiyuan_id:
|
||||
chy = Huiyuan.query.filter(huiyuan_id=tid).first()
|
||||
child_name = (chy.jieshao if chy else '') or tid
|
||||
# 正式会员迁入:继承源店正式购买次数,且至少 1(再充不可能算首次分红)
|
||||
formal_floor = 0
|
||||
if not bool(r.is_trial):
|
||||
from jituan.services.member_recharge import count_formal_paid_member_orders
|
||||
@@ -506,6 +579,9 @@ def _transfer_huiyuan(*, from_uid, to_uid, from_club, to_club, role) -> List[dic
|
||||
is_trial=bool(r.is_trial),
|
||||
has_used_trial=bool(r.has_used_trial),
|
||||
)
|
||||
if exp is None:
|
||||
continue
|
||||
transferred = True
|
||||
out.append({
|
||||
'item_type': ClubMigrateLedgerItem.ITEM_HUIYUAN,
|
||||
'role': role,
|
||||
@@ -526,6 +602,26 @@ def _transfer_huiyuan(*, from_uid, to_uid, from_club, to_club, role) -> List[dic
|
||||
'is_trial': bool(r.is_trial),
|
||||
},
|
||||
})
|
||||
|
||||
if transferred:
|
||||
_expire_source_huiyuan(r, now)
|
||||
else:
|
||||
out.append({
|
||||
'item_type': ClubMigrateLedgerItem.ITEM_HUIYUAN,
|
||||
'role': role,
|
||||
'field': 'huiyuan',
|
||||
'amount': Decimal('0'),
|
||||
'before_src': before.get('daoqi_time', ''),
|
||||
'after_src': 'kept',
|
||||
'before_dst': '',
|
||||
'after_dst': 'skipped',
|
||||
'huiyuan_id': r.huiyuan_id,
|
||||
'meta_json': {
|
||||
'before_src': before,
|
||||
'skip_reason': 'add_duration_failed',
|
||||
'name': name,
|
||||
},
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user