feat: 提现审核按俱乐部隔离并加固打款商户号,新增小溪审核目录复制脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-26 01:20:42 +08:00
parent 332c42c85d
commit 280d8742c9
10 changed files with 661 additions and 38 deletions

View File

@@ -0,0 +1,387 @@
# -*- coding: utf-8 -*-
"""
把龙先生(lxs)审核版商品目录复制到小溪(xx)。
范围(必须全部满足):
- 源俱乐部固定 lxsname 含「龙先生」)
- 目标俱乐部固定 xxname 含「小溪」或「小西」)
- 仅全局 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)重新编译验证。'
))

View 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 任职。'
)
)

View File

@@ -44,6 +44,12 @@ EXTRA_PERMISSIONS = [
'管理小程序身份装饰标签(与用户管理-身份装饰标签菜单对应)', '管理小程序身份装饰标签(与用户管理-身份装饰标签菜单对应)',
'用户管理', '用户管理',
), ),
(
'005bb',
'提现审核(按俱乐部)',
'审核本俱乐部提现(同意/拒绝/自动打款)。跨俱乐部仅集团高管;须角色 ClubID + 数据范围任职。',
'提现管理',
),
# ---------- 店铺管理999 系列:列表/详情/复制/提现等) ---------- # ---------- 店铺管理999 系列:列表/详情/复制/提现等) ----------
('999a', '店铺状态管理', '修改店铺封禁/正常状态', '店铺管理'), ('999a', '店铺状态管理', '修改店铺封禁/正常状态', '店铺管理'),
('999b', '店铺余额与创建', '修改可提现余额、添加新店铺', '店铺管理'), ('999b', '店铺余额与创建', '修改可提现余额、添加新店铺', '店铺管理'),

View File

@@ -1,11 +1,24 @@
"""集团级资金处置权限:提现审核、加余额等。俱乐部 000001 超级管理不能绕过。""" """集团级资金处置权限:加余额等仍仅集团高管。
提现审核:权限码 005bb + 按俱乐部隔离——
- 子公司视图:持有 005bb 且任职含当前俱乐部 → 可审本俱乐部(同意/拒绝/自动打款)
- 集团汇总视图 / 跨俱乐部须集团资金处置权GROUP_OWNER / GROUP_SUPER_ADMIN
俱乐部本地 000001 不能绕过「跨店」闸门,但可审本店提现。
"""
from django.db.models import Q from django.db.models import Q
from jituan.constants import DATA_SCOPE_ALL from jituan.constants import DATA_SCOPE_ALL
from jituan.models import AdminAssignment 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_GROUP_FINANCE_REQUIRED = '该操作仅集团高管可执行(需在数据范围配置中为集团级任职)'
MSG_WITHDRAW_CROSS_CLUB = '跨俱乐部提现审核仅集团高管可执行,请切换到具体俱乐部后再操作'
MSG_WITHDRAW_CLUB_DENIED = '无权审核该俱乐部提现'
def has_group_finance_exec(user, request=None): 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): 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 backend.utils import verify_kefu_permission
from rest_framework.response import Response 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) kefu, permissions = verify_kefu_permission(request, phone)
if kefu is None: if kefu is None:
return None, permissions return None, permissions
if '005bb' not in permissions: if '005bb' not in permissions:
return None, Response({'code': 403, 'msg': '您无权操作提现审核'}) return None, Response({'code': 403, 'msg': '您无权操作提现审核'})
deny = deny_group_finance_exec(request.user, request)
if deny: if has_group_finance_exec(request.user, request):
return None, deny 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 return kefu, None

View File

@@ -15,6 +15,24 @@ def club_id_for_tixian_yonghuid_safe(yonghuid):
return club_id_for_tixian_yonghuid(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): def filter_tixianjilu_by_request(qs, request):
return filter_queryset_by_club(qs, request, club_field='club_id') 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): def forbid_if_tixian_out_of_scope(request, tixian_record):
"""提现记录不在当前俱乐部范围时返回 Response。""" """提现记录不在当前俱乐部范围时返回 Response。"""
if resolve_club_scope(request) == DATA_SCOPE_ALL: 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 return None
club_id = resolve_club_id_from_request(request) club_id = resolve_club_id_from_request(request)
rec_club = getattr(tixian_record, 'club_id', None) or club_id_for_tixian_yonghuid_safe( rec_club = getattr(tixian_record, 'club_id', None) or club_id_for_tixian_yonghuid_safe(

View File

@@ -396,21 +396,27 @@ def get_wechat_v3_config(club_id=None):
# 共用商户的俱乐部:默认用 settings 私钥;仅当库中路径存在且文件在盘上才覆盖 # 共用商户的俱乐部:默认用 settings 私钥;仅当库中路径存在且文件在盘上才覆盖
# (避免旧库里曾填过、但原先被忽略的无效路径突然接管提现) # (避免旧库里曾填过、但原先被忽略的无效路径突然接管提现)
# 非共享俱乐部:严禁回落星阙私钥/商户,缺配置直接空值,由上层拒绝打款
if cid in SHARED_WX_MERCHANT_CLUBS:
private_key_path = path_defaults['PRIVATE_KEY_PATH'] private_key_path = path_defaults['PRIVATE_KEY_PATH']
if cid not in SHARED_WX_MERCHANT_CLUBS: if club:
private_key_path = (
_pick_club_field(club, cfg_json, 'private_key_path')
or private_key_path
)
elif club:
uploaded_key = (getattr(club, 'private_key_path', None) or '').strip() uploaded_key = (getattr(club, 'private_key_path', None) or '').strip()
if uploaded_key and os.path.isfile(uploaded_key): if uploaded_key and os.path.isfile(uploaded_key):
private_key_path = uploaded_key private_key_path = uploaded_key
platform_cert_dir = ( platform_cert_dir = (
_pick_club_field(club, cfg_json, 'platform_cert_dir') _pick_club_field(club, cfg_json, 'platform_cert_dir')
or path_defaults['PLATFORM_CERT_DIR'] or path_defaults['PLATFORM_CERT_DIR']
) )
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),
)
transfer_scene_id = ( transfer_scene_id = (
_pick_club_field(club, cfg_json, 'transfer_scene_id', 'TRANSFER_SCENE_ID') _pick_club_field(club, cfg_json, 'transfer_scene_id', 'TRANSFER_SCENE_ID')
or path_defaults['TRANSFER_SCENE_ID'] or path_defaults['TRANSFER_SCENE_ID']

View File

@@ -1105,7 +1105,7 @@ def query_wechat_transfer_bill(out_bill_no, club_id=None):
try: try:
rec = TixianAutoRecord.query.filter(tixian_id=out_bill_no).first() rec = TixianAutoRecord.query.filter(tixian_id=out_bill_no).first()
if rec: if rec:
club_id = club_id_for_tixian_yonghuid(rec.yonghuid) club_id = (getattr(rec, 'club_id', None) or '').strip() or club_id_for_tixian_yonghuid(rec.yonghuid)
except Exception: except Exception:
pass pass
@@ -1152,8 +1152,8 @@ def _parse_package_info(raw):
def _apply_platform_accounting(auto_record): def _apply_platform_accounting(auto_record):
"""平台收支统计(幂等由 mark_transfer_success 外层保证只调一次)""" """平台收支统计(幂等由 mark_transfer_success 外层保证只调一次)"""
from jituan.services.szjilu_accounting import apply_szjilu_expense from jituan.services.szjilu_accounting import apply_szjilu_expense
from jituan.services.wechat_pay import club_id_for_tixian_yonghuid from jituan.services.tixian_club import resolve_payout_club_id
payout_club = club_id_for_tixian_yonghuid(auto_record.yonghuid) payout_club = resolve_payout_club_id(audit=None, jilu=auto_record, yonghuid=auto_record.yonghuid)
update_daily_payout(auto_record.shijidaozhang, payout_club) update_daily_payout(auto_record.shijidaozhang, payout_club)
apply_szjilu_expense(auto_record.shijidaozhang, payout_club) apply_szjilu_expense(auto_record.shijidaozhang, payout_club)

View File

@@ -202,17 +202,12 @@ def process_audit_collect(request):
try: try:
from users.tixian_shenhe_services import get_auto_withdraw_openid from users.tixian_shenhe_services import get_auto_withdraw_openid
from jituan.services.club_user import get_user_club_id
from jituan.services.wechat_pay import get_wechat_v3_config from jituan.services.wechat_pay import get_wechat_v3_config
from jituan.services.tixian_club import resolve_payout_club_id
payout_club_id = get_user_club_id(user_main)
payout_openid = get_auto_withdraw_openid(user_main) payout_openid = get_auto_withdraw_openid(user_main)
if not payout_openid: if not payout_openid:
return Response({'code': 10, 'msg': '用户未绑定微信,无法收款'}) return Response({'code': 10, 'msg': '用户未绑定微信,无法收款'})
wx_cfg = get_wechat_v3_config(payout_club_id)
if not wx_cfg.get('APPID') or not wx_cfg.get('MCHID') or not wx_cfg.get('TRANSFER_SCENE_ID'):
logger.error('微信打款配置不完整 club=%s APPID=%s MCHID=%s', payout_club_id, wx_cfg.get('APPID'), wx_cfg.get('MCHID'))
return Response({'code': 11, 'msg': '系统配置异常,请联系客服'})
ctx, err = resolve_collect_context( ctx, err = resolve_collect_context(
yonghuid, yonghuid,
@@ -228,6 +223,18 @@ def process_audit_collect(request):
tixianjilu_id_val = ctx['jilu'].id if ctx.get('jilu') else audit.tixianjilu_id tixianjilu_id_val = ctx['jilu'].id if ctx.get('jilu') else audit.tixianjilu_id
leixing = audit.leixing leixing = audit.leixing
# 打款商户严格跟提现单 club_id禁止因用户迁店串到其他俱乐部商户号
payout_club_id = resolve_payout_club_id(
audit=audit, jilu=ctx.get('jilu'), user=user_main,
)
wx_cfg = get_wechat_v3_config(payout_club_id)
if not wx_cfg.get('APPID') or not wx_cfg.get('MCHID') or not wx_cfg.get('TRANSFER_SCENE_ID'):
logger.error(
'微信打款配置不完整 club=%s APPID=%s MCHID=%s audit=%s',
payout_club_id, wx_cfg.get('APPID'), wx_cfg.get('MCHID'), getattr(audit, 'id', None),
)
return Response({'code': 11, 'msg': '系统配置异常,请联系客服'})
amount_ok, amount_msg = validate_collect_amount(audit) amount_ok, amount_msg = validate_collect_amount(audit)
if not amount_ok: if not amount_ok:
with transaction.atomic(): with transaction.atomic():
@@ -303,10 +310,9 @@ def process_audit_collect(request):
quota_reserved = True quota_reserved = True
tixian_id = generate_tixian_id() tixian_id = generate_tixian_id()
from jituan.services.club_user import get_user_club_id
TixianAutoRecord.query.create( TixianAutoRecord.query.create(
tixian_id=tixian_id, tixian_id=tixian_id,
club_id=get_user_club_id(user_main), club_id=payout_club_id,
yonghuid=yonghuid, yonghuid=yonghuid,
leixing=leixing, leixing=leixing,
jine=jine, jine=jine,

View File

@@ -2285,6 +2285,8 @@ class AddtxshView(APIView):
# 🔴【修改】排序改为按申请时间升序(最早申请排最前) # 🔴【修改】排序改为按申请时间升序(最早申请排最前)
queryset = Tixianjilu.query.filter(query).order_by('CreateTime') queryset = Tixianjilu.query.filter(query).order_by('CreateTime')
from jituan.services.tixian_club import filter_tixianjilu_by_request
queryset = filter_tixianjilu_by_request(queryset, request)
# 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录 # 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录
if search_uid: if search_uid:
@@ -2297,8 +2299,11 @@ class AddtxshView(APIView):
current_count = len(records) current_count = len(records)
has_more = (offset + current_count) < total_count has_more = (offset + current_count) < total_count
# 获取状态统计(统计与搜索无关,依然按筛选条件统计) # 获取状态统计(统计与搜索无关,依然按筛选条件统计;按俱乐部隔离
_agg = Tixianjilu.query.filter(leixing=leixing).aggregate( _status_qs = filter_tixianjilu_by_request(
Tixianjilu.query.filter(leixing=leixing), request,
)
_agg = _status_qs.aggregate(
awaiting=Count('id', filter=Q(zhuangtai=1)), awaiting=Count('id', filter=Q(zhuangtai=1)),
processed=Count('id', filter=Q(zhuangtai__in=[2, 3])), processed=Count('id', filter=Q(zhuangtai__in=[2, 3])),
) )
@@ -2393,6 +2398,11 @@ class AdtixianqkView(APIView):
'data': None 'data': None
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, withdrawal)
if deny:
return deny
# 检查状态,只有待审核的可以处理 # 检查状态,只有待审核的可以处理
if withdrawal.zhuangtai != 1: if withdrawal.zhuangtai != 1:
return Response({ return Response({
@@ -2418,9 +2428,10 @@ class AdtixianqkView(APIView):
try: try:
from jituan.services.szjilu_accounting import apply_szjilu_expense from jituan.services.szjilu_accounting import apply_szjilu_expense
tixian_jine = withdrawal.jine tixian_jine = withdrawal.jine
from jituan.services.club_user import get_user_club_id from jituan.services.tixian_club import resolve_payout_club_id
payout_club = get_user_club_id( payout_club = resolve_payout_club_id(
User.query.filter(UserUID=withdrawal.yonghuid).first() jilu=withdrawal,
user=User.query.filter(UserUID=withdrawal.yonghuid).first(),
) )
update_daily_payout(tixian_jine, payout_club) update_daily_payout(tixian_jine, payout_club)
apply_szjilu_expense(tixian_jine, payout_club) apply_szjilu_expense(tixian_jine, payout_club)

View File

@@ -699,14 +699,15 @@ class KefuWithdrawActionView(APIView):
except User.DoesNotExist: except User.DoesNotExist:
raise ValueError(f'用户{tixian.yonghuid}不存在') raise ValueError(f'用户{tixian.yonghuid}不存在')
from jituan.services.club_user import get_user_club_id from jituan.services.tixian_club import resolve_payout_club_id
if action == 1: if action == 1:
tixian.zhuangtai = 2 tixian.zhuangtai = 2
try: try:
from jituan.services.szjilu_accounting import apply_szjilu_expense from jituan.services.szjilu_accounting import apply_szjilu_expense
update_daily_payout(tixian.jine, get_user_club_id(user)) payout_club = resolve_payout_club_id(jilu=tixian, user=user)
apply_szjilu_expense(tixian.jine, get_user_club_id(user)) update_daily_payout(tixian.jine, payout_club)
apply_szjilu_expense(tixian.jine, payout_club)
except Exception as e: except Exception as e:
logger.error(f'更新收支记录失败: {e}') logger.error(f'更新收支记录失败: {e}')
else: else: