Files
Django/jituan/services/huiyuan_bundle.py
XingQue cb89720260 feat: 打手赠送会员(免费接单)独立表与抢单资格
新增 dashou_zengsong_huiyuan,与购买会员分离;抢单/抢单池认赠送有效会员;后台送/改期/移除接口,送时积分非10则改为10并写操作日志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 02:14:07 +08:00

599 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""总会员(包含式会员):购买一个会员,可接多个子会员类型订单。"""
from datetime import timedelta
from django.utils import timezone
from products.models import Huiyuan, Huiyuangoumai
def normalize_huiyuan_id(huiyuan_id):
if huiyuan_id is None:
return ''
return str(huiyuan_id).strip()
def huiyuan_ids_match(a, b):
"""会员 ID 等价比较忽略首尾空格001 与 1 视为相同)。"""
left = normalize_huiyuan_id(a)
right = normalize_huiyuan_id(b)
if not left or not right:
return False
if left == right:
return True
left_core = left.lstrip('0') or '0'
right_core = right.lstrip('0') or '0'
return left_core == right_core
def expand_huiyuan_id_variants(huiyuan_id):
"""老小程序用 == 比较会员 ID补齐 1 / 01 / 001 等写法。"""
hid = normalize_huiyuan_id(huiyuan_id)
if not hid:
return []
variants = []
for candidate in (hid, hid.lstrip('0') or '0'):
if candidate and candidate not in variants:
variants.append(candidate)
core = hid.lstrip('0') or '0'
if core.isdigit():
for width in (3, 4, 6):
padded = core.zfill(width)
if padded not in variants:
variants.append(padded)
return variants
def format_legacy_daoqi(dt):
if not dt:
return ''
if timezone.is_aware(dt):
dt = timezone.localtime(dt)
return dt.strftime('%Y-%m-%d %H:%M:%S')
def _normalize_datetime_for_compare(dt):
"""统一 naive/aware避免 daoqi 与 now 比较时报 TypeError。"""
from django.conf import settings
if dt is None:
return timezone.now()
if settings.USE_TZ:
if timezone.is_naive(dt):
return timezone.make_aware(dt, timezone.get_current_timezone())
return dt
if timezone.is_aware(dt):
return timezone.make_naive(dt, timezone.get_current_timezone())
return dt
def get_bundle_included_ids(club_id, bundle_huiyuan_id):
from jituan.models import ClubHuiyuanBundleInclude
if not club_id or not bundle_huiyuan_id:
return []
for bid in expand_huiyuan_id_variants(bundle_huiyuan_id):
ids = list(
ClubHuiyuanBundleInclude.query.filter(
club_id=club_id,
bundle_huiyuan_id=bid,
).values_list('included_huiyuan_id', flat=True)
)
if ids:
return ids
stored_bids = ClubHuiyuanBundleInclude.objects.filter(
club_id=club_id,
).values_list('bundle_huiyuan_id', flat=True).distinct()
for stored_bid in stored_bids:
if huiyuan_ids_match(stored_bid, bundle_huiyuan_id):
return list(
ClubHuiyuanBundleInclude.query.filter(
club_id=club_id,
bundle_huiyuan_id=stored_bid,
).values_list('included_huiyuan_id', flat=True)
)
return []
def _membership_club_candidates(yonghu_id, club_id=None, record_club_id=None):
"""
会员/总会员子类型解析用的俱乐部候选(有序)。
Huiyuangoumai/Czjilu.club_id 历史上默认 xq星之界用户落库为 xq 时不能盲信。
"""
from jituan.constants import CLUB_ID_DEFAULT
record_cid = (record_club_id or '').strip()
request_cid = (club_id or '').strip()
user_cid = _resolve_user_club_id(yonghu_id, club_id=club_id)
candidates = []
def add(cid):
c = (cid or '').strip()
if c and c not in candidates:
candidates.append(c)
if record_cid and record_cid != CLUB_ID_DEFAULT:
add(record_cid)
add(request_cid)
add(user_cid)
if record_cid:
add(record_cid)
add(CLUB_ID_DEFAULT)
return candidates
def resolve_bundle_included_ids(bundle_huiyuan_id, club_id=None, yonghu_id=None, record_club_id=None):
"""按候选俱乐部合并总会员包含的子会员(去重,只读)。"""
if not bundle_huiyuan_id:
return []
merged = []
seen = set()
for cid in _membership_club_candidates(
yonghu_id, club_id=club_id, record_club_id=record_club_id,
):
for hid in get_bundle_included_ids(cid, bundle_huiyuan_id):
norm = normalize_huiyuan_id(hid)
core = norm.lstrip('0') or '0' if norm else ''
dedupe_key = core or norm
if dedupe_key and dedupe_key not in seen:
seen.add(dedupe_key)
merged.append(hid)
return merged
def _included_huiyuan_id_set(included_ids):
included_set = set()
for x in included_ids or []:
for variant in expand_huiyuan_id_variants(x):
sx = normalize_huiyuan_id(variant)
if sx:
included_set.add(sx)
included_set.add(sx.lstrip('0') or '0')
return included_set
def _resolve_user_club_id(yonghu_id, club_id=None):
"""解析用户所属俱乐部(体验/正式同一套;总会员子类型依赖此值)。"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_user import get_user_club_id
cid = (club_id or '').strip()
if cid:
return cid
try:
from users.business_models import User
user = User.query.filter(UserUID=yonghu_id).first()
return get_user_club_id(user) or CLUB_ID_DEFAULT
except Exception:
return CLUB_ID_DEFAULT
def _record_club_id(record, club_id=None):
candidates = _membership_club_candidates(
getattr(record, 'yonghu_id', ''),
club_id=club_id,
record_club_id=getattr(record, 'club_id', None),
)
if candidates:
return candidates[0]
from jituan.constants import CLUB_ID_DEFAULT
return CLUB_ID_DEFAULT
def is_bundle_huiyuan(huiyuan_id):
hid = normalize_huiyuan_id(huiyuan_id)
if not hid:
return False
for bid in Huiyuan.query.filter(is_bundle=True).values_list('huiyuan_id', flat=True):
if huiyuan_ids_match(bid, hid):
return True
return False
def _membership_record_active(record):
"""会员记录是否未过期(体验=正式;时区安全,不依赖 jiance 副作用)。"""
if not record or int(record.huiyuan_zhuangtai or 0) != 1:
return False
if not record.daoqi_time:
return False
now = _normalize_datetime_for_compare(timezone.now())
daoqi = _normalize_datetime_for_compare(record.daoqi_time)
return daoqi > now
def user_has_huiyuan_access(yonghu_id, required_huiyuan_id, club_id=None):
"""
抢单 /qiangdan 校验:有无对应会员且未过期(体验会员可抢对应类型单)。
总会员按包含子会员判断。提现不走此函数。
"""
req = normalize_huiyuan_id(required_huiyuan_id)
if not req:
return True
def _record_valid(rec):
return _membership_record_active(rec)
req_set = {req, req.lstrip('0') or '0'}
for rec in Huiyuangoumai.query.filter(yonghu_id=yonghu_id):
if not _record_valid(rec):
continue
if huiyuan_ids_match(rec.huiyuan_id, req):
return True
if is_bundle_huiyuan(rec.huiyuan_id):
included = resolve_bundle_included_ids(
rec.huiyuan_id,
club_id=club_id,
yonghu_id=yonghu_id,
record_club_id=rec.club_id,
)
if req_set & _included_huiyuan_id_set(included):
return True
if _czjilu_grants_huiyuan_access(yonghu_id, req, club_id):
return True
from jituan.services.gift_huiyuan import user_has_gift_huiyuan_access
if user_has_gift_huiyuan_access(yonghu_id, req, club_id=club_id):
return True
return False
def _czjilu_grants_huiyuan_access(yonghu_id, required_huiyuan_id, club_id=None):
"""已支付会员充值单(含体验)在有效期内,视同已开通;总会员含子类型。"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.member_recharge import (
MEMBER_LEIXING,
MEMBER_PAID_STATUS,
_czjilu_entitlement_revoked,
czjilu_entitlement_active,
)
from products.models import Czjilu
req = normalize_huiyuan_id(required_huiyuan_id)
if not req:
return False
req_set = {req, req.lstrip('0') or '0'}
effective_club = _resolve_user_club_id(yonghu_id, club_id)
for order in Czjilu.query.filter(
yonghuid=yonghu_id,
leixing=MEMBER_LEIXING,
zhuangtai=MEMBER_PAID_STATUS,
).order_by('-CreateTime')[:30]:
if _czjilu_entitlement_revoked(order):
continue
cid = getattr(order, 'club_id', None) or club_id or effective_club or CLUB_ID_DEFAULT
if not czjilu_entitlement_active(order, cid):
continue
hid = normalize_huiyuan_id(order.huiyuan_id)
if not hid:
continue
if huiyuan_ids_match(hid, req):
return True
if is_bundle_huiyuan(hid):
included = resolve_bundle_included_ids(
hid,
club_id=club_id,
yonghu_id=yonghu_id,
record_club_id=getattr(order, 'club_id', None),
)
if req_set & _included_huiyuan_id_set(included):
return True
return False
def user_has_huiyuan_access_any(yonghu_id, required_ids, club_id=None):
for rid in required_ids or []:
if user_has_huiyuan_access(yonghu_id, rid, club_id=club_id):
return True
return False
def resolve_order_membership_ids(order_dict):
"""订单可能关联的会员 ID订单字段 + 商品类型,兼容老数据)。"""
ids = []
mid = normalize_huiyuan_id(
order_dict.get('MembershipID') or order_dict.get('huiyuan_id')
)
if mid:
ids.append(mid)
grab_req = int(order_dict.get('GrabRequirement') or order_dict.get('yaoqiuleixing') or 0)
if grab_req != 1:
return ids
leixing_id = order_dict.get('ProductTypeID') or order_dict.get('leixing_id')
if leixing_id:
try:
from products.models import ShangpinLeixing
row = ShangpinLeixing.query.filter(id=leixing_id).values('huiyuan_id').first()
if row:
lx_hid = normalize_huiyuan_id(row.get('huiyuan_id'))
if lx_hid and not any(huiyuan_ids_match(lx_hid, x) for x in ids):
ids.append(lx_hid)
except Exception:
pass
return ids
def resolve_order_membership_id(order_dict):
ids = resolve_order_membership_ids(order_dict)
return ids[0] if ids else ''
def pool_display_has_valid_member(yonghu_id, required_huiyuan_id, club_id=None):
"""
【仅 dddhq 抢单池展示】有无对应会员且未过期。
体验会员与正式会员完全同等;不读 is_trial不拦截体验。
总会员按包含子会员判断。
"""
return user_has_huiyuan_access(yonghu_id, required_huiyuan_id, club_id=club_id)
def pool_display_has_valid_member_for_order(order_dict, yonghu_id, club_id=None):
"""抢单池:本单所需会员类型是否有效(只看类型+过期,体验=正式)。"""
grab_req = int(order_dict.get('GrabRequirement') or order_dict.get('yaoqiuleixing') or 0)
if grab_req != 1:
return True
required_ids = resolve_order_membership_ids(order_dict)
if not required_ids:
return False
for rid in required_ids:
if pool_display_has_valid_member(yonghu_id, rid, club_id=club_id):
return True
return False
def resolve_grab_pool_member_display(order_dict, yonghu_id, club_id=None, user_clumber=None):
"""
dddhq原样返回抢单要求类型 1/2 + 对应 huiyuan_id不改写成 0
体验会员与正式会员同一套 clumber老端用 clumber.huiyuanid 对订单 huiyuan_id。
"""
grab_req = int(order_dict.get('GrabRequirement') or order_dict.get('yaoqiuleixing') or 0)
membership_id = resolve_order_membership_id(order_dict)
if grab_req != 1:
return grab_req, membership_id or '', True
has_valid = pool_display_has_valid_member_for_order(order_dict, yonghu_id, club_id=club_id)
return 1, membership_id or '', has_valid
def enrich_clumber_item(record, club_id=None):
"""老端 clumber体验会员与正式会员返回格式完全一致不暴露 is_trial"""
daoqi_str = format_legacy_daoqi(record.daoqi_time)
item = {
'huiyuanid': record.huiyuan_id,
'huiyuan_id': record.huiyuan_id,
'huiyuanming': record.jieshao or f'会员{record.huiyuan_id}',
'daoqi': daoqi_str,
'huiyuan_zhuangtai': 1,
'shifou_daoqi': False,
}
if is_bundle_huiyuan(record.huiyuan_id):
item['is_bundle'] = True
item['bundle_huiyuan_id'] = record.huiyuan_id
item['included_huiyuan_ids'] = resolve_bundle_included_ids(
record.huiyuan_id,
club_id=club_id,
yonghu_id=record.yonghu_id,
record_club_id=record.club_id,
)
return item
def _append_clumber_id_variants(items, seen, base_item, huiyuan_id):
"""为老端 == 比较补齐多种会员 ID 写法。"""
for vid in expand_huiyuan_id_variants(huiyuan_id):
if vid in seen:
continue
seen.add(vid)
items.append({
**base_item,
'huiyuanid': vid,
'huiyuan_id': vid,
})
def _append_bundle_sub_clumber(items, seen, base_item):
"""总会员有效时,为抢单页展开子会员虚拟条目(体验=正式)。"""
if not base_item.get('is_bundle'):
return
bundle_hid = str(
base_item.get('bundle_huiyuan_id')
or base_item.get('huiyuanid')
or base_item.get('huiyuan_id')
or '',
)
for sub_id in base_item.get('included_huiyuan_ids') or []:
sid = str(sub_id).strip()
if not sid:
continue
sub_base = {
'huiyuanming': base_item.get('huiyuanming', ''),
'daoqi': base_item.get('daoqi', ''),
'from_bundle': bundle_hid,
'huiyuan_zhuangtai': 1,
'shifou_daoqi': False,
}
_append_clumber_id_variants(items, seen, sub_base, sid)
def _build_user_clumber_core(yonghu_id, club_id=None):
"""huiyuangoumai 未过期会员 → clumber体验与正式完全同等不读 is_trial"""
items = []
seen = set()
effective_club = _resolve_user_club_id(yonghu_id, club_id)
for record in Huiyuangoumai.query.filter(yonghu_id=yonghu_id):
if not _membership_record_active(record):
continue
base = enrich_clumber_item(record, club_id=effective_club)
_append_clumber_id_variants(items, seen, base, record.huiyuan_id)
_append_bundle_sub_clumber(items, seen, base)
return items
def _merge_clumber_extra_items(items, extra):
"""只读合并:有效期内充值单合成的会员条目补进 clumber体验=正式)。"""
if not extra:
return items
seen_cores = set()
for it in items:
hid = normalize_huiyuan_id(it.get('huiyuanid') or it.get('huiyuan_id'))
if hid:
seen_cores.add(hid.lstrip('0') or '0')
for it in extra:
hid = normalize_huiyuan_id(it.get('huiyuanid') or it.get('huiyuan_id'))
if not hid:
continue
core = hid.lstrip('0') or '0'
if core in seen_cores:
continue
items.append(it)
seen_cores.add(core)
return items
def _build_clumber_from_paid_czjilu(yonghu_id, club_id=None):
"""只读:有效期内充值单合成 clumber体验=正式,不写库);总会员展开子类型。"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.member_recharge import (
MEMBER_LEIXING,
MEMBER_PAID_STATUS,
czjilu_entitlement_active,
_czjilu_entitlement_end,
)
from products.models import Czjilu, Huiyuan
items = []
seen = set()
effective_club = _resolve_user_club_id(yonghu_id, club_id)
for order in Czjilu.query.filter(
yonghuid=yonghu_id,
leixing=MEMBER_LEIXING,
zhuangtai=MEMBER_PAID_STATUS,
).order_by('-CreateTime')[:30]:
hid = normalize_huiyuan_id(order.huiyuan_id)
if not hid:
continue
cid = getattr(order, 'club_id', None) or effective_club or CLUB_ID_DEFAULT
if not czjilu_entitlement_active(order, cid):
continue
try:
hy = Huiyuan.query.get(huiyuan_id=hid)
ming = hy.jieshao or f'会员{hid}'
except Huiyuan.DoesNotExist:
ming = f'会员{hid}'
daoqi = _czjilu_entitlement_end(order, cid)
base = {
'huiyuanming': ming,
'daoqi': format_legacy_daoqi(daoqi),
'huiyuan_zhuangtai': 1,
'shifou_daoqi': False,
}
if is_bundle_huiyuan(hid):
base['is_bundle'] = True
base['bundle_huiyuan_id'] = hid
base['included_huiyuan_ids'] = resolve_bundle_included_ids(
hid,
club_id=club_id,
yonghu_id=yonghu_id,
record_club_id=getattr(order, 'club_id', None),
)
_append_clumber_id_variants(items, seen, base, hid)
_append_bundle_sub_clumber(items, seen, base)
return items
def _finalize_clumber_for_legacy_client(items):
"""老端只认 huiyuanid去掉 is_trial / is_gift 等内部字段。"""
cleaned = []
for it in items or []:
row = dict(it)
row.pop('is_trial', None)
row.pop('is_gift', None)
cleaned.append(row)
return cleaned
def ensure_grab_pool_clumber(yonghu_id, club_id=None):
"""
dddhq / dashouxinxi只读 huiyuangoumai + 有效期内充值单 + 赠送会员 生成 clumber。
总会员展开子会员虚拟条目供老端 clumber 比对;不写库、不补开通。
"""
from jituan.services.gift_huiyuan import build_gift_clumber_items
effective_club = _resolve_user_club_id(yonghu_id, club_id)
items = _build_user_clumber_core(yonghu_id, club_id=effective_club)
items = _merge_clumber_extra_items(
items,
_build_clumber_from_paid_czjilu(yonghu_id, club_id=effective_club),
)
items = _merge_clumber_extra_items(
items,
build_gift_clumber_items(yonghu_id, club_id=effective_club),
)
items = _finalize_clumber_for_legacy_client(items)
return bool(items), items
def refresh_grab_pool_membership_state(yonghu_id, club_id=None):
"""
抢单池/打手资料:只读生成 clumber含总会员子类型展开
供 /dingdan/ddhq、/yonghu/dashouxinxi、登录 clumber 调用;禁止写库补会员。
"""
return ensure_grab_pool_clumber(yonghu_id, club_id=club_id)
def build_user_clumber(yonghu_id, club_id=None):
"""
老小程序抢单页 clumber体验=正式格式)。
仅展示用;只含未过期会员。
"""
_, items = ensure_grab_pool_clumber(yonghu_id, club_id=club_id)
return items
def build_auth_clumber(yonghu_id, club_id=None):
"""登录 clumber与 dddhq / dashouxinxi 同源,每次刷新会员状态。"""
return refresh_grab_pool_membership_state(yonghu_id, club_id=club_id)[1]
def save_bundle_includes(club_id, bundle_huiyuan_id, included_ids):
"""保存俱乐部总会员包含的子会员(须为本俱乐部已上架的普通会员)。"""
from jituan.models import ClubHuiyuanBundleInclude, ClubHuiyuanPrice
if not is_bundle_huiyuan(bundle_huiyuan_id):
return None, '该会员不是总会员类型'
enabled = set(
ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True).values_list(
'huiyuan_id', flat=True,
)
)
bundle_ids = set(
Huiyuan.query.filter(is_bundle=True).values_list('huiyuan_id', flat=True)
)
cleaned = []
for hid in included_ids or []:
hid = str(hid).strip()
if not hid or hid == bundle_huiyuan_id:
continue
if hid not in enabled:
return None, f'子会员 {hid} 未在本俱乐部上架'
if hid in bundle_ids:
return None, '总会员不能包含另一个总会员'
if not Huiyuan.query.filter(huiyuan_id=hid).exists():
return None, f'子会员 {hid} 不存在'
cleaned.append(hid)
if not cleaned:
return None, '请至少选择一个包含的子会员'
ClubHuiyuanBundleInclude.query.filter(
club_id=club_id, bundle_huiyuan_id=bundle_huiyuan_id,
).delete()
for hid in cleaned:
ClubHuiyuanBundleInclude.query.create(
club_id=club_id,
bundle_huiyuan_id=bundle_huiyuan_id,
included_huiyuan_id=hid,
)
return {'included_huiyuan_ids': cleaned}, None