feat: 只看审核按俱乐部隔离,商品管理支持俱乐部视图

未绑店出货开关按 club_id 配置;后台商品列表可在具体小程序下管理并切换审核展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-21 03:20:03 +08:00
parent ed627fa6f7
commit 6515568ec0
9 changed files with 249 additions and 42 deletions

View File

@@ -5,7 +5,7 @@ from .view import (GetRolePermissionView,
ModifyRolePermissionView, AddRoleView, GetAdminRolesView, GetAdminUserListView, AddAdminUserView,
ModifyAdminUserView, KefuGetDashouListView, KefuGetDashouDetailView, KefuUpdateDashouView, KefuGetShangjiaListView,
KefuGetShangjiaDetailView, KefuUpdateShangjiaView, GetProductBaseDataView, GetProductListView, SaveProductOrderView,
AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView, UpdateMemberView, GetMemberListView,
AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView, UpdateZhiKanShenheView, UpdateMemberView, GetMemberListView,
AddMemberView, GetGuanliListView, GetGuanliDetailView, PromoteGuanshiToZuzhangView, ChangeInviterView, UpdateGuanliView, GetWithdrawSettingsView,
UpdateWithdrawSettingsView, GetZuzhangListView, GetZuzhangDetailView, UpdateZuzhangView, GetOperationLogListView, GetProductTypeZoneView,
ModifyProductTypeZoneView, GetRateView, ModifyRateView, PopupNoticeListAPIView, PopupNoticeModifyAPIView,ShopListView,
@@ -44,6 +44,7 @@ urlpatterns = [
path('htscsp', DeleteProductView.as_view(), name='后台删除商品'),
path('htsphqxq', GetProductDetailView.as_view(), name='后台获取商品详情'),
path('bcspxg', UpdateProductView.as_view(), name='后台更新商品数据'),
path('htxgzhikan', UpdateZhiKanShenheView.as_view(), name='后台更新只看审核开关'),
path('hthqhylb', GetMemberListView.as_view(), name='后台获取会员列表'),
path('htxghyxx', UpdateMemberView.as_view(), name='后台更新会员信息'),

View File

@@ -41,6 +41,7 @@ from .views.kefu import (
from .views.products import (
GetProductBaseDataView, GetProductListView, SaveProductOrderView,
AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView,
UpdateZhiKanShenheView,
)
from .views.members import (
GetMemberListView, UpdateMemberView, AddMemberView,
@@ -103,6 +104,7 @@ __all__ = [
# products
'GetProductBaseDataView', 'GetProductListView', 'SaveProductOrderView',
'AddProductView', 'DeleteProductView', 'GetProductDetailView', 'UpdateProductView',
'UpdateZhiKanShenheView',
# members
'GetMemberListView', 'UpdateMemberView', 'AddMemberView',
'ClubMemberCatalogView', 'ClubMemberEnableView', 'ClubMemberDisableView',

View File

@@ -142,14 +142,24 @@ class GetProductBaseDataView(APIView):
if '2200a' not in permissions:
return Response({'code': 403, 'msg': '无权限管理商品'})
# 获取所有商品类型(按 paixu 升序)
# 获取所有商品类型(按 paixu 升序);商品数按当前俱乐部统计
from jituan.services.club_context import filter_club_char_field, resolve_club_scope
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_write import resolve_club_id_for_write
from shop.services.shenhe_config import get_or_create_shenhe_config, is_zhi_kan_shenhe_enabled
type_qs = ShangpinLeixing.query.all().order_by('paixu')
# 批量聚合商品数量,避免循环内 N+1 count
# 批量聚合商品数量,避免循环内 N+1 count(按俱乐部)
_public_sp = filter_club_char_field(
Shangpin.query.filter(
dianpu__isnull=True, dianpu_leixing__isnull=True, leixing_id__isnull=False
),
request,
field='club_id',
)
_type_counts = {
item['leixing_id']: item['cnt']
for item in Shangpin.query.filter(
dianpu__isnull=True, dianpu_leixing__isnull=True, leixing_id__isnull=False
).values('leixing_id').annotate(cnt=Count('id'))
for item in _public_sp.values('leixing_id').annotate(cnt=Count('id'))
}
type_list = []
for t in type_qs:
@@ -164,7 +174,6 @@ class GetProductBaseDataView(APIView):
})
# 获取专区(按俱乐部隔离;集团汇总看全部)
from jituan.services.club_context import filter_club_char_field
zone_qs = filter_club_char_field(
ShangpinZhuanqu.query.all().order_by('paixu'), request, field='club_id'
)
@@ -194,12 +203,24 @@ class GetProductBaseDataView(APIView):
# 获取所有会员(只需 ID、介绍、价格
member_list = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage'))
# 本俱乐部「只看审核」开关(集团汇总不下发可写状态)
club_scoped = resolve_club_scope(request) != DATA_SCOPE_ALL
zhi_kan_shenhe = True
club_id = ''
if club_scoped:
club_id = resolve_club_id_for_write(request) or ''
get_or_create_shenhe_config(club_id)
zhi_kan_shenhe = is_zhi_kan_shenhe_enabled(club_id)
return Response({
'code': 0,
'data': {
'type_list': type_list,
'zone_list': zone_list,
'member_list': member_list,
'zhi_kan_shenhe': zhi_kan_shenhe,
'club_scoped': club_scoped,
'club_id': club_id,
}
})
@@ -701,6 +722,14 @@ class AddProductView(APIView):
zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id)
if zhuanqu_obj.leixing_id != leixing_id:
return Response({'code': 400, 'msg': '专区不属于该商品类型'})
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import resolve_club_scope
from jituan.services.club_write import resolve_club_id_for_write
if resolve_club_scope(request) != DATA_SCOPE_ALL:
write_club = resolve_club_id_for_write(request)
zone_club = getattr(zhuanqu_obj, 'club_id', None) or 'xq'
if zone_club != write_club:
return Response({'code': 400, 'msg': '专区不属于当前俱乐部,请先切换俱乐部或选择本俱乐部专区'})
except ShangpinZhuanqu.DoesNotExist:
return Response({'code': 400, 'msg': '商品专区不存在'})
@@ -1134,6 +1163,52 @@ class UpdateProductView(APIView):
return Response({'code': 0, 'msg': '商品修改成功', 'data': {'id': product.id}})
class UpdateZhiKanShenheView(APIView):
"""
商品管理页:按俱乐部更新「只看审核商品」开关。
权限2200a
POST /houtai/htxgzhikan
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '2200a' not in permissions:
return Response({'code': 403, 'msg': '无权限管理商品'})
val = request.data.get('zhi_kan_shenhe')
if val is None:
return Response({'code': 400, 'msg': '缺少参数 zhi_kan_shenhe'})
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import resolve_club_scope
from jituan.services.club_write import resolve_club_id_for_write
from shop.services.shenhe_config import get_or_create_shenhe_config
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return Response({'code': 400, 'msg': '请先切换到具体俱乐部再修改只看审核开关'})
try:
with transaction.atomic():
club_id = resolve_club_id_for_write(request)
config = get_or_create_shenhe_config(club_id)
config = DianpuShangpinShenheShezhi.objects.select_for_update().get(pk=config.pk)
config.zhi_kan_shenhe = bool(val)
config.save(update_fields=['zhi_kan_shenhe'])
return Response({
'code': 0,
'msg': '只看审核开关已更新',
'data': {'club_id': club_id, 'zhi_kan_shenhe': bool(val)},
})
except Exception:
logging.getLogger(__name__).exception('更新只看审核开关失败')
return Response({'code': 500, 'msg': '服务器错误'})

View File

@@ -421,17 +421,38 @@ class ShopPublicTypeAndAuditView(APIView):
logger.exception("查询公共商品类型失败")
return Response({'code': 500, 'msg': '服务器错误,请稍后重试'})
# ---------- 获取审核模式配置 ----------
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False
zhi_kan_shenhe = getattr(audit_config, 'zhi_kan_shenhe', True) if audit_config else True
# ---------- 获取本俱乐部审核模式配置 ----------
from jituan.services.club_write import resolve_club_id_for_write
from shop.services.shenhe_config import get_or_create_shenhe_config
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import resolve_club_scope
if resolve_club_scope(request) == DATA_SCOPE_ALL:
# 集团汇总:不自动创建,返回默认值提示前端切到具体俱乐部
return Response({
'code': 0,
'data': {
'public_types': type_list,
'kaiqi_shenhe': False,
'zhi_kan_shenhe': True,
'club_scoped': False,
'msg': '请切换到具体俱乐部查看/修改审核展示开关',
}
})
club_id = resolve_club_id_for_write(request)
audit_config = get_or_create_shenhe_config(club_id)
kaiqi_shenhe = audit_config.kaiqi_shenhe
zhi_kan_shenhe = bool(getattr(audit_config, 'zhi_kan_shenhe', True))
return Response({
'code': 0,
'data': {
'public_types': type_list,
'kaiqi_shenhe': kaiqi_shenhe,
'zhi_kan_shenhe': bool(zhi_kan_shenhe),
'zhi_kan_shenhe': zhi_kan_shenhe,
'club_id': club_id,
'club_scoped': True,
}
})
@@ -640,18 +661,28 @@ class ShopProductModifyView(APIView):
if kaiqi is None:
return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'})
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import resolve_club_scope
from jituan.services.club_write import resolve_club_id_for_write
from shop.services.shenhe_config import get_or_create_shenhe_config
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return Response({'code': 400, 'msg': '请先切换到具体俱乐部再修改审核模式'})
try:
config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1)
club_id = resolve_club_id_for_write(request)
config = get_or_create_shenhe_config(club_id)
config = DianpuShangpinShenheShezhi.objects.select_for_update().get(pk=config.pk)
config.kaiqi_shenhe = bool(kaiqi)
config.save(update_fields=['kaiqi_shenhe'])
return Response({'code': 0, 'msg': '审核模式已更新'})
return Response({'code': 0, 'msg': '审核模式已更新', 'data': {'club_id': club_id}})
except Exception as e:
logger.exception("更新审核模式失败")
return Response({'code': 500, 'msg': '服务器错误'})
@transaction.atomic
def _update_zhi_kan_shenhe(self, request, user_perms):
"""点单端:未绑店用户是否按身份只看审核专用商品。"""
"""点单端:未绑店用户是否按身份只看审核专用商品(按俱乐部)"""
if not has_perm_code(user_perms, '1199ab'):
return Response({'code': 403, 'msg': '无权限修改只看审核开关'})
@@ -659,11 +690,21 @@ class ShopProductModifyView(APIView):
if val is None:
return Response({'code': 400, 'msg': '缺少参数 zhi_kan_shenhe'})
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import resolve_club_scope
from jituan.services.club_write import resolve_club_id_for_write
from shop.services.shenhe_config import get_or_create_shenhe_config
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return Response({'code': 400, 'msg': '请先切换到具体俱乐部再修改只看审核开关'})
try:
config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1)
club_id = resolve_club_id_for_write(request)
config = get_or_create_shenhe_config(club_id)
config = DianpuShangpinShenheShezhi.objects.select_for_update().get(pk=config.pk)
config.zhi_kan_shenhe = bool(val)
config.save(update_fields=['zhi_kan_shenhe'])
return Response({'code': 0, 'msg': '只看审核开关已更新'})
return Response({'code': 0, 'msg': '只看审核开关已更新', 'data': {'club_id': club_id}})
except Exception:
logger.exception("更新只看审核开关失败")
return Response({'code': 500, 'msg': '服务器错误'})

View File

@@ -0,0 +1,44 @@
from django.db import migrations, models
def forwards_fill_club_id(apps, schema_editor):
Model = apps.get_model('shop', 'DianpuShangpinShenheShezhi')
# 历史全局单例 → 归到星阙 xq若已有多行则只保留一条
rows = list(Model.objects.all().order_by('id'))
if not rows:
return
keep = rows[0]
keep.club_id = keep.club_id or 'xq'
keep.save(update_fields=['club_id'])
for extra in rows[1:]:
extra.delete()
def noop_reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('shop', '0003_zhi_kan_shenhe'),
]
operations = [
migrations.AddField(
model_name='dianpushangpinshenheshezhi',
name='club_id',
field=models.CharField(
db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部',
),
),
migrations.RunPython(forwards_fill_club_id, noop_reverse),
migrations.AlterField(
model_name='dianpushangpinshenheshezhi',
name='club_id',
field=models.CharField(
db_index=True, default='xq', max_length=16, unique=True,
verbose_name='所属俱乐部',
),
),
]

View File

@@ -211,11 +211,14 @@ class ShangpinLeixingDianpu(QModel):
# shangdian/models.py 或独立应用 models.py
class DianpuShangpinShenheShezhi(QModel):
"""店铺商品审核模式全局配置(仅一条记录)"""
"""点单展示 / 店铺商品审核:按俱乐部各一条。"""
club_id = models.CharField(
max_length=16, unique=True, default='xq', db_index=True,
verbose_name='所属俱乐部',
)
kaiqi_shenhe = models.BooleanField(default=False, verbose_name='是否开启商品审核模式')
# True=未注册打手等特殊身份只看审核专用商品False=未绑店一律看正常公共商品
# True=未绑店且无打手等身份只看审核专用False=未绑店一律看正常公共商品
zhi_kan_shenhe = models.BooleanField(default=True, verbose_name='是否只看审核商品')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
@@ -224,7 +227,3 @@ class DianpuShangpinShenheShezhi(QModel):
db_table = 'dianpu_shangpin_shenhe_shezhi'
verbose_name = '商品审核模式配置'
verbose_name_plural = verbose_name
def save(self, *args, **kwargs):
self.pk = 1 # 强制使用 ID=1 保证唯一
super().save(*args, **kwargs)

View File

@@ -0,0 +1,43 @@
"""点单端商品展示 / 店铺审核模式:按俱乐部隔离。"""
from jituan.constants import CLUB_ID_DEFAULT
from shop.models import DianpuShangpinShenheShezhi
def _norm_club_id(club_id):
return (club_id or CLUB_ID_DEFAULT).strip() or CLUB_ID_DEFAULT
def get_or_create_shenhe_config(club_id):
"""获取或创建某俱乐部的审核展示配置。"""
cid = _norm_club_id(club_id)
config, _ = DianpuShangpinShenheShezhi.objects.get_or_create(
club_id=cid,
defaults={
'kaiqi_shenhe': False,
'zhi_kan_shenhe': True,
},
)
return config
def is_zhi_kan_shenhe_enabled(club_id):
"""
未绑店用户是否只看审核专用商品。
无配置时默认 True保持历史行为。
"""
config = DianpuShangpinShenheShezhi.query.filter(
club_id=_norm_club_id(club_id)
).first()
if not config:
return True
return bool(getattr(config, 'zhi_kan_shenhe', True))
def is_kaiqi_shenhe_enabled(club_id):
"""店铺发布商品是否需平台审核。无配置默认 False。"""
config = DianpuShangpinShenheShezhi.query.filter(
club_id=_norm_club_id(club_id)
).first()
if not config:
return False
return bool(config.kaiqi_shenhe)

View File

@@ -139,22 +139,20 @@ class ShopGoodsView(APIView):
# 店铺封禁或不属于当前俱乐部 → 公共正常商品
data = self._get_public_goods(True)
else:
# 未绑店:受「是否只看审核」开关控制
# 未绑店:受本俱乐部「是否只看审核」开关控制
# 开启 → 有特殊身份看正常(=1),否则看审核专用(=2)
# 关闭 → 一律看正常公共商品(与身份无关)
use_normal = True
if self._is_zhi_kan_shenhe_enabled():
if self._is_zhi_kan_shenhe_enabled(club_id):
use_normal = has_special
data = self._get_public_goods(use_normal)
return Response({'code': 200, 'data': data})
def _is_zhi_kan_shenhe_enabled(self):
"""全局开关:未绑店用户是否按身份只看审核专用商品。默认开启以保持历史行为"""
config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
if not config:
return True
return bool(getattr(config, 'zhi_kan_shenhe', True))
def _is_zhi_kan_shenhe_enabled(self, club_id):
"""本俱乐部开关:未绑店用户是否按身份只看审核专用商品。默认开启。"""
from shop.services.shenhe_config import is_zhi_kan_shenhe_enabled
return is_zhi_kan_shenhe_enabled(club_id)
def _has_special_identity(self, user_main):
"""判断用户是否拥有打手/商家/管事/组长任一身份"""

View File

@@ -237,9 +237,10 @@ class ShopProductModifyView(APIView):
# 提取相对路径
tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
# 审核状态
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
shenhe_status = not (audit_config and audit_config.kaiqi_shenhe)
# 审核状态(按店铺所属俱乐部)
from shop.services.shenhe_config import is_kaiqi_shenhe_enabled
shop_club = getattr(dianpu, 'club_id', None) or 'xq'
shenhe_status = not is_kaiqi_shenhe_enabled(shop_club)
ShangpinLeixingDianpu.query.create(
dianpu_id=dianpu.id,
@@ -320,8 +321,9 @@ class ShopProductModifyView(APIView):
# 审核重置
if content_changed:
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
if audit_config and audit_config.kaiqi_shenhe:
from shop.services.shenhe_config import is_kaiqi_shenhe_enabled
shop_club = getattr(dianpu, 'club_id', None) or 'xq'
if is_kaiqi_shenhe_enabled(shop_club):
obj.shenhe_zhuangtai = False
update_fields.append('shenhe_zhuangtai')
@@ -872,8 +874,9 @@ class ShopProductModifyView1(APIView):
content_changed = True
if content_changed:
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
if audit_config and audit_config.kaiqi_shenhe:
from shop.services.shenhe_config import is_kaiqi_shenhe_enabled
shop_club = getattr(dianpu, 'club_id', None) or 'xq'
if is_kaiqi_shenhe_enabled(shop_club):
product.shenhe_zhuangtai = False
update_fields.append('shenhe_zhuangtai')
@@ -962,8 +965,9 @@ class ShopProductModifyView1(APIView):
return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})'''
shenhe_status = True
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
if audit_config and audit_config.kaiqi_shenhe:
from shop.services.shenhe_config import is_kaiqi_shenhe_enabled
shop_club = getattr(dianpu, 'club_id', None) or 'xq'
if is_kaiqi_shenhe_enabled(shop_club):
shenhe_status = False
product = Shangpin.query.create(