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

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

599 lines
21 KiB
Python

"""总会员(包含式会员):购买一个会员,可接多个子会员类型订单。"""
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