fix: 迁移积分/分红次数/冻结入账,并修正无计划误导文案

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-29 03:44:53 +08:00
parent 2b6d91fc4a
commit 7dc1dd6f04
5 changed files with 208 additions and 38 deletions

View File

@@ -41,6 +41,21 @@ OPEN_ORDER_STATUSES = (1, 2, 4, 7, 8, 9)
# 未缴清罚单:待缴纳/申诉中/平台审核中
UNPAID_PENALTY_STATUSES = (1, 3, 5)
# 目标店新建打手时绑定的默认管事邀请码(与小程序 PLATFORM 默认码一致)
CLUB_MIGRATE_DEFAULT_GUANSHI_INVITE = {
'lxs': '0000008VfJKPu4Spmcjt',
'xq': '0000008RffVgKHMj7kQC',
}
# 可提现类字段:迁入走目标店冻结网关(押金/积分/已冻结池除外)
_FREEZE_CREDIT_FIELDS = frozenset({
('dashou', 'yue'),
('guanshi', 'yue'),
('zuzhang', 'ketixian_jine'),
('shenheguan', 'yue'),
})
MEMBER_JIFEN_CAP = 10
def _money(v) -> Decimal:
return Decimal(str(v or 0)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
@@ -129,8 +144,25 @@ def _load_user(uid: str):
return User.query.filter(UserUID=str(uid)).first()
def _get_or_create_profile(user, role: str):
"""目标店无角色时创建空扩展行(仅迁移模块调用)"""
def _resolve_default_guanshi_uid(club_id: str) -> Optional[str]:
"""按俱乐部硬编码邀请码解析默认管事 UID解析失败返回 None"""
code = CLUB_MIGRATE_DEFAULT_GUANSHI_INVITE.get((club_id or '').strip())
if not code:
return None
from users.models import UserGuanshi
gs = UserGuanshi.query.filter(yaoqingma=code).select_related('user').first()
if not gs or not getattr(gs, 'user', None):
logger.warning('migrate default guanshi invite not found club=%s code=%s', club_id, code)
return None
return str(gs.user.UserUID)
def _get_or_create_profile(user, role: str, *, to_club_id: str = ''):
"""目标店无角色时创建空扩展行(仅迁移模块调用)。
新建打手:绑定目标店默认管事(源店邀请人在新店无效)。
已有打手资料:不改 yaoqingren曾在新店注册过的情况保留原绑定
"""
from users.models import UserDashou, UserShangjia, UserGuanshi, UserZuzhang, UserShenheguan
mapping = {
@@ -150,6 +182,9 @@ def _get_or_create_profile(user, role: str):
defaults['nicheng'] = f'SJ{user.UserUID}'
elif role == 'dashou':
defaults['nicheng'] = f'DS{user.UserUID}'
gs_uid = _resolve_default_guanshi_uid(to_club_id)
if gs_uid:
defaults['yaoqingren'] = gs_uid
obj = model.query.create(**defaults)
return obj
@@ -425,6 +460,15 @@ def _transfer_huiyuan(*, from_uid, to_uid, from_club, to_club, role) -> List[dic
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
src_paid = count_formal_paid_member_orders(
from_uid, r.huiyuan_id, from_club,
)
formal_floor = max(int(src_paid or 0), 1)
exp, before_dst = _add_huiyuan_duration(
to_uid=to_uid,
to_club=to_club,
@@ -450,6 +494,8 @@ def _transfer_huiyuan(*, from_uid, to_uid, from_club, to_club, role) -> List[dic
'src_huiyuan_id': r.huiyuan_id,
'name': child_name,
'mapped_from_bundle': tid != r.huiyuan_id,
'formal_purchase_floor': formal_floor,
'is_trial': bool(r.is_trial),
},
})
return out
@@ -510,6 +556,7 @@ def gate_for_user(*, club_id: str, user_uid: str, roles: Optional[List[str]] = N
"""小程序启动/进打手端调用。无计划 → 全 false现网无感。"""
plan = find_active_plan_for_source(club_id)
if not plan:
target = find_active_plan_for_target(club_id)
return {
'active': False,
'force': False,
@@ -517,6 +564,8 @@ def gate_for_user(*, club_id: str, user_uid: str, roles: Optional[List[str]] = N
'completed': False,
'plan_id': '',
'to_club_id': '',
'is_target': bool(target),
'target_from_club_id': (target.from_club_id if target else ''),
}
done = not can_migrate_again(plan, user_uid)
force_roles = plan.force_roles or ['dashou']
@@ -536,6 +585,7 @@ def gate_for_user(*, club_id: str, user_uid: str, roles: Optional[List[str]] = N
'force_roles': force_roles,
'success_count': success_count(plan.plan_id, user_uid),
'max_allowed': max_allowed_success(plan, user_uid),
'is_target': False,
}
@@ -662,10 +712,11 @@ def confirm_migrate(
# 目标店:白名单涉及角色先建空扩展行
for role in roles_from_whitelist(keys):
if _profile_of(to_user, role) is None:
_get_or_create_profile(to_user, role)
_get_or_create_profile(to_user, role, to_club_id=plan.to_club_id)
items_payload = []
migrate_seq = int(cons.success_count or 0) + 1
freeze_biz_prefix = f'cm:{token_row.token}:{migrate_seq}'
for key in keys:
meta = ASSET_CATALOG[key]
@@ -689,6 +740,8 @@ def confirm_migrate(
kind=meta['kind'],
key=key,
label=meta['label'],
to_club_id=plan.to_club_id,
freeze_biz_id=f'{freeze_biz_prefix}:{key}'[:64],
)
)
@@ -735,7 +788,18 @@ def confirm_migrate(
return serialize_ledger(ledger), ''
def _transfer_balance_field(*, from_user, to_user, role, field, kind, key, label) -> dict:
def _transfer_balance_field(
*,
from_user,
to_user,
role,
field,
kind,
key,
label,
to_club_id: str = '',
freeze_biz_id: str = '',
) -> dict:
src_prof = _lock_profile(from_user, role)
amount = _read_field(src_prof, field, kind)
before_src = amount
@@ -750,17 +814,53 @@ def _transfer_balance_field(*, from_user, to_user, role, field, kind, key, label
dst_prof = _lock_profile(to_user, role)
created = False
if dst_prof is None:
dst_prof = _get_or_create_profile(to_user, role)
dst_prof = _get_or_create_profile(to_user, role, to_club_id=to_club_id)
created = True
dst_prof = _lock_profile(to_user, role)
before_dst = _read_field(dst_prof, field, kind) if dst_prof else (0 if kind == 'int' else Decimal('0.00'))
after_dst = before_dst
meta_extra = {}
if dst_prof is not None and amount:
if kind == 'int':
# 积分:目标已满 10 保持 10相加后也不得超过 10
if field == 'jifen' and kind == 'int':
if int(before_dst) >= MEMBER_JIFEN_CAP:
after_dst = MEMBER_JIFEN_CAP
else:
after_dst = min(MEMBER_JIFEN_CAP, int(before_dst) + int(amount))
type(dst_prof).objects.filter(pk=dst_prof.pk).update(jifen=int(after_dst))
meta_extra['jifen_capped'] = True
meta_extra['jifen_cap'] = MEMBER_JIFEN_CAP
elif (role, field) in _FREEZE_CREDIT_FIELDS:
# 可提现入账:走目标店冻结网关(开了冻结则按配置拆分)
from jituan.services.fund_freeze import credit_role_balance, SOURCE_CLUB_MIGRATE
to_uid = str(getattr(to_user, 'UserUID', '') or '')
credit_res = credit_role_balance(
club_id=to_club_id,
user_id=to_uid,
role=role,
amount=_money(amount),
source_type=SOURCE_CLUB_MIGRATE,
biz_id=(freeze_biz_id or f'cm:{to_uid}:{key}')[:64],
profile=dst_prof,
freeze_reason='跨店资产迁移入账',
ignore_source_scope=True,
)
dst_prof = _lock_profile(to_user, role)
after_dst = _read_field(dst_prof, field, kind) if dst_prof else before_dst
meta_extra.update({
'freeze_available': str(credit_res.get('available') or 0),
'freeze_frozen': str(credit_res.get('frozen') or 0),
'freeze_ledger_id': credit_res.get('ledger_id'),
'freeze_skipped': bool(credit_res.get('skipped_freeze')),
'freeze_skip_reason': credit_res.get('skip_reason'),
})
elif kind == 'int':
type(dst_prof).objects.filter(pk=dst_prof.pk).update(**{field: F(field) + int(amount)})
after_dst = int(before_dst) + int(amount)
else:
# 押金 / 已冻结池等:原样累加,不再二次冻结
type(dst_prof).objects.filter(pk=dst_prof.pk).update(**{field: F(field) + _money(amount)})
after_dst = _money(before_dst) + _money(amount)
@@ -773,7 +873,7 @@ def _transfer_balance_field(*, from_user, to_user, role, field, kind, key, label
'after_src': after_src if amount else before_src,
'before_dst': before_dst,
'after_dst': after_dst,
'meta_json': {'key': key, 'label': label, 'dst_created': created},
'meta_json': {'key': key, 'label': label, 'dst_created': created, **meta_extra},
}