Files
Django/jituan/views.py
XingQue 97e9093aa5 fix: 登录接口彻底跳过鉴权,消除 403 权限不足
ClubWechatLoginView / WechatMiniProgramLoginView 覆盖 initial,不做 JWT/权限/限流;动态配置接口也不再解析坏 Token。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 01:11:18 +08:00

1020 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""集团平台 API 视图。"""
import logging
from django.utils import timezone
from rest_framework import status
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken
from jituan.constants import SUPER_ADMIN_PHONES, ADMIN_ROLE_LABELS, CLUB_ID_DEFAULT, DATA_SCOPE_ALL, DATA_SCOPE_SINGLE
from jituan.models import Club, AdminAssignment
from jituan.services.admin_context import build_admin_club_context, is_kefu_backend_account, can_manage_admin_assignments
from jituan.services.kefu_menu import build_menu_access_payload
from jituan.services.wechat_login import login_or_register_by_club, refresh_wx_openid_binding
from jituan.services.caiwu_stats import build_caiwu_payload
from jituan.services.szxx_stats import build_szxx_payload
from jituan.services.admin_assignments import (
create_or_reactivate_assignment,
deactivate_assignment,
dedupe_active_assignments,
)
from jituan.services.user_stats import build_user_summary
from jituan.services.finance_detail_stats import (
build_chongzhi_finance_payload,
build_huiyuan_bankuai_payload,
build_huiyuan_stats_payload,
build_order_finance_payload,
build_order_types_payload,
)
from backend.utils import verify_kefu_permission
from users.business_models import User
logger = logging.getLogger(__name__)
def _client_ip(request):
xff = request.META.get('HTTP_X_FORWARDED_FOR')
if xff:
return xff.split(',')[0].strip()
return request.META.get('REMOTE_ADDR', '')
class ClubWechatLoginView(APIView):
"""
多俱乐部微信小程序登录(新接口,旧 /yonghu/wechatlogin 不变)。
POST /jituan/auth/wechat-login
body: { code, club_id?, app_id? }
注意:必须完全跳过 JWT/权限/限流。
DRF 在 authentication_classes=[] 时若仍抛 AuthenticationFailed
会因无 WWW-Authenticate 头被改写成 HTTP 403前端显示「权限不足」
"""
authentication_classes = ()
permission_classes = ()
throttle_classes = ()
def initial(self, request, *args, **kwargs):
# 登录接口不做任何鉴权/权限/限流,避免 403「权限不足」
return
def post(self, request):
code = (request.data.get('code') or '').strip()
club_id = (request.data.get('club_id') or '').strip() or None
app_id = (request.data.get('app_id') or '').strip() or None
if not code:
return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
try:
data, err = login_or_register_by_club(
code, club_id=club_id, app_id=app_id, client_ip=_client_ip(request),
)
if err:
# 配置缺失 / 微信侧错误:明确 400方便小程序提示运营配 AppSecret
return Response({'code': 2, 'msg': f'微信登录失败: {err}', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
return Response({'code': 0, 'msg': '登录成功', 'data': data})
except Exception as e:
logger.exception('ClubWechatLoginView 异常')
return Response({
'code': 99,
'msg': f'登录异常: {type(e).__name__}: {e}',
'data': None,
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ClubWxBindRefreshView(APIView):
"""
已登录用户静默刷新当前小程序 openid 绑定(支付前 AppID/openid 对齐)。
POST /jituan/auth/refresh-wx-bind
body: { code, club_id?, app_id? }
"""
permission_classes = [IsAuthenticated]
def post(self, request):
code = (request.data.get('code') or '').strip()
club_id = (request.data.get('club_id') or '').strip() or None
app_id = (request.data.get('app_id') or '').strip() or None
if not code:
return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
try:
data, err = refresh_wx_openid_binding(
code, request.user, club_id=club_id, app_id=app_id,
)
if err:
return Response({'code': 2, 'msg': err, 'data': None},
status=status.HTTP_400_BAD_REQUEST)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubWxBindRefreshView 异常')
return Response({'code': 99, 'msg': '刷新绑定失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ClubKefuLoginView(APIView):
"""
多俱乐部客服/集团后台登录(新接口,旧 /yonghu/kefujinru 不变)。
POST /jituan/auth/kefu-login
body: { phone, password, erjimima }
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
def post(self, request):
phone = (request.data.get('phone') or '').strip()
password = request.data.get('password') or ''
erjimima = request.data.get('erjimima') or ''
if not phone or not password:
return Response({'code': 1, 'msg': '请填写手机号和密码', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
user_mains = list(
User.query.filter(Phone=phone, KefuProfile__isnull=False).select_related('KefuProfile')
)
if not user_mains:
admin_candidates = User.query.filter(Phone=phone)
for admin_user in admin_candidates:
if admin_user.IsSuperuser or admin_user.UserType == 'admin' or admin_user.Phone in SUPER_ADMIN_PHONES:
user_mains = [admin_user]
break
if not user_mains:
return Response({'code': 1, 'msg': '用户不存在', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
target_user = None
target_kefu = None
password_matched = False
for user in user_mains:
if not user.CheckPassword(password):
continue
password_matched = True
is_admin = user.IsSuperuser or user.UserType == 'admin' or user.Phone in SUPER_ADMIN_PHONES
if is_admin:
target_user = user
target_kefu = None
break
kefu = user.KefuProfile
stored_erji = kefu.erjimima or ''
if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60:
import bcrypt as _bcrypt
if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')):
continue
elif stored_erji != erjimima:
continue
if kefu.zhuangtai != 1:
if target_user is None:
target_user = user
target_kefu = kefu
continue
target_user = user
target_kefu = kefu
break
else:
if target_user and target_kefu:
return Response({'code': 4, 'msg': '账号已被封禁', 'data': None},
status=status.HTTP_403_FORBIDDEN)
if password_matched:
return Response({'code': 1, 'msg': '二级密码错误', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
return Response({'code': 1, 'msg': '密码错误', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
target_user.IP = _client_ip(request)
target_user.UserLastLoginDate = timezone.now()
target_user.save(update_fields=['IP', 'UserLastLoginDate'])
refresh = RefreshToken.for_user(target_user)
token = str(refresh.access_token)
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
club_context = build_admin_club_context(target_user)
# 登录请求尚未带 JWT需用本次登录用户 + 主任职俱乐部计算菜单
request.user = target_user
request.club_id = club_context.get('club_id') or CLUB_ID_DEFAULT
request.club_scope = club_context.get('scope') or DATA_SCOPE_SINGLE
menu_access, _menu_err = build_menu_access_payload(request, phone)
return Response({
'code': 0,
'msg': '登录成功',
'data': {
'token': token,
'nicheng': nicheng,
'yonghuid': target_user.UserUID,
'club_context': club_context,
'menu_access': menu_access if menu_access else {
'visible_page_ids': [],
'visible_paths': [],
'permission_codes': [],
'role_names': [],
'yonghuid': target_user.UserUID or '',
'can_switch_club': False,
'is_group_admin': False,
'menu_ready': False,
},
},
})
class ClubListView(APIView):
"""GET /jituan/club/list — 启用中的俱乐部列表(登录前后均可读)。"""
permission_classes = [AllowAny]
def get(self, request):
clubs = Club.query.filter(status=1).order_by('sort_order', 'club_id')
data = [
{
'club_id': c.club_id,
'name': c.name,
'wx_appid': c.wx_appid,
'h5_domain': c.h5_domain,
}
for c in clubs
]
return Response({'code': 0, 'msg': 'ok', 'data': data})
class ClubMeContextView(APIView):
"""GET /jituan/auth/me-context — 已登录客服刷新俱乐部上下文。"""
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
if not is_kefu_backend_account(user):
return Response({'code': 403, 'msg': '非后台账号', 'data': None}, status=403)
return Response({
'code': 0,
'msg': 'ok',
'data': build_admin_club_context(user),
})
class ClubKefuMenuAccessView(APIView):
"""GET /jituan/auth/menu-access — 已登录客服可见侧栏菜单。"""
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
if not is_kefu_backend_account(user):
return Response({'code': 403, 'msg': '非后台账号', 'data': None}, status=403)
payload, err = build_menu_access_payload(request)
if err is not None:
return err
return Response({'code': 0, 'msg': 'ok', 'data': payload})
class ClubPermDebugView(APIView):
"""GET /jituan/auth/perm-debug — 当前俱乐部权限解析明细。"""
permission_classes = [IsAuthenticated]
def get(self, request):
from backend.utils import verify_kefu_permission
from jituan.services.admin_context import get_allowed_club_ids, resolve_admin_effective_club
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
from jituan.services.club_rbac import describe_roles_in_scope
user = request.user
if not is_kefu_backend_account(user):
return Response({'code': 403, 'msg': '非后台账号'}, status=403)
requested_club = resolve_club_id_from_request(request)
requested_scope = resolve_club_scope(request)
eff_club, eff_scope, _ = resolve_admin_effective_club(user, request)
allowed = list(get_allowed_club_ids(user))
_, permissions = verify_kefu_permission(request, None)
codes = list(permissions) if isinstance(permissions, list) else ['*']
return Response({
'code': 0,
'msg': 'ok',
'data': {
'yonghuid': user.UserUID,
'phone': user.Phone,
'requested_club_id': requested_club,
'requested_scope': requested_scope,
'effective_club_id': eff_club,
'effective_scope': eff_scope,
'allowed_club_ids': allowed,
'permission_codes': codes,
'roles_in_scope': describe_roles_in_scope(user, eff_club, eff_scope, allowed),
},
})
class ClubAdminAssignmentListView(APIView):
"""POST /jituan/houtai/admin-assignments — 数据范围任职(非 gvsdsdk 功能权限)。"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def _is_super(self, user):
return (
bool(user.IsSuperuser)
or user.UserType == 'admin'
or getattr(user, 'Phone', '') in SUPER_ADMIN_PHONES
)
def _list_rows(self, assignments):
club_name_map = {c.club_id: c.name for c in Club.query.filter(status=1)}
uids = list({a.yonghuid for a in assignments})
user_map = {}
if uids:
for u in User.query.filter(UserUID__in=uids).only('UserUID', 'Phone', 'UserName'):
user_map[u.UserUID] = u.Phone or u.UserName or u.UserUID
rows = []
for a in assignments:
club_label = '集团(全部子公司)'
if a.club_id:
club_label = club_name_map.get(a.club_id, a.club_id)
rows.append({
'id': a.id,
'yonghuid': a.yonghuid,
'phone': user_map.get(a.yonghuid, ''),
'club_id': a.club_id,
'club_label': club_label,
'role_code': a.role_code,
'role_name': ADMIN_ROLE_LABELS.get(a.role_code, a.role_code),
'data_scope': a.data_scope,
'is_primary': a.is_primary,
})
return rows
def post(self, request):
try:
username = (request.data.get('phone') or request.data.get('username') or '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少账号'}, status=401)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
user = request.user
is_super = self._is_super(user)
action = (request.data.get('action') or 'list').strip()
if action in ('create', 'update', 'delete'):
if not can_manage_admin_assignments(user, permissions):
return Response({'code': 403, 'msg': '无权限维护任职数据(需超管或集团超管任职)'}, status=403)
if action == 'create':
yonghuid = (request.data.get('yonghuid') or '').strip()
phone = (request.data.get('target_phone') or '').strip()
if not yonghuid and phone:
target = User.query.filter(Phone=phone).first()
if not target:
return Response({'code': 404, 'msg': f'未找到手机号 {phone}'})
yonghuid = target.UserUID
if not yonghuid:
return Response({'code': 400, 'msg': '需要 yonghuid 或 target_phone'})
club_id = request.data.get('club_id')
if club_id is not None and str(club_id).strip() == '':
club_id = None
elif club_id is not None:
club_id = str(club_id).strip()
role_code = (request.data.get('role_code') or 'CLUB_ADMIN').strip()
data_scope = (request.data.get('data_scope') or DATA_SCOPE_SINGLE).strip()
if club_id is None:
data_scope = DATA_SCOPE_ALL
is_primary = bool(request.data.get('is_primary', False))
_, err = create_or_reactivate_assignment(
yonghuid=yonghuid,
club_id=club_id,
role_code=role_code,
data_scope=data_scope,
is_primary=is_primary,
granted_by=user.UserUID,
)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '任职已添加'})
if action == 'delete':
assignment_id = request.data.get('id')
if not assignment_id:
return Response({'code': 400, 'msg': '缺少任职 id'})
ok, err = deactivate_assignment(assignment_id)
if not ok:
return Response({'code': 404, 'msg': err or '停用失败'})
dedupe_active_assignments()
return Response({'code': 0, 'msg': '任职已停用'})
if action == 'update':
assignment_id = request.data.get('id')
if not assignment_id:
return Response({'code': 400, 'msg': '缺少任职 id'})
row = AdminAssignment.query.filter(id=assignment_id, status=1).first()
if not row:
return Response({'code': 404, 'msg': '任职记录不存在'})
if 'role_code' in request.data:
row.role_code = (request.data.get('role_code') or '').strip() or row.role_code
if 'data_scope' in request.data:
row.data_scope = request.data.get('data_scope') or row.data_scope
if 'is_primary' in request.data:
row.is_primary = bool(request.data.get('is_primary'))
if row.is_primary:
from jituan.services.admin_assignments import clear_other_primary_assignments
clear_other_primary_assignments(row.yonghuid, except_id=row.id)
if 'club_id' in request.data:
cid = request.data.get('club_id')
row.club_id = None if cid is None or str(cid).strip() == '' else str(cid).strip()
if row.club_id is None:
row.data_scope = DATA_SCOPE_ALL
row.granted_by = user.UserUID
row.save()
return Response({'code': 0, 'msg': '任职已更新'})
if is_super:
dedupe_active_assignments()
qs = AdminAssignment.query.filter(status=1).order_by('yonghuid', '-is_primary', 'club_id')
else:
qs = AdminAssignment.query.filter(yonghuid=user.UserUID, status=1).order_by('-is_primary')
return Response({
'code': 0,
'msg': 'ok',
'data': {
'list': self._list_rows(qs.to_list()),
'can_manage': can_manage_admin_assignments(user, permissions),
'perm_note': (
'【功能权限】在「角色管理」里给账号绑 gvsdsdk 角色perm_code 如 caiwu、002ab、000001'
'【数据范围】在本页维护 admin_assignment能看哪个俱乐部/集团汇总)。'
'两套独立:集团高管需在本页配「集团全部」任职 + 角色管理里勾功能菜单。'
),
'current_context': build_admin_club_context(user),
},
})
except Exception:
logger.exception('ClubAdminAssignmentListView 异常')
return Response({'code': 99, 'msg': '任职数据操作失败,请确认已执行 migrate jituan'}, status=500)
class ClubManageView(APIView):
"""POST /jituan/houtai/club-manage — 俱乐部主数据与密钥配置(超级管理员)。"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
_EDITABLE_FIELDS = (
'name', 'status', 'wx_appid', 'wx_secret',
# 收款商户
'mch_id', 'pay_app_id', 'mch_key', 'pay_cert_path', 'pay_key_path',
# 小程序收款通道wechat / fubei
'mini_pay_channel',
# 提现商户
'withdraw_mch_id', 'api_v3_key',
'cert_serial_no', 'private_key_path', 'platform_cert_dir',
'official_appid', 'official_secret', 'official_token', 'encoding_aes_key',
'template_id', 'template_max_per_minute', 'h5_domain', 'oss_prefix',
'goeasy_appkey', 'goeasy_secret', 'sort_order',
)
@staticmethod
def _effective_mch_key(club):
cfg_json = club.config_json or {}
return (
(club.mch_key or '').strip()
or (cfg_json.get('mch_key') or '').strip()
or (cfg_json.get('shanghumiyao') or '').strip()
)
@staticmethod
def _sync_mch_key_to_config_json(club, mch_key):
cfg = dict(club.config_json or {})
cfg['mch_key'] = (mch_key or '').strip()
club.config_json = cfg
def _pay_config_meta(self, club):
"""明确区分:小程序 V2 支付密钥 vs APIv3 密钥,避免后台误导。"""
from jituan.services.wechat_pay import get_wechat_v2_config
cfg_json = club.config_json or {}
mch_key = self._effective_mch_key(club)
api_v3 = (club.api_v3_key or '').strip()
v2_cfg = get_wechat_v2_config(club.club_id)
warnings = []
if not mch_key:
warnings.append(
'未配置收款 APIv2 密钥 mch_key点单/会员等收款不会使用提现 api_v3_key。'
)
if mch_key and api_v3 and mch_key == api_v3:
warnings.append(
'收款 mch_key 与提现 api_v3_key 相同:通常不正确,收款与提现是两套密钥。'
)
withdraw_mch = (getattr(club, 'withdraw_mch_id', None) or '').strip()
pay_mch = (club.mch_id or '').strip()
if withdraw_mch and pay_mch and withdraw_mch != pay_mch:
# 这是预期场景,给信息而非警告
pass
elif not withdraw_mch and pay_mch:
warnings.append(
'提现商户号未单独填写:提现将回落收款商户号 mch_id。若两套商户不同请填写 withdraw_mch_id。'
)
wx_a = (club.wx_appid or '').strip()
pay_a = (club.pay_app_id or '').strip()
if wx_a and pay_a and wx_a != pay_a:
warnings.append(
f'wx_appid({wx_a}) 与 pay_app_id({pay_a}) 不一致:'
'小程序登录/支付 openid 与 wx_appid 绑定,请保持一致或留空 pay_app_id。'
)
if not wx_a and pay_a:
warnings.append(
f'wx_appid 为空但 pay_app_id={pay_a}:登录仍可能回落旧全局 AppID'
'导致 openid 与支付 AppID 不匹配,请填写 wx_appid。'
)
return {
'mch_key': mch_key,
'mch_key_label': 'APIv2 密钥 (mch_key)',
'mch_key_help': '微信商户平台 → API安全 → 设置APIv2密钥用于会员/点单/押金/积分等 JSAPI 收款',
'mch_key_configured': bool(mch_key),
'miniapp_wechat_v2_key': mch_key,
'miniapp_wechat_v2_key_configured': bool(mch_key),
'miniapp_wechat_v2_key_label': 'APIv2 密钥 (mch_key)',
'miniapp_wechat_v2_key_help': '微信商户平台 → API安全 → 设置APIv2密钥用于会员/点单/押金/积分等 JSAPI 收款',
'api_v3_key': api_v3,
'api_v3_key_label': 'APIv3 密钥 (api_v3_key)',
'api_v3_key_help': '微信商户平台 → API安全 → 设置APIv3密钥仅用于自动提现/转账回调解密,不用于小程序收款',
'effective_miniapp_pay': {
'club_id': v2_cfg.get('club_id'),
'appid': v2_cfg.get('appid'),
'mch_id': v2_cfg.get('mch_id'),
'key_len': len(v2_cfg.get('key') or ''),
'key_source': v2_cfg.get('_key_source'),
},
'warnings': warnings,
}
def _serialize(self, club, full=False):
base = {
'club_id': club.club_id,
'name': club.name,
'status': club.status,
'wx_appid': club.wx_appid,
'h5_domain': club.h5_domain,
'oss_prefix': club.oss_prefix,
'mch_id': club.mch_id,
'pay_app_id': club.pay_app_id,
'withdraw_mch_id': getattr(club, 'withdraw_mch_id', '') or '',
'mini_pay_channel': getattr(club, 'mini_pay_channel', None) or 'wechat',
'goeasy_appkey': club.goeasy_appkey,
'template_id': club.template_id,
'sort_order': club.sort_order,
}
if full:
mch_key = self._effective_mch_key(club)
base.update({
'wx_secret': club.wx_secret,
'mch_key': mch_key,
'pay_cert_path': getattr(club, 'pay_cert_path', '') or '',
'pay_key_path': getattr(club, 'pay_key_path', '') or '',
'api_v3_key': club.api_v3_key,
'pay_config': self._pay_config_meta(club),
'cert_serial_no': club.cert_serial_no,
'private_key_path': club.private_key_path,
'platform_cert_dir': club.platform_cert_dir,
'official_appid': club.official_appid,
'official_secret': club.official_secret,
'official_token': club.official_token,
'encoding_aes_key': club.encoding_aes_key,
'goeasy_secret': club.goeasy_secret,
'template_max_per_minute': club.template_max_per_minute,
})
return base
def post(self, request):
from jituan.constants import CLUB_ID_DEFAULT
try:
username = (request.data.get('phone') or request.data.get('username') or '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少账号'}, status=401)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if not can_manage_admin_assignments(request.user):
return Response({'code': 403, 'msg': '仅集团超管可管理俱乐部配置'}, status=403)
action = (request.data.get('action') or 'get').strip()
club_id = (request.data.get('club_id') or '').strip() or CLUB_ID_DEFAULT
if action == 'list':
clubs = Club.query.order_by('sort_order', 'club_id')
return Response({
'code': 0,
'data': [self._serialize(c, full=False) for c in clubs],
})
club = Club.query.filter(club_id=club_id).first()
if not club and action != 'create':
return Response({'code': 1, 'msg': f'俱乐部 {club_id} 不存在'})
if action == 'get':
return Response({'code': 0, 'data': self._serialize(club, full=True)})
if action == 'update':
update_fields = []
for field in self._EDITABLE_FIELDS:
if field in request.data:
val = request.data[field]
if field in ('mch_key', 'api_v3_key', 'wx_secret'):
val = (val or '').strip()
if field == 'mini_pay_channel':
val = (val or 'wechat').strip().lower()
if val not in ('wechat', 'fubei'):
return Response({'code': 400, 'msg': 'mini_pay_channel 仅支持 wechat/fubei'})
setattr(club, field, val)
update_fields.append(field)
if 'mch_key' in request.data:
self._sync_mch_key_to_config_json(club, request.data.get('mch_key'))
if 'config_json' not in update_fields:
update_fields.append('config_json')
if update_fields:
club.save(update_fields=update_fields)
return Response({
'code': 0,
'msg': '保存成功',
'data': self._serialize(club, full=True),
})
if action == 'create':
from jituan.services.club_bootstrap import bootstrap_club_from_template
new_club_id = (request.data.get('new_club_id') or request.data.get('club_id') or '').strip()
name = (request.data.get('name') or '').strip()
if not new_club_id or not name:
return Response({'code': 400, 'msg': '新建俱乐部需要 new_club_id 与 name'})
try:
club = bootstrap_club_from_template(
new_club_id,
name,
wx_appid=(request.data.get('wx_appid') or '').strip(),
template_club_id=(request.data.get('from_club') or CLUB_ID_DEFAULT).strip(),
)
except ValueError as exc:
return Response({'code': 400, 'msg': str(exc)})
return Response({
'code': 0,
'msg': '俱乐部创建成功',
'data': self._serialize(club, full=True),
})
return Response({'code': 400, 'msg': '无效 action支持 list/get/update/create'})
except Exception:
logger.exception('ClubManageView 异常')
return Response({'code': 99, 'msg': '俱乐部配置操作失败'}, status=500)
class ClubCertUploadView(APIView):
"""
POST /jituan/houtai/club-cert-upload
multipart: phone, club_id, kind, file
kind: pay_key | pay_cert | withdraw_key | withdraw_cert | withdraw_platform
"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser]
def post(self, request):
try:
username = (request.data.get('phone') or request.data.get('username') or '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少账号'}, status=401)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if not can_manage_admin_assignments(request.user):
return Response({'code': 403, 'msg': '仅集团超管可上传商户证书'}, status=403)
club_id = (request.data.get('club_id') or '').strip()
kind = (request.data.get('kind') or '').strip()
uploaded = request.FILES.get('file')
if not club_id or not kind or not uploaded:
return Response({'code': 400, 'msg': '需要 club_id、kind、file'})
club = Club.query.filter(club_id=club_id).first()
if not club:
return Response({'code': 404, 'msg': f'俱乐部 {club_id} 不存在'})
from jituan.services.club_cert_upload import save_club_cert_file, CERT_KINDS
if kind not in CERT_KINDS:
return Response({
'code': 400,
'msg': f'kind 无效,支持: {", ".join(CERT_KINDS.keys())}',
})
try:
result = save_club_cert_file(club, kind, uploaded)
except ValueError as exc:
return Response({'code': 400, 'msg': str(exc)})
except OSError as exc:
logger.exception('证书落盘失败 club=%s kind=%s', club_id, kind)
return Response({'code': 500, 'msg': f'写入失败: {exc}'})
club = Club.query.filter(club_id=club_id).first()
return Response({
'code': 0,
'msg': '上传成功',
'data': {
**result,
'pay_cert_path': getattr(club, 'pay_cert_path', '') or '',
'pay_key_path': getattr(club, 'pay_key_path', '') or '',
'private_key_path': club.private_key_path or '',
'platform_cert_dir': club.platform_cert_dir or '',
'withdraw_mch_id': getattr(club, 'withdraw_mch_id', '') or '',
'mch_id': club.mch_id or '',
},
})
except Exception:
logger.exception('ClubCertUploadView 异常')
return Response({'code': 99, 'msg': '证书上传失败'}, status=500)
class ClubCaiwuView(APIView):
"""
俱乐部维度财务(新接口)。
旧 POST /houtai/caiwu 完全不变kefu 切换俱乐部后应调本接口。
Header: X-Club-Id, 集团汇总 X-Club-Scope: all
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'caiwu' not in permissions:
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
try:
data = build_caiwu_payload(request)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubCaiwuView 异常')
return Response({'code': 99, 'msg': '统计失败', 'data': None}, status=500)
class ClubSzxxView(APIView):
"""
俱乐部维度每日收支详细统计(新接口)。
旧 POST /houtai/szxx 不变;参数与旧接口一致。
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'caiwu' not in permissions:
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
granularity = request.data.get('granularity', 'day')
year = request.data.get('year')
month = request.data.get('month')
summary_date = request.data.get('summary_date')
try:
if year is not None:
year = int(year)
if month is not None:
month = int(month)
except (ValueError, TypeError):
return Response({'code': 1, 'msg': '年份或月份格式错误'}, status=400)
try:
data, err = build_szxx_payload(request, granularity, year, month, summary_date)
if err:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubSzxxView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class ClubAuditLogListView(APIView):
"""
后台审计日志admin_audit_log
POST /jituan/houtai/audit-log
权限000001 或 czrz666与操作日志一致
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = (request.data.get('username') or request.data.get('phone') or '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions and 'czrz666' not in permissions:
return Response({'code': 403, 'msg': '无权限查看审计日志'})
try:
page = int(request.data.get('page', 1))
except (ValueError, TypeError):
page = 1
try:
page_size = int(request.data.get('page_size', 20))
except (ValueError, TypeError):
page_size = 20
data = list_admin_audit_logs(
request,
page=page,
page_size=page_size,
operator_yonghuid=request.data.get('operator_yonghuid'),
target_yonghuid=request.data.get('target_yonghuid'),
action=request.data.get('action'),
keyword=request.data.get('keyword'),
)
return Response({'code': 0, 'msg': 'ok', 'data': data})
def _parse_year_month(data):
year = data.get('year')
month = data.get('month')
try:
if year is not None:
year = int(year)
if month is not None:
month = int(month)
except (ValueError, TypeError):
return None, None, Response({'code': 1, 'msg': '年份或月份格式错误'}, status=400)
return year, month, None
class ClubOrderTypesView(APIView):
"""POST /jituan/houtai/cwddhqlx — 订单财务商品类型筛选。"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'dingdancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
return Response({'code': 0, 'msg': 'ok', 'data': build_order_types_payload()})
class ClubOrderFinanceView(APIView):
"""POST /jituan/houtai/cwhqjtddsj — 订单财务详细统计(按俱乐部)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'dingdancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
year, month, err_resp = _parse_year_month(request.data)
if err_resp:
return err_resp
try:
data, err = build_order_finance_payload(
request,
request.data.get('granularity', 'day'),
year,
month,
request.data.get('type_id', 0),
request.data.get('order_source', 0),
request.data.get('summary_date'),
)
if err:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubOrderFinanceView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class ClubChongzhiFinanceView(APIView):
"""POST /jituan/houtai/cwqtczhq — 其他充值/罚款财务统计(按俱乐部)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'qtczcaiwu' not in permissions:
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
year, month, err_resp = _parse_year_month(request.data)
if err_resp:
return err_resp
try:
data, err = build_chongzhi_finance_payload(
request,
request.data.get('granularity', 'day'),
year,
month,
request.data.get('types', [2, 3, 4, 5, 6]),
request.data.get('summary_date'),
)
if err:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubChongzhiFinanceView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class ClubHuiyuanBankuaiView(APIView):
"""POST /jituan/houtai/cwhybkhq — 板块及会员列表(附带俱乐部售价)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'huiyuancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
payload = build_huiyuan_bankuai_payload(request)
return Response({
'code': 0,
'msg': 'ok',
'data': payload['bankuai_list'],
'club_id': payload['club_id'],
'scope': payload['scope'],
})
class ClubHuiyuanStatsView(APIView):
"""POST /jituan/houtai/hybkjtsj — 会员财务详细统计(按俱乐部)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'huiyuancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
year, month, err_resp = _parse_year_month(request.data)
if err_resp:
return err_resp
try:
data, err = build_huiyuan_stats_payload(
request,
request.data.get('huiyuan_id'),
request.data.get('granularity', 'day'),
year,
month,
request.data.get('include_guanshi', False),
request.data.get('include_zuzhang', False),
request.data.get('summary_date'),
)
if err:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubHuiyuanStatsView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class ClubUserSummaryView(APIView):
"""
用户管理汇总(打手/管事/商家/组长数量)。
集团视图 X-Club-Scope: all 时额外返回 by_club 各公司拆分。
POST /jituan/houtai/user-summary
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = (request.data.get('phone') or request.data.get('username') or '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少账号'}, status=401)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
try:
data = build_user_summary(request)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubUserSummaryView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class ClubDashouRegisterView(APIView):
"""
打手邀请码注册(带俱乐部校验)。
POST /jituan/auth/dashou-register body: { inviteCode }
与 /yonghu/dashouzhuce 逻辑一致,推荐新小程序使用。
"""
permission_classes = [IsAuthenticated]
def post(self, request):
from users.views import DashouZhuceView
return DashouZhuceView().post(request)