feat: 提现审核按俱乐部隔离并加固打款商户号,新增小溪审核目录复制脚本
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
387
jituan/management/commands/copy_lxs_shenhe_catalog_to_xx.py
Normal file
387
jituan/management/commands/copy_lxs_shenhe_catalog_to_xx.py
Normal file
@@ -0,0 +1,387 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
把龙先生(lxs)审核版商品目录复制到小溪(xx)。
|
||||
|
||||
范围(必须全部满足):
|
||||
- 源俱乐部固定 lxs(name 含「龙先生」)
|
||||
- 目标俱乐部固定 xx(name 含「小溪」或「小西」)
|
||||
- 仅全局 ShangpinLeixing.shenhezhuangtai=2 的类型
|
||||
- 仅公共目录 dianpu IS NULL
|
||||
- 专区/商品均要求 shenhezhuangtai=2
|
||||
|
||||
步骤:
|
||||
1) (可选)清空 xx 下上述审核类型的专区+商品(只删 DB,不删 OSS)
|
||||
2) 从 lxs 复制同类型专区+商品到 xx(图片复用相对路径;销量归零)
|
||||
3) 确保 xx 的 ClubShangpinLeixingConfig 启用这些审核类型(图标可复用源配置)
|
||||
|
||||
不碰:正常商品(shenhezhuangtai=1)、其它俱乐部、店铺商品、全局类型表、订单等。
|
||||
|
||||
用法:
|
||||
python manage.py copy_lxs_shenhe_catalog_to_xx --list
|
||||
python manage.py copy_lxs_shenhe_catalog_to_xx
|
||||
python manage.py copy_lxs_shenhe_catalog_to_xx --apply --i-confirm=lxs-to-xx
|
||||
python manage.py copy_lxs_shenhe_catalog_to_xx --apply --i-confirm=lxs-to-xx --clear-target
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
SOURCE_CLUB = 'lxs'
|
||||
SOURCE_NAME_HINT = '龙先生'
|
||||
TARGET_CLUB = 'xx'
|
||||
TARGET_NAME_HINTS = ('小溪', '小西')
|
||||
SHENHE = 2
|
||||
CONFIRM_TOKEN = 'lxs-to-xx'
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '复制龙先生审核专区/商品到小溪(xx)(默认 dry-run)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--apply', action='store_true', help='真正写库')
|
||||
parser.add_argument(
|
||||
'--i-confirm',
|
||||
type=str,
|
||||
default='',
|
||||
help=f'必须等于 {CONFIRM_TOKEN} 才允许 --apply',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--clear-target',
|
||||
action='store_true',
|
||||
help='写入前先清空 xx 下对应审核专区/商品(只删 DB)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--list',
|
||||
action='store_true',
|
||||
help='只列出审核类型及 lxs/xx 下专区商品数量',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from jituan.models import Club, ClubShangpinLeixingConfig
|
||||
from products.models import Shangpin, ShangpinLeixing, ShangpinZhuanqu
|
||||
|
||||
apply = bool(options.get('apply'))
|
||||
confirm = (options.get('i_confirm') or '').strip()
|
||||
clear_target = bool(options.get('clear_target'))
|
||||
|
||||
src_club = Club.query.filter(club_id=SOURCE_CLUB).first()
|
||||
if not src_club:
|
||||
raise CommandError(f'源俱乐部 {SOURCE_CLUB} 不存在')
|
||||
if SOURCE_NAME_HINT not in (src_club.name or ''):
|
||||
raise CommandError(
|
||||
f'安全拦截:{SOURCE_CLUB} 的 name={src_club.name!r} 不含「{SOURCE_NAME_HINT}」'
|
||||
)
|
||||
|
||||
dst_club = Club.query.filter(club_id=TARGET_CLUB).first()
|
||||
if not dst_club:
|
||||
raise CommandError(
|
||||
f'目标俱乐部 {TARGET_CLUB} 不存在。请先在后台创建小溪俱乐部(club_id=xx)。'
|
||||
)
|
||||
if not any(h in (dst_club.name or '') for h in TARGET_NAME_HINTS):
|
||||
raise CommandError(
|
||||
f'安全拦截:{TARGET_CLUB} 的 name={dst_club.name!r} '
|
||||
f'不含「{"/".join(TARGET_NAME_HINTS)}」'
|
||||
)
|
||||
|
||||
audit_types = list(
|
||||
ShangpinLeixing.query.filter(shenhezhuangtai=SHENHE).order_by('id')
|
||||
)
|
||||
if not audit_types:
|
||||
raise CommandError('没有任何审核专用商品类型(shenhezhuangtai=2)')
|
||||
if len(audit_types) != 3:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f'注意:当前审核类型数量={len(audit_types)}(预期常为 3),将按实际全部处理。'
|
||||
))
|
||||
|
||||
leixing_ids = [lx.id for lx in audit_types]
|
||||
|
||||
def counts(club_id):
|
||||
z = ShangpinZhuanqu.query.filter(
|
||||
club_id=club_id,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).count()
|
||||
p = Shangpin.query.filter(
|
||||
club_id=club_id,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).count()
|
||||
return z, p
|
||||
|
||||
def per_type(club_id):
|
||||
rows = []
|
||||
for lx in audit_types:
|
||||
z = ShangpinZhuanqu.query.filter(
|
||||
club_id=club_id,
|
||||
leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).count()
|
||||
p = Shangpin.query.filter(
|
||||
club_id=club_id,
|
||||
leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).count()
|
||||
rows.append((lx, z, p))
|
||||
return rows
|
||||
|
||||
if options.get('list'):
|
||||
self.stdout.write(self.style.NOTICE('审核类型及两侧存量:'))
|
||||
for lx in audit_types:
|
||||
sz, sp = (
|
||||
ShangpinZhuanqu.query.filter(
|
||||
club_id=SOURCE_CLUB, leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE, dianpu__isnull=True,
|
||||
).count(),
|
||||
Shangpin.query.filter(
|
||||
club_id=SOURCE_CLUB, leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE, dianpu__isnull=True,
|
||||
).count(),
|
||||
)
|
||||
dz, dp = (
|
||||
ShangpinZhuanqu.query.filter(
|
||||
club_id=TARGET_CLUB, leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE, dianpu__isnull=True,
|
||||
).count(),
|
||||
Shangpin.query.filter(
|
||||
club_id=TARGET_CLUB, leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE, dianpu__isnull=True,
|
||||
).count(),
|
||||
)
|
||||
self.stdout.write(
|
||||
f' id={lx.id}「{lx.jieshao}」 '
|
||||
f'lxs: zones={sz} products={sp} | '
|
||||
f'xx: zones={dz} products={dp}'
|
||||
)
|
||||
return
|
||||
|
||||
if apply and confirm != CONFIRM_TOKEN:
|
||||
raise CommandError(
|
||||
f'安全拦截:--apply 必须带 --i-confirm={CONFIRM_TOKEN}\n'
|
||||
f' python manage.py copy_lxs_shenhe_catalog_to_xx '
|
||||
f'--apply --i-confirm={CONFIRM_TOKEN}'
|
||||
)
|
||||
|
||||
src_z_total, src_p_total = counts(SOURCE_CLUB)
|
||||
dst_z_total, dst_p_total = counts(TARGET_CLUB)
|
||||
|
||||
src_zones = list(
|
||||
ShangpinZhuanqu.query.filter(
|
||||
club_id=SOURCE_CLUB,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).order_by('leixing_id', 'paixu', 'id')
|
||||
)
|
||||
src_products = list(
|
||||
Shangpin.query.filter(
|
||||
club_id=SOURCE_CLUB,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).order_by('leixing_id', 'paixu', 'id')
|
||||
)
|
||||
|
||||
src_zone_ids = {z.id for z in src_zones}
|
||||
orphan = [p for p in src_products if p.zhuanqu_id not in src_zone_ids]
|
||||
if orphan:
|
||||
sample = ', '.join(f'{p.id}:{p.biaoqian}' for p in orphan[:5])
|
||||
raise CommandError(
|
||||
f'源商品有 {len(orphan)} 条专区不在本批审核专区内,已中止。样例: {sample}'
|
||||
)
|
||||
|
||||
if src_z_total == 0 and src_p_total == 0:
|
||||
raise CommandError('龙先生(lxs)下没有可复制的审核专区/商品')
|
||||
|
||||
mode = 'APPLY' if apply else 'DRY-RUN'
|
||||
self.stdout.write(self.style.NOTICE(
|
||||
f'\n===== {mode} 小溪审核目录 ← 龙先生 =====\n'
|
||||
f'源: {SOURCE_CLUB} ({src_club.name}) '
|
||||
f'专区={src_z_total} 商品={src_p_total}\n'
|
||||
f'目标: {TARGET_CLUB} ({dst_club.name}) '
|
||||
f'现有 专区={dst_z_total} 商品={dst_p_total}\n'
|
||||
f'clear_target={clear_target}\n'
|
||||
f'审核类型数={len(audit_types)} ids={leixing_ids}\n'
|
||||
f'OSS: 不删文件;复制时复用龙先生相对路径(同一 COS 域名拼接)\n'
|
||||
))
|
||||
|
||||
self.stdout.write('—— 分类型存量 ——')
|
||||
for lx, sz, sp in per_type(SOURCE_CLUB):
|
||||
self.stdout.write(f' [lxs] id={lx.id}「{lx.jieshao}」 zones={sz} products={sp}')
|
||||
for lx, dz, dp in per_type(TARGET_CLUB):
|
||||
self.stdout.write(f' [xx] id={lx.id}「{lx.jieshao}」 zones={dz} products={dp}')
|
||||
|
||||
self.stdout.write('—— 将从 lxs 复制的专区样例 ——')
|
||||
for z in src_zones[:40]:
|
||||
self.stdout.write(
|
||||
f' zq#{z.id} leixing={z.leixing_id} {z.mingzi!r}'
|
||||
)
|
||||
if len(src_zones) > 40:
|
||||
self.stdout.write(f' ... 其余 {len(src_zones) - 40} 个省略')
|
||||
|
||||
self.stdout.write('—— 将从 lxs 复制的商品样例 ——')
|
||||
for p in src_products[:15]:
|
||||
self.stdout.write(
|
||||
f' sp#{p.id} leixing={p.leixing_id} {p.biaoqian!r} ¥{p.jiage}'
|
||||
)
|
||||
if len(src_products) > 15:
|
||||
self.stdout.write(f' ... 其余 {len(src_products) - 15} 个省略')
|
||||
|
||||
if not apply:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'\n以上仅计划:未删库、未写库。确认无误后:\n'
|
||||
f' python manage.py copy_lxs_shenhe_catalog_to_xx '
|
||||
f'--apply --i-confirm={CONFIRM_TOKEN}\n'
|
||||
f'若目标已有旧审核目录需先清空,再加 --clear-target\n'
|
||||
))
|
||||
return
|
||||
|
||||
stats = {
|
||||
'deleted_products': 0,
|
||||
'deleted_zones': 0,
|
||||
'zones_created': 0,
|
||||
'products_created': 0,
|
||||
'cfg_created': 0,
|
||||
'cfg_enabled': 0,
|
||||
}
|
||||
|
||||
with transaction.atomic():
|
||||
if clear_target:
|
||||
n_prod, _ = Shangpin.query.filter(
|
||||
club_id=TARGET_CLUB,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).delete()
|
||||
n_zone, _ = ShangpinZhuanqu.query.filter(
|
||||
club_id=TARGET_CLUB,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).delete()
|
||||
stats['deleted_products'] = n_prod
|
||||
stats['deleted_zones'] = n_zone
|
||||
|
||||
zone_id_map = {}
|
||||
for z in src_zones:
|
||||
if z.leixing_id not in leixing_ids:
|
||||
raise CommandError(f'专区 {z.id} 类型 {z.leixing_id} 不在审核列表')
|
||||
# 未 clear 时若同名专区已存在则复用,避免重复堆叠
|
||||
exist = None
|
||||
if not clear_target:
|
||||
exist = ShangpinZhuanqu.query.filter(
|
||||
club_id=TARGET_CLUB,
|
||||
leixing_id=z.leixing_id,
|
||||
mingzi=z.mingzi,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).first()
|
||||
if exist:
|
||||
zone_id_map[z.id] = exist.id
|
||||
continue
|
||||
row = ShangpinZhuanqu.query.create(
|
||||
mingzi=z.mingzi,
|
||||
leixing_id=z.leixing_id,
|
||||
club_id=TARGET_CLUB,
|
||||
tupian_url=z.tupian_url or '',
|
||||
shenhezhuangtai=SHENHE,
|
||||
paixu=int(z.paixu or 0),
|
||||
shangjia_zhuangtai=bool(getattr(z, 'shangjia_zhuangtai', True)),
|
||||
fengjin_zhuangtai=bool(getattr(z, 'fengjin_zhuangtai', True)),
|
||||
dianpu=None,
|
||||
dianpu_leixing=None,
|
||||
)
|
||||
zone_id_map[z.id] = row.id
|
||||
stats['zones_created'] += 1
|
||||
|
||||
for p in src_products:
|
||||
if p.leixing_id not in leixing_ids:
|
||||
raise CommandError(f'商品 {p.id} 类型 {p.leixing_id} 不在审核列表')
|
||||
dst_zq = zone_id_map.get(p.zhuanqu_id)
|
||||
if not dst_zq:
|
||||
raise CommandError(
|
||||
f'商品「{p.biaoqian}」id={p.id} 无法映射 zhuanqu_id={p.zhuanqu_id}'
|
||||
)
|
||||
if not clear_target:
|
||||
exist_p = Shangpin.query.filter(
|
||||
club_id=TARGET_CLUB,
|
||||
leixing_id=p.leixing_id,
|
||||
zhuanqu_id=dst_zq,
|
||||
biaoqian=p.biaoqian,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).first()
|
||||
if exist_p:
|
||||
continue
|
||||
Shangpin.query.create(
|
||||
biaoqian=p.biaoqian,
|
||||
jiage=p.jiage,
|
||||
kucun=p.kucun,
|
||||
leixing_id=p.leixing_id,
|
||||
zhuanqu_id=dst_zq,
|
||||
zhenshi_xiaoliang=0,
|
||||
duiwai_xiaoliang=0,
|
||||
jieshao=p.jieshao,
|
||||
xiadan_xuzhi=p.xiadan_xuzhi,
|
||||
guize_tupian=p.guize_tupian or '',
|
||||
tupian_url=p.tupian_url or '',
|
||||
yaoqiuleixing=p.yaoqiuleixing,
|
||||
huiyuan_id=p.huiyuan_id,
|
||||
yongjin=p.yongjin,
|
||||
kaioi_ewai_dashou_fencheng=bool(p.kaioi_ewai_dashou_fencheng),
|
||||
ewai_dashou_fencheng=p.ewai_dashou_fencheng,
|
||||
paixu=int(p.paixu or 0),
|
||||
shenhezhuangtai=SHENHE,
|
||||
shangjia_zhuangtai=bool(p.shangjia_zhuangtai),
|
||||
fengjin_zhuangtai=bool(p.fengjin_zhuangtai),
|
||||
shenhe_zhuangtai=bool(getattr(p, 'shenhe_zhuangtai', True)),
|
||||
club_id=TARGET_CLUB,
|
||||
shi_zhuanpan=False,
|
||||
dianpu=None,
|
||||
dianpu_leixing=None,
|
||||
)
|
||||
stats['products_created'] += 1
|
||||
|
||||
# 启用审核类型上架配置
|
||||
for lx in audit_types:
|
||||
src_cfg = ClubShangpinLeixingConfig.query.filter(
|
||||
club_id=SOURCE_CLUB, leixing_id=lx.id,
|
||||
).first()
|
||||
dst_cfg = ClubShangpinLeixingConfig.query.filter(
|
||||
club_id=TARGET_CLUB, leixing_id=lx.id,
|
||||
).first()
|
||||
icon = ''
|
||||
paixu = int(getattr(lx, 'paixu', 0) or 0)
|
||||
if src_cfg:
|
||||
icon = src_cfg.tupian_url or ''
|
||||
paixu = int(src_cfg.paixu or paixu)
|
||||
if dst_cfg is None:
|
||||
ClubShangpinLeixingConfig.query.create(
|
||||
club_id=TARGET_CLUB,
|
||||
leixing_id=lx.id,
|
||||
is_enabled=True,
|
||||
tupian_url=icon,
|
||||
paixu=paixu,
|
||||
)
|
||||
stats['cfg_created'] += 1
|
||||
else:
|
||||
changed = False
|
||||
if not dst_cfg.is_enabled:
|
||||
dst_cfg.is_enabled = True
|
||||
changed = True
|
||||
if icon and not (dst_cfg.tupian_url or '').strip():
|
||||
dst_cfg.tupian_url = icon
|
||||
changed = True
|
||||
if changed:
|
||||
dst_cfg.save()
|
||||
stats['cfg_enabled'] += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\n完成: {stats}\n'
|
||||
f'龙先生(lxs)未改动;小溪(xx)已具备审核目录。\n'
|
||||
f'请用小溪小程序(CLUB_ID=xx)重新编译验证。'
|
||||
))
|
||||
153
jituan/management/commands/seed_club_withdraw_audit_perm.py
Normal file
153
jituan/management/commands/seed_club_withdraw_audit_perm.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""确保提现审核权限码 005bb,并按俱乐部挂到指定角色(可重复执行)。
|
||||
|
||||
权限码全局共用;隔离靠 Role.ClubID + 请求头俱乐部。
|
||||
集团高管仍可跨俱乐部;俱乐部管理员仅本俱乐部。
|
||||
|
||||
用法示例:
|
||||
python manage.py seed_club_withdraw_audit_perm
|
||||
python manage.py seed_club_withdraw_audit_perm --club-id lxs --role-name 管理员
|
||||
python manage.py seed_club_withdraw_audit_perm --all-clubs --role-name 管理员
|
||||
python manage.py seed_club_withdraw_audit_perm --all-clubs --role-name 管理员 --dry-run
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from gvsdsdk.models import Permission, Role, RolePermission
|
||||
|
||||
from jituan.models import Club
|
||||
|
||||
WITHDRAW_AUDIT_PERM = '005bb'
|
||||
WITHDRAW_AUDIT_NAME = '提现审核(按俱乐部)'
|
||||
WITHDRAW_AUDIT_DESC = (
|
||||
'审核本俱乐部提现申请(同意/拒绝/自动打款待收款)。'
|
||||
'跨俱乐部仅集团高管可操作;须配合后台角色 ClubID 与数据范围任职。'
|
||||
)
|
||||
WITHDRAW_AUDIT_CATEGORY = '提现管理'
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '写入 005bb 提现审核权限码,并按俱乐部挂到角色'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--club-id', default='', help='仅处理该俱乐部角色(如 lxs / xq)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--all-clubs', action='store_true', help='处理全部启用中的俱乐部',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--role-name', default='',
|
||||
help='角色名(如「管理员」);为空则只确保权限码存在,不挂角色',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run', action='store_true', help='只打印不写入',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = options['dry_run']
|
||||
club_id = (options.get('club_id') or '').strip()
|
||||
all_clubs = options.get('all_clubs')
|
||||
role_name = (options.get('role_name') or '').strip()
|
||||
|
||||
sample = Permission.objects.filter(PermStatus=1).first()
|
||||
tenant_uuid = sample.TenantUUID if sample else uuid.UUID(
|
||||
'00000000-0000-0000-0000-000000000001'
|
||||
).bytes
|
||||
|
||||
perm = Permission.objects.filter(PermCode=WITHDRAW_AUDIT_PERM).first()
|
||||
if perm:
|
||||
self.stdout.write(f' 权限码已存在: {WITHDRAW_AUDIT_PERM}')
|
||||
if not dry_run and (perm.PermName != WITHDRAW_AUDIT_NAME or not perm.PermDesc):
|
||||
perm.PermName = WITHDRAW_AUDIT_NAME
|
||||
perm.PermDesc = WITHDRAW_AUDIT_DESC
|
||||
perm.PermCategory = WITHDRAW_AUDIT_CATEGORY
|
||||
perm.save(update_fields=['PermName', 'PermDesc', 'PermCategory'])
|
||||
self.stdout.write(self.style.SUCCESS(' 已更新 005bb 名称/说明'))
|
||||
else:
|
||||
if dry_run:
|
||||
self.stdout.write(f'would create perm {WITHDRAW_AUDIT_PERM}')
|
||||
else:
|
||||
perm = Permission.objects.create(
|
||||
PermUUID=uuid.uuid4().bytes,
|
||||
TenantUUID=tenant_uuid,
|
||||
PermCode=WITHDRAW_AUDIT_PERM,
|
||||
PermName=WITHDRAW_AUDIT_NAME,
|
||||
PermDesc=WITHDRAW_AUDIT_DESC,
|
||||
PermCategory=WITHDRAW_AUDIT_CATEGORY,
|
||||
PermStatus=1,
|
||||
CreateTime=timezone.now(),
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f' 新增权限: {WITHDRAW_AUDIT_PERM}'))
|
||||
|
||||
if not role_name:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
'未指定 --role-name:仅确保权限码。'
|
||||
'给俱乐部挂权请加:--club-id lxs --role-name 管理员'
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not club_id and not all_clubs:
|
||||
raise CommandError('挂角色时请指定 --club-id 或 --all-clubs')
|
||||
|
||||
if all_clubs:
|
||||
club_ids = list(
|
||||
Club.query.filter(status=1).values_list('club_id', flat=True)
|
||||
)
|
||||
else:
|
||||
if not Club.query.filter(club_id=club_id, status=1).exists():
|
||||
raise CommandError(f'俱乐部不存在或未启用: {club_id}')
|
||||
club_ids = [club_id]
|
||||
|
||||
if not perm and dry_run:
|
||||
self.stdout.write('dry-run: skip role binding (perm not loaded)')
|
||||
return
|
||||
|
||||
linked = 0
|
||||
missing_roles = 0
|
||||
with transaction.atomic():
|
||||
for cid in club_ids:
|
||||
role = Role.objects.filter(
|
||||
RoleName=role_name, ClubID=cid, RoleStatus=1,
|
||||
).first()
|
||||
if not role:
|
||||
missing_roles += 1
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f' [{cid}] 无角色「{role_name}」,跳过')
|
||||
)
|
||||
continue
|
||||
exists = RolePermission.objects.filter(
|
||||
RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID,
|
||||
).exists()
|
||||
if exists:
|
||||
self.stdout.write(f' [{cid}] 角色「{role_name}」已有 {WITHDRAW_AUDIT_PERM}')
|
||||
continue
|
||||
if dry_run:
|
||||
self.stdout.write(
|
||||
f'would link {WITHDRAW_AUDIT_PERM} -> [{cid}] {role_name}'
|
||||
)
|
||||
linked += 1
|
||||
continue
|
||||
RolePermission.objects.create(
|
||||
RolePermUUID=uuid.uuid4().bytes,
|
||||
RoleUUID=role.RoleUUID,
|
||||
PermUUID=perm.PermUUID,
|
||||
CreateTime=timezone.now(),
|
||||
)
|
||||
linked += 1
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f' [{cid}] 已给角色「{role_name}」挂上 {WITHDRAW_AUDIT_PERM}'
|
||||
)
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'完成:挂权 {linked},缺角色 {missing_roles}。'
|
||||
f'另请确认该账号在「数据范围配置」有对应俱乐部 SINGLE_CLUB 任职。'
|
||||
)
|
||||
)
|
||||
@@ -44,6 +44,12 @@ EXTRA_PERMISSIONS = [
|
||||
'管理小程序身份装饰标签(与用户管理-身份装饰标签菜单对应)',
|
||||
'用户管理',
|
||||
),
|
||||
(
|
||||
'005bb',
|
||||
'提现审核(按俱乐部)',
|
||||
'审核本俱乐部提现(同意/拒绝/自动打款)。跨俱乐部仅集团高管;须角色 ClubID + 数据范围任职。',
|
||||
'提现管理',
|
||||
),
|
||||
# ---------- 店铺管理(999 系列:列表/详情/复制/提现等) ----------
|
||||
('999a', '店铺状态管理', '修改店铺封禁/正常状态', '店铺管理'),
|
||||
('999b', '店铺余额与创建', '修改可提现余额、添加新店铺', '店铺管理'),
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
"""集团级资金处置权限:提现审核、加余额等。俱乐部 000001 超级管理不能绕过。"""
|
||||
"""集团级资金处置权限:加余额等仍仅集团高管。
|
||||
|
||||
提现审核:权限码 005bb + 按俱乐部隔离——
|
||||
- 子公司视图:持有 005bb 且任职含当前俱乐部 → 可审本俱乐部(同意/拒绝/自动打款)
|
||||
- 集团汇总视图 / 跨俱乐部:须集团资金处置权(GROUP_OWNER / GROUP_SUPER_ADMIN)
|
||||
俱乐部本地 000001 不能绕过「跨店」闸门,但可审本店提现。
|
||||
"""
|
||||
from django.db.models import Q
|
||||
|
||||
from jituan.constants import DATA_SCOPE_ALL
|
||||
from jituan.models import AdminAssignment
|
||||
from jituan.services.admin_context import GROUP_MANAGE_ROLES, build_admin_club_context, is_system_super_admin
|
||||
from jituan.services.admin_context import (
|
||||
GROUP_MANAGE_ROLES,
|
||||
build_admin_club_context,
|
||||
get_allowed_club_ids,
|
||||
is_system_super_admin,
|
||||
)
|
||||
|
||||
MSG_GROUP_FINANCE_REQUIRED = '该操作仅集团高管可执行(需在数据范围配置中为集团级任职)'
|
||||
MSG_WITHDRAW_CROSS_CLUB = '跨俱乐部提现审核仅集团高管可执行,请切换到具体俱乐部后再操作'
|
||||
MSG_WITHDRAW_CLUB_DENIED = '无权审核该俱乐部提现'
|
||||
|
||||
|
||||
def has_group_finance_exec(user, request=None):
|
||||
@@ -35,16 +48,32 @@ def deny_group_finance_exec(user, request=None):
|
||||
|
||||
|
||||
def require_withdraw_audit_access(request, phone):
|
||||
"""提现审核:须 005bb 且集团资金处置权。"""
|
||||
"""
|
||||
提现审核访问:
|
||||
1) 须权限码 005bb
|
||||
2) 集团高管:可在 ALL_CLUBS 或任意子公司视图操作
|
||||
3) 俱乐部管理员:仅 SINGLE_CLUB 且当前 club 在其任职范围内
|
||||
"""
|
||||
from backend.utils import verify_kefu_permission
|
||||
from rest_framework.response import Response
|
||||
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
|
||||
|
||||
kefu, permissions = verify_kefu_permission(request, phone)
|
||||
if kefu is None:
|
||||
return None, permissions
|
||||
if '005bb' not in permissions:
|
||||
return None, Response({'code': 403, 'msg': '您无权操作提现审核'})
|
||||
deny = deny_group_finance_exec(request.user, request)
|
||||
if deny:
|
||||
return None, deny
|
||||
|
||||
if has_group_finance_exec(request.user, request):
|
||||
return kefu, None
|
||||
|
||||
scope = resolve_club_scope(request)
|
||||
if scope == DATA_SCOPE_ALL:
|
||||
return None, Response({'code': 403, 'msg': MSG_WITHDRAW_CROSS_CLUB})
|
||||
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
allowed = get_allowed_club_ids(request.user)
|
||||
if not club_id or club_id not in allowed:
|
||||
return None, Response({'code': 403, 'msg': MSG_WITHDRAW_CLUB_DENIED})
|
||||
|
||||
return kefu, None
|
||||
|
||||
@@ -15,6 +15,24 @@ def club_id_for_tixian_yonghuid_safe(yonghuid):
|
||||
return club_id_for_tixian_yonghuid(yonghuid)
|
||||
|
||||
|
||||
def resolve_payout_club_id(*, audit=None, jilu=None, yonghuid=None, user=None):
|
||||
"""
|
||||
打款商户归属俱乐部:优先提现单/审核单上的 club_id,再回落用户归属。
|
||||
避免用户迁店后用新俱乐部商户号打旧单资金。
|
||||
"""
|
||||
for obj in (audit, jilu):
|
||||
if obj is None:
|
||||
continue
|
||||
cid = (getattr(obj, 'club_id', None) or '').strip()
|
||||
if cid:
|
||||
return cid
|
||||
if user is not None:
|
||||
return get_user_club_id(user) or ''
|
||||
if yonghuid:
|
||||
return club_id_for_tixian_yonghuid(yonghuid) or ''
|
||||
return ''
|
||||
|
||||
|
||||
def filter_tixianjilu_by_request(qs, request):
|
||||
return filter_queryset_by_club(qs, request, club_field='club_id')
|
||||
|
||||
@@ -22,6 +40,12 @@ def filter_tixianjilu_by_request(qs, request):
|
||||
def forbid_if_tixian_out_of_scope(request, tixian_record):
|
||||
"""提现记录不在当前俱乐部范围时返回 Response。"""
|
||||
if resolve_club_scope(request) == DATA_SCOPE_ALL:
|
||||
from jituan.services.group_finance_gate import has_group_finance_exec
|
||||
if not has_group_finance_exec(request.user, request):
|
||||
return Response({
|
||||
'code': 403,
|
||||
'msg': '跨俱乐部提现操作仅集团高管可执行,请切换到具体俱乐部',
|
||||
})
|
||||
return None
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
rec_club = getattr(tixian_record, 'club_id', None) or club_id_for_tixian_yonghuid_safe(
|
||||
|
||||
@@ -396,21 +396,27 @@ def get_wechat_v3_config(club_id=None):
|
||||
|
||||
# 共用商户的俱乐部:默认用 settings 私钥;仅当库中路径存在且文件在盘上才覆盖
|
||||
# (避免旧库里曾填过、但原先被忽略的无效路径突然接管提现)
|
||||
private_key_path = path_defaults['PRIVATE_KEY_PATH']
|
||||
if cid not in SHARED_WX_MERCHANT_CLUBS:
|
||||
private_key_path = (
|
||||
_pick_club_field(club, cfg_json, 'private_key_path')
|
||||
or private_key_path
|
||||
# 非共享俱乐部:严禁回落星阙私钥/商户,缺配置直接空值,由上层拒绝打款
|
||||
if cid in SHARED_WX_MERCHANT_CLUBS:
|
||||
private_key_path = path_defaults['PRIVATE_KEY_PATH']
|
||||
if club:
|
||||
uploaded_key = (getattr(club, 'private_key_path', None) or '').strip()
|
||||
if uploaded_key and os.path.isfile(uploaded_key):
|
||||
private_key_path = uploaded_key
|
||||
platform_cert_dir = (
|
||||
_pick_club_field(club, cfg_json, 'platform_cert_dir')
|
||||
or path_defaults['PLATFORM_CERT_DIR']
|
||||
)
|
||||
elif club:
|
||||
uploaded_key = (getattr(club, 'private_key_path', None) or '').strip()
|
||||
if uploaded_key and os.path.isfile(uploaded_key):
|
||||
private_key_path = uploaded_key
|
||||
else:
|
||||
private_key_path = _pick_club_field(club, cfg_json, 'private_key_path')
|
||||
platform_cert_dir = _pick_club_field(club, cfg_json, 'platform_cert_dir')
|
||||
if not mch_id or not api_v3_key or not private_key_path:
|
||||
logger.error(
|
||||
'[WX_V3_CONFIG] club=%s 提现商户配置不完整(mch=%s key=%s pem=%s),'
|
||||
'禁止回落星阙商户,避免串号打款',
|
||||
cid, bool(mch_id), bool(api_v3_key), bool(private_key_path),
|
||||
)
|
||||
|
||||
platform_cert_dir = (
|
||||
_pick_club_field(club, cfg_json, 'platform_cert_dir')
|
||||
or path_defaults['PLATFORM_CERT_DIR']
|
||||
)
|
||||
transfer_scene_id = (
|
||||
_pick_club_field(club, cfg_json, 'transfer_scene_id', 'TRANSFER_SCENE_ID')
|
||||
or path_defaults['TRANSFER_SCENE_ID']
|
||||
|
||||
Reference in New Issue
Block a user