feat: 迁移预览展示会员名称与到期日,总会员按子会员加时长
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import hashlib
|
|||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from decimal import Decimal, ROUND_HALF_UP
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
@@ -207,6 +207,12 @@ def preview_assets(plan: ClubMigratePlan, from_uid: str) -> List[dict]:
|
|||||||
continue
|
continue
|
||||||
profile = _profile_of(user, role)
|
profile = _profile_of(user, role)
|
||||||
amount = _read_field(profile, field, kind)
|
amount = _read_field(profile, field, kind)
|
||||||
|
if kind == 'money':
|
||||||
|
display = f'¥{amount}'
|
||||||
|
elif kind == 'int':
|
||||||
|
display = str(int(amount) if amount is not None else 0)
|
||||||
|
else:
|
||||||
|
display = str(amount)
|
||||||
items.append({
|
items.append({
|
||||||
'key': key,
|
'key': key,
|
||||||
'role': role,
|
'role': role,
|
||||||
@@ -214,13 +220,15 @@ def preview_assets(plan: ClubMigratePlan, from_uid: str) -> List[dict]:
|
|||||||
'label': meta['label'],
|
'label': meta['label'],
|
||||||
'kind': kind,
|
'kind': kind,
|
||||||
'amount': str(amount),
|
'amount': str(amount),
|
||||||
|
'display': display,
|
||||||
'has_profile': profile is not None,
|
'has_profile': profile is not None,
|
||||||
})
|
})
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def _preview_huiyuan(uid: str, club_id: str, role: str) -> List[dict]:
|
def _preview_huiyuan(uid: str, club_id: str, role: str) -> List[dict]:
|
||||||
from products.models import Huiyuangoumai
|
from products.models import Huiyuan, Huiyuangoumai
|
||||||
|
|
||||||
now = _now()
|
now = _now()
|
||||||
rows = list(
|
rows = list(
|
||||||
Huiyuangoumai.query.filter(
|
Huiyuangoumai.query.filter(
|
||||||
@@ -230,23 +238,231 @@ def _preview_huiyuan(uid: str, club_id: str, role: str) -> List[dict]:
|
|||||||
daoqi_time__gt=now,
|
daoqi_time__gt=now,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
ids = [r.huiyuan_id for r in rows]
|
||||||
|
name_map = {
|
||||||
|
h.huiyuan_id: (h.jieshao or '').strip() or '会员'
|
||||||
|
for h in Huiyuan.query.filter(huiyuan_id__in=ids)
|
||||||
|
}
|
||||||
out = []
|
out = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
|
name = (r.jieshao or '').strip() or name_map.get(r.huiyuan_id) or '会员'
|
||||||
|
exp = r.daoqi_time
|
||||||
|
exp_text = timezone.localtime(exp).strftime('%Y-%m-%d %H:%M') if exp else ''
|
||||||
out.append({
|
out.append({
|
||||||
'key': f'{role}.huiyuan',
|
'key': f'{role}.huiyuan:{r.huiyuan_id}',
|
||||||
'role': role,
|
'role': role,
|
||||||
'field': 'huiyuan',
|
'field': 'huiyuan',
|
||||||
'label': f'会员 {r.huiyuan_id}',
|
'label': name,
|
||||||
'kind': 'huiyuan',
|
'kind': 'huiyuan',
|
||||||
'amount': '1',
|
'amount': exp_text,
|
||||||
|
'display': f'{name} · 到期 {exp_text}' if exp_text else name,
|
||||||
'huiyuan_id': r.huiyuan_id,
|
'huiyuan_id': r.huiyuan_id,
|
||||||
'daoqi_time': r.daoqi_time.isoformat() if r.daoqi_time else '',
|
'daoqi_time': exp.isoformat() if exp else '',
|
||||||
'jieshao': r.jieshao or '',
|
'jieshao': name,
|
||||||
'has_profile': True,
|
'has_profile': True,
|
||||||
})
|
})
|
||||||
return out
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def _bundle_child_ids(club_id: str, bundle_huiyuan_id: str) -> List[str]:
|
||||||
|
from jituan.models import ClubHuiyuanBundleInclude
|
||||||
|
qs = ClubHuiyuanBundleInclude.query.filter(
|
||||||
|
club_id=str(club_id), bundle_huiyuan_id=str(bundle_huiyuan_id),
|
||||||
|
)
|
||||||
|
kids = [str(x.included_huiyuan_id) for x in qs if x.included_huiyuan_id]
|
||||||
|
if kids:
|
||||||
|
return kids
|
||||||
|
# 源店未配包含关系时,回退任意俱乐部同总会员配置
|
||||||
|
qs2 = ClubHuiyuanBundleInclude.query.filter(bundle_huiyuan_id=str(bundle_huiyuan_id))
|
||||||
|
seen = set()
|
||||||
|
out = []
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_migrate_target_huiyuan_ids(
|
||||||
|
*, src_huiyuan_id: str, from_club: str, to_club: str,
|
||||||
|
) -> List[str]:
|
||||||
|
"""
|
||||||
|
目标店开通了同 ID → 迁该 ID;
|
||||||
|
总会员目标店无对应 ID → 按源店子会员映射到目标店已开通的子会员。
|
||||||
|
"""
|
||||||
|
from products.models import Huiyuan
|
||||||
|
|
||||||
|
src_id = str(src_huiyuan_id or '').strip()
|
||||||
|
if not src_id:
|
||||||
|
return []
|
||||||
|
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 []
|
||||||
|
|
||||||
|
children = _bundle_child_ids(from_club, src_id)
|
||||||
|
out = []
|
||||||
|
for cid in children:
|
||||||
|
if _club_huiyuan_enabled(to_club, cid):
|
||||||
|
out.append(cid)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _add_huiyuan_duration(*, to_uid: str, to_club: str, huiyuan_id: str, remain, jieshao: str, is_trial: bool, has_used_trial: bool):
|
||||||
|
"""给目标用户对应会员加时长(已有则累加剩余,否则从现在起算)。"""
|
||||||
|
from products.models import Huiyuangoumai
|
||||||
|
|
||||||
|
now = _now()
|
||||||
|
remain = remain if remain and remain.total_seconds() > 0 else None
|
||||||
|
if remain is None:
|
||||||
|
return None, {}
|
||||||
|
|
||||||
|
dst = Huiyuangoumai.objects.select_for_update().filter(
|
||||||
|
yonghu_id=str(to_uid),
|
||||||
|
huiyuan_id=str(huiyuan_id),
|
||||||
|
).first()
|
||||||
|
before_dst = {}
|
||||||
|
if dst:
|
||||||
|
before_dst = {
|
||||||
|
'daoqi_time': dst.daoqi_time.isoformat() if dst.daoqi_time else '',
|
||||||
|
'zhuangtai': dst.huiyuan_zhuangtai,
|
||||||
|
'club_id': dst.club_id,
|
||||||
|
}
|
||||||
|
dst_exp = _aware(dst.daoqi_time)
|
||||||
|
base = dst_exp if (dst_exp and dst_exp > now and int(dst.huiyuan_zhuangtai or 0) == 1) else now
|
||||||
|
exp = base + remain
|
||||||
|
dst.daoqi_time = exp
|
||||||
|
dst.huiyuan_zhuangtai = 1
|
||||||
|
dst.club_id = to_club
|
||||||
|
if jieshao:
|
||||||
|
dst.jieshao = jieshao
|
||||||
|
dst.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai', 'club_id', 'jieshao', 'UpdateTime'])
|
||||||
|
return exp, before_dst
|
||||||
|
|
||||||
|
exp = now + remain
|
||||||
|
Huiyuangoumai.query.create(
|
||||||
|
huiyuan_id=str(huiyuan_id),
|
||||||
|
yonghu_id=str(to_uid),
|
||||||
|
huiyuan_zhuangtai=1,
|
||||||
|
jieshao=jieshao or '',
|
||||||
|
daoqi_time=exp,
|
||||||
|
club_id=to_club,
|
||||||
|
is_trial=bool(is_trial),
|
||||||
|
has_used_trial=bool(has_used_trial),
|
||||||
|
)
|
||||||
|
return exp, before_dst
|
||||||
|
|
||||||
|
|
||||||
|
def _transfer_huiyuan(*, from_uid, to_uid, from_club, to_club, role) -> List[dict]:
|
||||||
|
from products.models import Huiyuan, Huiyuangoumai
|
||||||
|
|
||||||
|
now = _now()
|
||||||
|
rows = list(
|
||||||
|
Huiyuangoumai.objects.select_for_update().filter(
|
||||||
|
yonghu_id=str(from_uid),
|
||||||
|
club_id=from_club,
|
||||||
|
huiyuan_zhuangtai=1,
|
||||||
|
daoqi_time__gt=now,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
out = []
|
||||||
|
for r in rows:
|
||||||
|
src_exp = _aware(r.daoqi_time)
|
||||||
|
name = (r.jieshao or '').strip()
|
||||||
|
if not name:
|
||||||
|
hy = Huiyuan.query.filter(huiyuan_id=r.huiyuan_id).first()
|
||||||
|
name = (hy.jieshao if hy else '') or '会员'
|
||||||
|
before = {
|
||||||
|
'huiyuan_id': r.huiyuan_id,
|
||||||
|
'daoqi_time': src_exp.isoformat() if src_exp else '',
|
||||||
|
'zhuangtai': r.huiyuan_zhuangtai,
|
||||||
|
'is_trial': bool(r.is_trial),
|
||||||
|
'jieshao': name,
|
||||||
|
}
|
||||||
|
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:
|
||||||
|
out.append({
|
||||||
|
'item_type': ClubMigrateLedgerItem.ITEM_HUIYUAN,
|
||||||
|
'role': role,
|
||||||
|
'field': 'huiyuan',
|
||||||
|
'amount': Decimal('0'),
|
||||||
|
'before_src': before.get('daoqi_time', ''),
|
||||||
|
'after_src': 'expired',
|
||||||
|
'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',
|
||||||
|
'name': name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
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
|
||||||
|
exp, before_dst = _add_huiyuan_duration(
|
||||||
|
to_uid=to_uid,
|
||||||
|
to_club=to_club,
|
||||||
|
huiyuan_id=tid,
|
||||||
|
remain=remain,
|
||||||
|
jieshao=child_name,
|
||||||
|
is_trial=bool(r.is_trial),
|
||||||
|
has_used_trial=bool(r.has_used_trial),
|
||||||
|
)
|
||||||
|
out.append({
|
||||||
|
'item_type': ClubMigrateLedgerItem.ITEM_HUIYUAN,
|
||||||
|
'role': role,
|
||||||
|
'field': 'huiyuan',
|
||||||
|
'amount': Decimal('1'),
|
||||||
|
'before_src': before.get('daoqi_time', ''),
|
||||||
|
'after_src': 'expired',
|
||||||
|
'before_dst': before_dst.get('daoqi_time', ''),
|
||||||
|
'after_dst': exp.isoformat() if exp else '',
|
||||||
|
'huiyuan_id': tid,
|
||||||
|
'meta_json': {
|
||||||
|
'before_src': before,
|
||||||
|
'before_dst': before_dst,
|
||||||
|
'src_huiyuan_id': r.huiyuan_id,
|
||||||
|
'name': child_name,
|
||||||
|
'mapped_from_bundle': tid != r.huiyuan_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _aware(dt):
|
||||||
|
if dt is None:
|
||||||
|
return None
|
||||||
|
if timezone.is_naive(dt):
|
||||||
|
return timezone.make_aware(dt, timezone.get_current_timezone())
|
||||||
|
return dt
|
||||||
|
|
||||||
|
|
||||||
def check_conditions(plan: ClubMigratePlan, from_uid: str) -> Tuple[bool, str]:
|
def check_conditions(plan: ClubMigratePlan, from_uid: str) -> Tuple[bool, str]:
|
||||||
"""返回 (ok, msg)。"""
|
"""返回 (ok, msg)。"""
|
||||||
cond = plan.condition_json or {}
|
cond = plan.condition_json or {}
|
||||||
@@ -561,85 +777,17 @@ def _transfer_balance_field(*, from_user, to_user, role, field, kind, key, label
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _aware(dt):
|
def _fmt_ledger_time(v: str) -> str:
|
||||||
if dt is None:
|
s = str(v or '').strip()
|
||||||
return None
|
if not s or s in ('skipped', 'expired') or 'T' not in s:
|
||||||
|
return s
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(s.replace('Z', '+00:00'))
|
||||||
if timezone.is_naive(dt):
|
if timezone.is_naive(dt):
|
||||||
return timezone.make_aware(dt, timezone.get_current_timezone())
|
dt = timezone.make_aware(dt, timezone.get_current_timezone())
|
||||||
return dt
|
return timezone.localtime(dt).strftime('%Y-%m-%d %H:%M')
|
||||||
|
except Exception:
|
||||||
|
return s
|
||||||
def _transfer_huiyuan(*, from_uid, to_uid, from_club, to_club, role) -> List[dict]:
|
|
||||||
from products.models import Huiyuangoumai
|
|
||||||
|
|
||||||
now = _now()
|
|
||||||
rows = list(
|
|
||||||
Huiyuangoumai.objects.select_for_update().filter(
|
|
||||||
yonghu_id=str(from_uid),
|
|
||||||
club_id=from_club,
|
|
||||||
huiyuan_zhuangtai=1,
|
|
||||||
daoqi_time__gt=now,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
out = []
|
|
||||||
for r in rows:
|
|
||||||
src_exp = r.daoqi_time
|
|
||||||
before = {
|
|
||||||
'huiyuan_id': r.huiyuan_id,
|
|
||||||
'daoqi_time': src_exp.isoformat() if src_exp else '',
|
|
||||||
'zhuangtai': r.huiyuan_zhuangtai,
|
|
||||||
'is_trial': bool(r.is_trial),
|
|
||||||
'jieshao': r.jieshao or '',
|
|
||||||
}
|
|
||||||
# 源失效(保留原到期进流水 before)
|
|
||||||
r.huiyuan_zhuangtai = 0
|
|
||||||
r.daoqi_time = now
|
|
||||||
r.save(update_fields=['huiyuan_zhuangtai', 'daoqi_time', 'UpdateTime'])
|
|
||||||
|
|
||||||
dst = Huiyuangoumai.objects.select_for_update().filter(
|
|
||||||
yonghu_id=str(to_uid),
|
|
||||||
huiyuan_id=r.huiyuan_id,
|
|
||||||
).first()
|
|
||||||
before_dst = {}
|
|
||||||
exp = _aware(src_exp) or now
|
|
||||||
if dst:
|
|
||||||
before_dst = {
|
|
||||||
'daoqi_time': dst.daoqi_time.isoformat() if dst.daoqi_time else '',
|
|
||||||
'zhuangtai': dst.huiyuan_zhuangtai,
|
|
||||||
'club_id': dst.club_id,
|
|
||||||
}
|
|
||||||
dst_exp = _aware(dst.daoqi_time)
|
|
||||||
if dst_exp and dst_exp > exp:
|
|
||||||
exp = dst_exp
|
|
||||||
dst.daoqi_time = exp
|
|
||||||
dst.huiyuan_zhuangtai = 1
|
|
||||||
dst.club_id = to_club
|
|
||||||
dst.jieshao = r.jieshao or dst.jieshao
|
|
||||||
dst.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai', 'club_id', 'jieshao', 'UpdateTime'])
|
|
||||||
else:
|
|
||||||
Huiyuangoumai.query.create(
|
|
||||||
huiyuan_id=r.huiyuan_id,
|
|
||||||
yonghu_id=str(to_uid),
|
|
||||||
huiyuan_zhuangtai=1,
|
|
||||||
jieshao=before.get('jieshao') or '',
|
|
||||||
daoqi_time=exp,
|
|
||||||
club_id=to_club,
|
|
||||||
is_trial=bool(r.is_trial),
|
|
||||||
has_used_trial=bool(r.has_used_trial),
|
|
||||||
)
|
|
||||||
out.append({
|
|
||||||
'item_type': ClubMigrateLedgerItem.ITEM_HUIYUAN,
|
|
||||||
'role': role,
|
|
||||||
'field': 'huiyuan',
|
|
||||||
'amount': Decimal('1'),
|
|
||||||
'before_src': before.get('daoqi_time', ''),
|
|
||||||
'after_src': 'expired',
|
|
||||||
'before_dst': before_dst.get('daoqi_time', ''),
|
|
||||||
'after_dst': exp.isoformat() if exp else '',
|
|
||||||
'huiyuan_id': r.huiyuan_id,
|
|
||||||
'meta_json': {'before_src': before, 'before_dst': before_dst},
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_ledger(ledger: ClubMigrateLedger) -> dict:
|
def serialize_ledger(ledger: ClubMigrateLedger) -> dict:
|
||||||
@@ -663,9 +811,12 @@ def serialize_ledger(ledger: ClubMigrateLedger) -> dict:
|
|||||||
'before_src': it.before_src,
|
'before_src': it.before_src,
|
||||||
'after_src': it.after_src,
|
'after_src': it.after_src,
|
||||||
'before_dst': it.before_dst,
|
'before_dst': it.before_dst,
|
||||||
'after_dst': it.after_dst,
|
'after_dst': _fmt_ledger_time(it.after_dst) if it.field == 'huiyuan' else it.after_dst,
|
||||||
'huiyuan_id': it.huiyuan_id,
|
'huiyuan_id': it.huiyuan_id,
|
||||||
'meta': it.meta_json or {},
|
'meta': it.meta_json or {},
|
||||||
|
'label': (it.meta_json or {}).get('name')
|
||||||
|
or (it.meta_json or {}).get('label')
|
||||||
|
or f'{it.role}.{it.field}',
|
||||||
}
|
}
|
||||||
for it in items
|
for it in items
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user