diff --git a/backend/views/products.py b/backend/views/products.py index 3992386..9de46a5 100644 --- a/backend/views/products.py +++ b/backend/views/products.py @@ -643,11 +643,26 @@ class SaveProductOrderView(APIView): except: return Response({'code': 400, 'msg': 'id或paixu格式错误'}) + from jituan.constants import DATA_SCOPE_ALL + from jituan.services.club_context import ( + filter_club_char_field, + resolve_club_scope, + ) + from jituan.services.club_shangpin_leixing import save_club_leixing_order + + club_scoped = resolve_club_scope(request) != DATA_SCOPE_ALL + with transaction.atomic(): if sort_type == 'leixing': - # 更新商品类型的排序 - for item in items: - ShangpinLeixing.query.filter(id=item['id']).update(paixu=item['paixu']) + if club_scoped: + # 单店:写 ClubShangpinLeixingConfig.paixu,各俱乐部互不影响 + err = save_club_leixing_order(request, items) + if err: + return Response({'code': 400, 'msg': err}) + else: + # 集团汇总:仍写全局类型排序(仅集团视图) + for item in items: + ShangpinLeixing.query.filter(id=item['id']).update(paixu=item['paixu']) else: # zhuanqu leixing_id = request.data.get('leixing_id') if not leixing_id: @@ -656,9 +671,12 @@ class SaveProductOrderView(APIView): leixing_id = int(leixing_id) except: return Response({'code': 400, 'msg': 'leixing_id格式错误'}) - # 可选:检查该专区是否属于该类型,但不强制,由前端保证 + # 专区本身按 club_id 隔离;更新时再按当前店过滤,防止串店 for item in items: - ShangpinZhuanqu.query.filter(id=item['id']).update(paixu=item['paixu']) + qs = ShangpinZhuanqu.query.filter(id=item['id'], leixing_id=leixing_id) + if club_scoped: + qs = filter_club_char_field(qs, request, field='club_id') + qs.update(paixu=item['paixu']) return Response({'code': 0, 'msg': '排序保存成功'}) diff --git a/config/views/merchant_links.py b/config/views/merchant_links.py index cb67980..9cd3260 100644 --- a/config/views/merchant_links.py +++ b/config/views/merchant_links.py @@ -95,6 +95,11 @@ class ShangjiaGenerateLinkView(APIView): except ShangpinLeixing.DoesNotExist: return Response({'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=400) + club_id = resolve_club_id_from_request(request) + from jituan.services.club_shangpin_leixing import is_leixing_enabled_for_club + if not is_leixing_enabled_for_club(club_id, shangpin_type_id): + return Response({'code': 400, 'msg': '该商品类型未在本俱乐部上架', 'data': {}}, status=400) + # 查询模板 try: moban = ShangjiaMoban.query.get(id=moban_id, UserID=user.UserUID, ProductTypeID=shangpin_type_id) @@ -106,7 +111,6 @@ class ShangjiaGenerateLinkView(APIView): return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元', 'data': {}}, status=400) # 打手+管事分成(与普通派单 sjpaifa 同一套算法,打手优先) - club_id = resolve_club_id_from_request(request) try: from jituan.services.order_deal import check_merchant_dispatch_block block = check_merchant_dispatch_block(club_id, user.UserUID) diff --git a/config/views/merchant_templates.py b/config/views/merchant_templates.py index fd84a92..1d8f3e7 100644 --- a/config/views/merchant_templates.py +++ b/config/views/merchant_templates.py @@ -266,7 +266,7 @@ class ShangjiaTianjiaMobanView(APIView): except (ValueError, TypeError): return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400) - # 8. 验证商品类型是否存在且可用 + # 8. 验证商品类型是否存在且可用(且本俱乐部已上架) try: shangpin_obj = ShangpinLeixing.query.get( id=leixing_id, @@ -275,6 +275,11 @@ class ShangjiaTianjiaMobanView(APIView): except ShangpinLeixing.DoesNotExist: return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404) + from jituan.services.club_context import resolve_club_id_from_request + from jituan.services.club_shangpin_leixing import is_leixing_enabled_for_club + if not is_leixing_enabled_for_club(resolve_club_id_from_request(request), leixing_id): + return Response({'code': 400, 'msg': '该商品类型未在本俱乐部上架', 'data': {}}, status=400) + # 9. 检查同一用户、同一类型下是否已存在相同介绍的模板 if ShangjiaMoban.query.filter( UserID=yonghu_id, diff --git a/jituan/services/club_shangpin_leixing.py b/jituan/services/club_shangpin_leixing.py index 29d41c1..0acf2d1 100644 --- a/jituan/services/club_shangpin_leixing.py +++ b/jituan/services/club_shangpin_leixing.py @@ -49,6 +49,10 @@ def _is_visible(club_id, leixing_id, cfg): def build_leixing_item(leixing, cfg=None): + if cfg is not None: + paixu = int(cfg.paixu or 0) + else: + paixu = int(leixing.paixu or 0) item = { 'id': leixing.id, 'jieshao': leixing.jieshao or '', @@ -57,7 +61,7 @@ def build_leixing_item(leixing, cfg=None): 'yaoqiuleixing': leixing.yaoqiuleixing, 'huiyuan_id': leixing.huiyuan_id, 'yongjin': float(leixing.yongjin) if leixing.yongjin else None, - 'paixu': (cfg.paixu if cfg and cfg.paixu else leixing.paixu) or 0, + 'paixu': paixu, 'shenhezhuangtai': leixing.shenhezhuangtai or 1, 'bankuai_id': leixing.bankuai_id, 'club_enabled': True, @@ -68,6 +72,44 @@ def build_leixing_item(leixing, cfg=None): return item +def is_leixing_enabled_for_club(club_id, leixing_id): + """本俱乐部是否已上架该商品类型。""" + club_id = (club_id or '').strip() + if not club_id or leixing_id is None: + return False + cfg = ClubShangpinLeixingConfig.query.filter( + club_id=club_id, leixing_id=int(leixing_id), + ).first() + return _is_visible(club_id, int(leixing_id), cfg) + + +def save_club_leixing_order(request, items): + """ + 按本俱乐部写入类型排序(互不影响全局/其他店)。 + items: [{id, paixu}, ...] + 成功返回 None,失败返回错误文案。 + """ + if club_write_blocked(request): + return CLUB_WRITE_SCOPE_MSG + club_id = club_id_for_write(request) + if not club_id: + return '无法解析当前俱乐部' + with transaction.atomic(): + for item in items: + leixing_id = int(item['id']) + paixu = int(item['paixu']) + row, _ = ClubShangpinLeixingConfig.query.get_or_create( + club_id=club_id, + leixing_id=leixing_id, + defaults={'is_enabled': True, 'paixu': paixu}, + ) + if not row.is_enabled: + continue + row.paixu = paixu + row.save(update_fields=['paixu', 'UpdateTime']) + return None + + def build_club_leixing_list_for_api(club_id): """小程序/抢单池:仅返回本俱乐部已上架的类型,图标优先俱乐部配置。""" cfg_map = _config_map(club_id) diff --git a/merchant_ops/services/dispatch.py b/merchant_ops/services/dispatch.py index 6194f76..5ba0846 100644 --- a/merchant_ops/services/dispatch.py +++ b/merchant_ops/services/dispatch.py @@ -63,6 +63,11 @@ def staff_dispatch_order(member, staff_user, data): except ShangpinLeixing.DoesNotExist: return False, {'code': 400, 'msg': '商品类型不存在', 'data': None} + club_id = getattr(member, 'club_id', None) or '' + from jituan.services.club_shangpin_leixing import is_leixing_enabled_for_club + if not is_leixing_enabled_for_club(club_id, shangpin_type.id): + return False, {'code': 400, 'msg': '该商品类型未在本俱乐部上架', 'data': None} + if commission_enabled and commission_value_str: try: cv = float(commission_value_str) diff --git a/merchant_ops/services/link.py b/merchant_ops/services/link.py index 1821f3b..c8ffa9f 100644 --- a/merchant_ops/services/link.py +++ b/merchant_ops/services/link.py @@ -106,6 +106,10 @@ def staff_generate_link(member, staff_user, data): except ShangpinLeixing.DoesNotExist: return False, {'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}} + from jituan.services.club_shangpin_leixing import is_leixing_enabled_for_club + if not is_leixing_enabled_for_club(club_id, shangpin_type_id): + return False, {'code': 400, 'msg': '该商品类型未在本俱乐部上架', 'data': {}} + try: moban = ShangjiaMoban.query.get( id=moban_id, UserID=merchant_id, ProductTypeID=shangpin_type_id, diff --git a/orders/views/merchant.py b/orders/views/merchant.py index 25e4a71..d37bd3d 100644 --- a/orders/views/merchant.py +++ b/orders/views/merchant.py @@ -116,21 +116,23 @@ class ShangpinLeixingView(APIView): return Response({'code': 403, 'msg': '非商家或有效客服,无权获取订单类型', 'data': []}) try: - # 1. 查询审核通过且抢单要求类型为1的商品类型,按排序权重升序 - shangpin_queryset = ShangpinLeixing.query.filter( - yaoqiuleixing=1, - shenhezhuangtai=1 - ).select_related('bankuai').order_by('paixu') + from jituan.services.club_shangpin_leixing import build_club_leixing_list_for_api - # 2. 收集所有涉及到的板块ID - bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct() + # 仅本俱乐部已上架类型(生成链接 / 自定义发单 / 常规单统一入口) + club_id = resolve_club_id_from_request(request) + club_rows = [ + r for r in build_club_leixing_list_for_api(club_id) + if int(r.get('yaoqiuleixing') or 0) == 1 + and int(r.get('shenhezhuangtai') or 0) == 1 + ] - # 3. 查询这些板块下的所有称号(每个称号绑定到一个板块) + bankuai_ids = { + r.get('bankuai_id') for r in club_rows if r.get('bankuai_id') + } chenghao_list = Chenghao.query.filter( bankuai_id__in=bankuai_ids - ).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id') + ).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id') if bankuai_ids else [] - # 4. 构建 板块ID -> 称号列表 的映射(确保准确关联) bankuai_chenghao_map = {} for ch in chenghao_list: bid = ch['bankuai_id'] @@ -139,21 +141,18 @@ class ShangpinLeixingView(APIView): bankuai_chenghao_map[bid].append({ 'id': ch['id'], 'mingcheng': ch['mingcheng'], - 'texiao_json': ch['texiao_miaoshu'] # 前端 chenghao-tag 组件需要 + 'texiao_json': ch['texiao_miaoshu'], }) - # 5. 组装最终数据 data_list = [] - for sp in shangpin_queryset: - item = { - 'id': sp.id, - 'yaoqiuleixing': sp.yaoqiuleixing, - 'huiyuan_id': sp.huiyuan_id, - 'jieshao': sp.jieshao, - # 当前类型所属板块的称号列表(无板块则为空数组) - 'chenghaoList': bankuai_chenghao_map.get(sp.bankuai_id, []) - } - data_list.append(item) + for sp in club_rows: + data_list.append({ + 'id': sp['id'], + 'yaoqiuleixing': sp.get('yaoqiuleixing'), + 'huiyuan_id': sp.get('huiyuan_id'), + 'jieshao': sp.get('jieshao') or '', + 'chenghaoList': bankuai_chenghao_map.get(sp.get('bankuai_id'), []), + }) return Response({ 'code': 200, @@ -214,13 +213,18 @@ class ShangjiaPaifaView(APIView): except ValueError: return Response({'code': 400, 'msg': '价格格式错误'}) - # ----- 5. 商品类型 ----- + # ----- 5. 商品类型(必须本俱乐部已上架)----- try: shangpin_type = ShangpinLeixing.query.get(id=shangpin_type_id) except ShangpinLeixing.DoesNotExist: logger.error(f"商品类型不存在: {shangpin_type_id}") return Response({'code': 400, 'msg': '商品类型不存在'}) + from jituan.services.club_shangpin_leixing import is_leixing_enabled_for_club + _dispatch_club = resolve_club_id_from_request(request) + if not is_leixing_enabled_for_club(_dispatch_club, shangpin_type.id): + return Response({'code': 400, 'msg': '该商品类型未在本俱乐部上架'}) + # ----- 6. 押金 / 会员模式决定 ----- final_yongjin = None if commission_enabled and commission_value_str: