1281 lines
55 KiB
Python
1281 lines
55 KiB
Python
"""集团平台 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,
|
|
get_allowed_club_ids,
|
|
is_system_super_admin,
|
|
)
|
|
from jituan.services.kefu_menu import build_menu_access_payload
|
|
from jituan.services.wechat_login import (
|
|
login_or_register_by_club,
|
|
login_or_register_by_official_oauth,
|
|
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? }
|
|
"""
|
|
throttle_classes = [AnonRateThrottle]
|
|
permission_classes = [AllowAny]
|
|
|
|
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 ClubWechatOAuthLoginView(APIView):
|
|
"""
|
|
服务号网页授权登录(H5 UniApp)。
|
|
POST /jituan/auth/wechat-oauth-login
|
|
body: { code, club_id? }
|
|
不改动 wechat-login;仅新增本接口。
|
|
"""
|
|
throttle_classes = [AnonRateThrottle]
|
|
permission_classes = [AllowAny]
|
|
|
|
def post(self, request):
|
|
code = (request.data.get('code') or '').strip()
|
|
club_id = (request.data.get('club_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_official_oauth(
|
|
code, club_id=club_id, client_ip=_client_ip(request),
|
|
)
|
|
if err:
|
|
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('ClubWechatOAuthLoginView 异常')
|
|
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
|
|
|
|
from jituan.services.admin_context import is_hardcoded_super_phone
|
|
try:
|
|
kefu_probe = user.KefuProfile
|
|
except Exception:
|
|
kefu_probe = None
|
|
if kefu_probe is not None and int(getattr(kefu_probe, 'zhuangtai', 1) or 0) != 1:
|
|
if not is_hardcoded_super_phone(user):
|
|
if target_user is None:
|
|
target_user = user
|
|
target_kefu = kefu_probe
|
|
continue
|
|
|
|
is_admin = user.IsSuperuser or user.UserType == 'admin' or user.Phone in SUPER_ADMIN_PHONES
|
|
if is_admin:
|
|
target_user = user
|
|
target_kefu = kefu_probe
|
|
break
|
|
kefu = kefu_probe
|
|
if kefu is None:
|
|
continue
|
|
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 and int(getattr(target_kefu, 'zhuangtai', 1) or 0) != 1:
|
|
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 ClubDefaultInviteView(APIView):
|
|
"""
|
|
GET/POST /jituan/club/default-invite
|
|
小程序登录页拉取当前俱乐部默认打手邀请码(AllowAny)。
|
|
俱乐部由 X-Club-Id / AppID 解析;码来自 config_json.default_dashou_invite 或内置回落。
|
|
"""
|
|
permission_classes = [AllowAny]
|
|
throttle_classes = [AnonRateThrottle]
|
|
|
|
def _payload(self, request):
|
|
from jituan.services.club_resolver import resolve_club_id_for_wechat_request
|
|
from jituan.services.default_invite import get_club_default_dashou_invite
|
|
|
|
club_id = resolve_club_id_for_wechat_request(request)
|
|
invite = get_club_default_dashou_invite(club_id)
|
|
return Response({
|
|
'code': 0,
|
|
'msg': 'ok',
|
|
'data': {
|
|
'club_id': club_id or '',
|
|
'inviteCode': invite,
|
|
},
|
|
})
|
|
|
|
def get(self, request):
|
|
return self._payload(request)
|
|
|
|
def post(self, request):
|
|
return self._payload(request)
|
|
|
|
|
|
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 EhConsoleLoginView(APIView):
|
|
"""
|
|
2H 运营台专用登录(全新接口,与旧后台无关)。
|
|
POST /jituan/auth/eh-console-login
|
|
body: { phone, password, erjimima }
|
|
|
|
注意:绝不改动 /jituan/auth/kefu-login;旧 kefu 后台继续用旧登录。
|
|
"""
|
|
throttle_classes = [AnonRateThrottle]
|
|
permission_classes = [AllowAny]
|
|
|
|
def post(self, request):
|
|
from jituan.services.eh_console_login import EH_CONSOLE_CLUB_ID, login_eh_console
|
|
|
|
phone = (request.data.get('phone') or '').strip()
|
|
password = request.data.get('password') or ''
|
|
erjimima = request.data.get('erjimima') or ''
|
|
http_status, body = login_eh_console(
|
|
request, phone, password, erjimima, club_id=EH_CONSOLE_CLUB_ID,
|
|
)
|
|
return Response(body, status=http_status)
|
|
|
|
|
|
class ClubConsoleGateView(APIView):
|
|
"""
|
|
GET /jituan/auth/console-gate?club_id=eh
|
|
2H 运营台会话复查门禁(新接口)。不参与旧后台登录。
|
|
"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
from jituan.services.eh_console_login import user_can_access_console_club
|
|
|
|
user = request.user
|
|
if not is_kefu_backend_account(user):
|
|
return Response({'code': 403, 'msg': '非后台账号', 'data': None}, status=403)
|
|
|
|
target = (request.query_params.get('club_id') or '').strip() or 'eh'
|
|
allowed = sorted(get_allowed_club_ids(user))
|
|
ok = bool(user_can_access_console_club(user, target))
|
|
payload = {
|
|
'allowed': ok,
|
|
'club_id': target,
|
|
'allowed_club_ids': allowed,
|
|
'phone': getattr(user, 'Phone', '') or '',
|
|
'is_super': bool(is_system_super_admin(user)),
|
|
}
|
|
if not ok:
|
|
return Response(
|
|
{
|
|
'code': 403,
|
|
'msg': f'无权进入俱乐部「{target}」运营台,请使用已在该俱乐部配置的账号',
|
|
'data': payload,
|
|
},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
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 None or str(club_id).strip() == '':
|
|
club_id = ''
|
|
else:
|
|
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 not club_id:
|
|
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})
|
|
from jituan.services.admin_assignments import dedupe_active_assignments
|
|
dedupe_active_assignments()
|
|
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 '停用失败'})
|
|
from jituan.services.admin_assignments import dedupe_active_assignments
|
|
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):
|
|
from jituan.services.default_invite import get_club_default_dashou_invite
|
|
from jituan.services.force_phone_auth import is_club_force_phone_auth
|
|
from jituan.services.dashou_chat_gate import (
|
|
get_dashou_chat_yajin_threshold,
|
|
is_dashou_chat_allow_without_member,
|
|
)
|
|
from jituan.services.yajin_jifen_switch import is_yajin_purchase_fill_jifen
|
|
from jituan.services.team_recruit_switch import is_dashou_team_recruit_enabled
|
|
from jituan.services.withdraw_eligibility_switch import (
|
|
is_withdraw_require_formal_huiyuan,
|
|
is_withdraw_require_jifen,
|
|
)
|
|
|
|
cfg_json = club.config_json or {}
|
|
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,
|
|
'default_dashou_invite': get_club_default_dashou_invite(club.club_id),
|
|
'force_phone_auth': is_club_force_phone_auth(club.club_id),
|
|
'dashou_chat_allow_without_member': is_dashou_chat_allow_without_member(club.club_id),
|
|
'dashou_chat_yajin_threshold': float(get_dashou_chat_yajin_threshold(club.club_id)),
|
|
'yajin_purchase_fill_jifen': is_yajin_purchase_fill_jifen(club.club_id),
|
|
'dashou_team_recruit_enabled': is_dashou_team_recruit_enabled(club.club_id),
|
|
'withdraw_require_formal_huiyuan': is_withdraw_require_formal_huiyuan(club.club_id),
|
|
'withdraw_require_jifen': is_withdraw_require_jifen(club.club_id),
|
|
}
|
|
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,
|
|
'default_dashou_invite_raw': (cfg_json.get('default_dashou_invite') or '').strip(),
|
|
})
|
|
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 'default_dashou_invite' in request.data:
|
|
from jituan.services.default_invite import set_club_default_dashou_invite
|
|
set_club_default_dashou_invite(club, request.data.get('default_dashou_invite'))
|
|
if 'config_json' not in update_fields:
|
|
update_fields.append('config_json')
|
|
if 'force_phone_auth' in request.data:
|
|
from jituan.services.force_phone_auth import set_club_force_phone_auth
|
|
raw = request.data.get('force_phone_auth')
|
|
if isinstance(raw, str):
|
|
enabled = raw.strip().lower() in ('1', 'true', 'yes', 'on')
|
|
else:
|
|
enabled = bool(raw)
|
|
set_club_force_phone_auth(club, enabled)
|
|
if 'config_json' not in update_fields:
|
|
update_fields.append('config_json')
|
|
if 'dashou_chat_allow_without_member' in request.data:
|
|
from jituan.services.dashou_chat_gate import set_dashou_chat_allow_without_member
|
|
raw = request.data.get('dashou_chat_allow_without_member')
|
|
if isinstance(raw, str):
|
|
enabled = raw.strip().lower() in ('1', 'true', 'yes', 'on')
|
|
else:
|
|
enabled = bool(raw)
|
|
set_dashou_chat_allow_without_member(club, enabled)
|
|
if 'config_json' not in update_fields:
|
|
update_fields.append('config_json')
|
|
if 'dashou_chat_yajin_threshold' in request.data:
|
|
from jituan.services.dashou_chat_gate import set_dashou_chat_yajin_threshold
|
|
set_dashou_chat_yajin_threshold(club, request.data.get('dashou_chat_yajin_threshold'))
|
|
if 'config_json' not in update_fields:
|
|
update_fields.append('config_json')
|
|
if 'yajin_purchase_fill_jifen' in request.data:
|
|
from jituan.services.yajin_jifen_switch import set_yajin_purchase_fill_jifen
|
|
raw = request.data.get('yajin_purchase_fill_jifen')
|
|
if isinstance(raw, str):
|
|
enabled = raw.strip().lower() in ('1', 'true', 'yes', 'on')
|
|
else:
|
|
enabled = bool(raw)
|
|
set_yajin_purchase_fill_jifen(club, enabled)
|
|
if 'config_json' not in update_fields:
|
|
update_fields.append('config_json')
|
|
if 'dashou_team_recruit_enabled' in request.data:
|
|
from jituan.services.team_recruit_switch import set_dashou_team_recruit_enabled
|
|
raw = request.data.get('dashou_team_recruit_enabled')
|
|
if isinstance(raw, str):
|
|
enabled = raw.strip().lower() in ('1', 'true', 'yes', 'on')
|
|
else:
|
|
enabled = bool(raw)
|
|
set_dashou_team_recruit_enabled(club, enabled)
|
|
if 'config_json' not in update_fields:
|
|
update_fields.append('config_json')
|
|
if 'withdraw_require_formal_huiyuan' in request.data:
|
|
from jituan.services.withdraw_eligibility_switch import set_withdraw_require_formal_huiyuan
|
|
raw = request.data.get('withdraw_require_formal_huiyuan')
|
|
if isinstance(raw, str):
|
|
enabled = raw.strip().lower() in ('1', 'true', 'yes', 'on')
|
|
else:
|
|
enabled = bool(raw)
|
|
set_withdraw_require_formal_huiyuan(club, enabled)
|
|
if 'config_json' not in update_fields:
|
|
update_fields.append('config_json')
|
|
if 'withdraw_require_jifen' in request.data:
|
|
from jituan.services.withdraw_eligibility_switch import set_withdraw_require_jifen
|
|
raw = request.data.get('withdraw_require_jifen')
|
|
if isinstance(raw, str):
|
|
enabled = raw.strip().lower() in ('1', 'true', 'yes', 'on')
|
|
else:
|
|
enabled = bool(raw)
|
|
set_withdraw_require_jifen(club, enabled)
|
|
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),
|
|
})
|
|
|
|
if action in ('seed_invite_status', 'seed_invite'):
|
|
from jituan.services.club_seed_invite import (
|
|
club_invite_seed_status,
|
|
ensure_club_seed_invite,
|
|
)
|
|
if action == 'seed_invite_status':
|
|
return Response({
|
|
'code': 0,
|
|
'data': club_invite_seed_status(club_id),
|
|
})
|
|
result = ensure_club_seed_invite(club_id)
|
|
if not result.get('ok'):
|
|
return Response({'code': 400, 'msg': result.get('msg') or '种子失败'})
|
|
club = Club.query.filter(club_id=club_id).first()
|
|
return Response({
|
|
'code': 0,
|
|
'msg': result.get('msg') or 'ok',
|
|
'data': {
|
|
**result,
|
|
'club': self._serialize(club, full=True) if club else None,
|
|
},
|
|
})
|
|
|
|
return Response({
|
|
'code': 400,
|
|
'msg': '无效 action,支持 list/get/update/create/seed_invite/seed_invite_status',
|
|
})
|
|
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)
|