diff --git a/backend/views/members.py b/backend/views/members.py index bd78011..3abeef0 100644 --- a/backend/views/members.py +++ b/backend/views/members.py @@ -206,6 +206,7 @@ class UpdateMemberView(APIView): 'trial_zuzhangfc': request.data.get('trial_zuzhangfc'), 'card_image': request.data.get('card_image'), 'card_bg': request.data.get('card_bg'), + 'included_huiyuan_ids': request.data.get('included_huiyuan_ids'), } data, err = update_member_for_club( request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, extra=extra, @@ -270,7 +271,10 @@ class AddMemberView(APIView): 'msg': '仅集团汇总视图可新建全局会员类型;俱乐部请从会员 catalog 上架', }) - data, err = add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao) + data, err = add_global_member( + jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, + is_bundle=bool(request.data.get('is_bundle')), + ) if err: return Response({'code': 400, 'msg': err}) return Response({'code': 0, 'msg': '添加成功', 'data': data}) diff --git a/jituan/migrations/0010_club_huiyuan_bundle_include.py b/jituan/migrations/0010_club_huiyuan_bundle_include.py new file mode 100644 index 0000000..d9ba1aa --- /dev/null +++ b/jituan/migrations/0010_club_huiyuan_bundle_include.py @@ -0,0 +1,26 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('jituan', '0009_club_huiyuan_recharge_ui'), + ] + + operations = [ + migrations.CreateModel( + name='ClubHuiyuanBundleInclude', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=16)), + ('bundle_huiyuan_id', models.CharField(db_index=True, max_length=6, verbose_name='总会员ID')), + ('included_huiyuan_id', models.CharField(db_index=True, max_length=6, verbose_name='包含的子会员ID')), + ('CreateTime', models.DateTimeField(auto_now_add=True)), + ('UpdateTime', models.DateTimeField(auto_now=True)), + ], + options={ + 'db_table': 'club_huiyuan_bundle_include', + 'unique_together': {('club_id', 'bundle_huiyuan_id', 'included_huiyuan_id')}, + }, + ), + ] diff --git a/jituan/models.py b/jituan/models.py index 918e713..09151fa 100644 --- a/jituan/models.py +++ b/jituan/models.py @@ -127,6 +127,19 @@ class ClubHuiyuanPrice(QModel): unique_together = [['club_id', 'huiyuan_id']] +class ClubHuiyuanBundleInclude(QModel): + """俱乐部总会员包含的子会员类型(仅 is_bundle 会员使用)。""" + club_id = models.CharField(max_length=16, db_index=True) + bundle_huiyuan_id = models.CharField(max_length=6, db_index=True, verbose_name='总会员ID') + included_huiyuan_id = models.CharField(max_length=6, db_index=True, verbose_name='包含的子会员ID') + CreateTime = models.DateTimeField(auto_now_add=True) + UpdateTime = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'club_huiyuan_bundle_include' + unique_together = [['club_id', 'bundle_huiyuan_id', 'included_huiyuan_id']] + + class ClubShangpinLeixingConfig(QModel): """各俱乐部小程序商品类型上架与图标(全局 ShangpinLeixing 由集团定义)。""" club_id = models.CharField(max_length=16, db_index=True) diff --git a/jituan/services/club_member_admin.py b/jituan/services/club_member_admin.py index 4adc402..c6256bb 100644 --- a/jituan/services/club_member_admin.py +++ b/jituan/services/club_member_admin.py @@ -36,7 +36,7 @@ def _price_row_to_dict(row): } -def _format_member(m, price_row=None): +def _format_member(m, price_row=None, club_id=None): global_jiage = str(m.jiage) global_guanshifc = str(m.guanshifc) global_zuzhangfc = str(m.zuzhangfc) @@ -62,6 +62,8 @@ def _format_member(m, price_row=None): 'trial_days': 0, 'trial_guanshifc': '0.00', 'trial_zuzhangfc': '0.00', + 'is_bundle': bool(getattr(m, 'is_bundle', False)), + 'included_huiyuan_ids': [], } if price_row is not None: item['jiage'] = str(price_row.jiage) @@ -70,6 +72,9 @@ def _format_member(m, price_row=None): item['club_enabled'] = bool(price_row.is_enabled) item['price_source'] = 'club' item.update(_price_row_to_dict(price_row)) + if item.get('is_bundle') and club_id: + from jituan.services.huiyuan_bundle import get_bundle_included_ids + item['included_huiyuan_ids'] = get_bundle_included_ids(club_id, m.huiyuan_id) return item @@ -110,7 +115,7 @@ def build_member_list_payload(request): continue if row is not None and not row.is_enabled: continue - item = _format_member(m, row) + item = _format_member(m, row, club_id=club_id) item['goumai_cishu'] = count_club_member_sales(club_id, m.huiyuan_id) member_list.append(item) @@ -142,6 +147,7 @@ def build_member_catalog_payload(request): 'huiyuan_id': m.huiyuan_id, 'jieshao': m.jieshao, 'jtjieshao': m.jtjieshao, + 'is_bundle': bool(getattr(m, 'is_bundle', False)), }) return {'club_id': club_id, 'catalog': catalog} @@ -179,6 +185,14 @@ def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzha club_id = club_id_for_write(request) extra = extra or {} + try: + member_preview = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + if getattr(member_preview, 'is_bundle', False): + extra = dict(extra) + extra['trial_enabled'] = False + except Huiyuan.DoesNotExist: + pass + try: trial_fields = _parse_trial_fields(extra, jiage, guanshifc, zuzhangfc) except ValueError as e: @@ -219,6 +233,12 @@ def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzha row.card_bg = (extra.get('card_bg') or '').strip() row.save() + if 'included_huiyuan_ids' in extra and getattr(member, 'is_bundle', False): + from jituan.services.huiyuan_bundle import save_bundle_includes + _, err = save_bundle_includes(club_id, huiyuan_id, extra.get('included_huiyuan_ids')) + if err: + return None, err + if club_id == CLUB_ID_DEFAULT: member.jiage = jiage member.guanshifc = guanshifc @@ -272,6 +292,10 @@ def enable_member_for_club(request, huiyuan_id, jiage, guanshifc, zuzhangfc, ext except Huiyuan.DoesNotExist: return None, '集团会员类型不存在' + if getattr(member, 'is_bundle', False): + extra = dict(extra or {}) + extra['trial_enabled'] = False + try: jiage = Decimal(str(jiage)) guanshifc = Decimal(str(guanshifc)) @@ -302,6 +326,11 @@ def enable_member_for_club(request, huiyuan_id, jiage, guanshifc, zuzhangfc, ext for k, v in trial_fields.items(): setattr(row, k, v) row.save() + if getattr(member, 'is_bundle', False) and extra.get('included_huiyuan_ids') is not None: + from jituan.services.huiyuan_bundle import save_bundle_includes + _, err = save_bundle_includes(club_id, huiyuan_id, extra.get('included_huiyuan_ids')) + if err: + return None, err return {'huiyuan_id': huiyuan_id}, None @@ -318,7 +347,7 @@ def disable_member_for_club(request, huiyuan_id): return {'huiyuan_id': huiyuan_id}, None -def add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_id=None): +def add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_id=None, is_bundle=False): """集团创建全局会员类型(仅集团后台)。""" def generate_huiyuan_id(): @@ -338,6 +367,7 @@ def add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_i zuzhangfc=zuzhangfc, jtjieshao=jtjieshao or '', goumai_cishu=0, + is_bundle=bool(is_bundle), ) if bankuai_id is not None: create_kwargs['bankuai_id'] = bankuai_id diff --git a/jituan/services/huiyuan_bundle.py b/jituan/services/huiyuan_bundle.py new file mode 100644 index 0000000..d434fa0 --- /dev/null +++ b/jituan/services/huiyuan_bundle.py @@ -0,0 +1,117 @@ +"""总会员(包含式会员):购买一个会员,可接多个子会员类型订单。""" +from products.models import Huiyuan, Huiyuangoumai + + +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 [] + return list( + ClubHuiyuanBundleInclude.query.filter( + club_id=club_id, + bundle_huiyuan_id=bundle_huiyuan_id, + ).values_list('included_huiyuan_id', flat=True) + ) + + +def is_bundle_huiyuan(huiyuan_id): + try: + return bool(Huiyuan.query.get(huiyuan_id=huiyuan_id).is_bundle) + except Huiyuan.DoesNotExist: + return False + + +def user_has_huiyuan_access(yonghu_id, required_huiyuan_id, club_id=None): + """用户是否可接要求 required_huiyuan_id 的会员单(含总会员覆盖)。""" + if not required_huiyuan_id: + return True + + req = str(required_huiyuan_id).strip() + + def _record_valid(rec): + return ( + rec + and not rec.jiance_shifou_daoqi() + and rec.huiyuan_zhuangtai == 1 + ) + + try: + direct = Huiyuangoumai.query.get(yonghu_id=yonghu_id, huiyuan_id=req) + if _record_valid(direct): + return True + except Huiyuangoumai.DoesNotExist: + pass + + bundle_ids = set( + Huiyuan.query.filter(is_bundle=True).values_list('huiyuan_id', flat=True) + ) + if not bundle_ids: + return False + + for rec in Huiyuangoumai.query.filter(yonghu_id=yonghu_id, huiyuan_id__in=bundle_ids): + if not _record_valid(rec): + continue + cid = club_id or rec.club_id or '' + included = get_bundle_included_ids(cid, rec.huiyuan_id) + if req in {str(x) for x in included}: + return True + return False + + +def enrich_clumber_item(record): + """为 clumber 条目附加总会员覆盖的子会员 ID 列表(旧端忽略该字段)。""" + item = { + 'huiyuanid': record.huiyuan_id, + 'huiyuanming': record.jieshao or f'会员{record.huiyuan_id}', + 'daoqi': record.daoqi_time, + } + if is_bundle_huiyuan(record.huiyuan_id): + item['is_bundle'] = True + item['included_huiyuan_ids'] = get_bundle_included_ids( + record.club_id or '', record.huiyuan_id, + ) + return item + + +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 diff --git a/orders/views/dashou.py b/orders/views/dashou.py index c86e1b8..47efacf 100644 --- a/orders/views/dashou.py +++ b/orders/views/dashou.py @@ -527,16 +527,17 @@ class QiangdanView(APIView): return {'success': True, 'message': '验证通过'} def _validate_huiyuan_requirement(self, order, dashou_yonghuid): + from jituan.services.huiyuan_bundle import user_has_huiyuan_access + from jituan.services.club_context import resolve_effective_club_id + + club_id = None try: - huiyuan_record = Huiyuangoumai.query.get( - huiyuan_id=order.MembershipID, - yonghu_id=dashou_yonghuid - ) - if huiyuan_record.jiance_shifou_daoqi() or huiyuan_record.huiyuan_zhuangtai != 1: - return {'success': False, 'message': '会员已过期或未开通对应会员'} + club_id = resolve_effective_club_id(self.request, self.request.user) + except Exception: + pass + if user_has_huiyuan_access(dashou_yonghuid, order.MembershipID, club_id=club_id): return {'success': True, 'message': '会员验证通过'} - except Huiyuangoumai.DoesNotExist: - return {'success': False, 'message': '未开通对应会员,无法抢此订单'} + return {'success': False, 'message': '未开通对应会员,无法抢此订单'} def _validate_yajin_requirement(self, order, dashou_profile): yajin_required = order.CommissionReq or 0 diff --git a/products/migrations/0011_huiyuan_is_bundle.py b/products/migrations/0011_huiyuan_is_bundle.py new file mode 100644 index 0000000..e3d6a45 --- /dev/null +++ b/products/migrations/0011_huiyuan_is_bundle.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0010_backfill_penalty_czjilu_leixing'), + ] + + operations = [ + migrations.AddField( + model_name='huiyuan', + name='is_bundle', + field=models.BooleanField(default=False, verbose_name='是否为总会员(包含式)'), + ), + ] diff --git a/products/models.py b/products/models.py index 6afc506..a1512ac 100644 --- a/products/models.py +++ b/products/models.py @@ -150,6 +150,7 @@ class Huiyuan(QModel): blank=True, verbose_name='所属板块' ) + is_bundle = models.BooleanField(default=False, verbose_name='是否为总会员(包含式)') class Meta: db_table = 'huiyuan' diff --git a/products/views/product_query.py b/products/views/product_query.py index 9efaad2..6dc17e3 100644 --- a/products/views/product_query.py +++ b/products/views/product_query.py @@ -296,16 +296,18 @@ class DashouHuiyuanList(APIView): club_row = club_rows.get(huiyuan.huiyuan_id) card_image = (club_row.card_image or '').strip() if club_row else '' card_bg = (club_row.card_bg or '').strip() if club_row else '' + is_bundle = bool(getattr(huiyuan, 'is_bundle', False)) huiyuan_list.append({ 'id': huiyuan.huiyuan_id, 'mingzi': huiyuan.jieshao, 'jiage': format_yuan(club_price), 'formal_days': formal_days, 'jieshao': huiyuan.jtjieshao, - 'trial_enabled': bool(trial_sellable), - 'trial_price': format_yuan(trial_price) if trial_sellable else '0.00', - 'trial_days': trial_days if trial_sellable else 0, - 'can_buy_trial': bool(trial_ok and trial_sellable), + 'is_bundle': is_bundle, + 'trial_enabled': False if is_bundle else bool(trial_sellable), + 'trial_price': '0.00' if is_bundle else (format_yuan(trial_price) if trial_sellable else '0.00'), + 'trial_days': 0 if is_bundle else (trial_days if trial_sellable else 0), + 'can_buy_trial': False if is_bundle else bool(trial_ok and trial_sellable), 'card_image': card_image, 'card_bg': card_bg, }) diff --git a/users/views/user_info.py b/users/views/user_info.py index d93c8b2..c709603 100644 --- a/users/views/user_info.py +++ b/users/views/user_info.py @@ -349,11 +349,8 @@ class DashouXinxiAPIView(APIView): if record.jiance_shifou_daoqi(): continue if record.huiyuan_zhuangtai == 1: - huiyuan_list.append({ - 'huiyuanid': record.huiyuan_id, - 'huiyuanming': record.jieshao or f"会员{record.huiyuan_id}", - 'daoqi': record.daoqi_time - }) + from jituan.services.huiyuan_bundle import enrich_clumber_item + huiyuan_list.append(enrich_clumber_item(record)) except Exception as e: # 会员信息查询失败不影响主要功能,返回空列表 huiyuan_list = []