Files
Django/jituan/views.py
XingQue 5d3e4e32a2 fix: 小程序支付 AppID 与登录一致,登录覆盖过期 openid 绑定
- JSAPI 支付优先 club.wx_appid(与 jscode2session 一致,不再优先 pay_app_id)
- 登录时同一用户+俱乐部只保留最新 openid,清除旧绑定
- 支付仅读 user_wx_openid,彻底禁用 legacy OpenID/UserName 回落

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 20:52:56 +08:00

875 lines
37 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
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
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:
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': '系统繁忙,请稍后重试', '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', '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',
)
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 = (cfg_json.get('mch_key') or cfg_json.get('shanghumiyao') or '').strip()
api_v3 = (club.api_v3_key or '').strip()
v2_cfg = get_wechat_v2_config(club.club_id)
warnings = []
if not mch_key:
warnings.append(
'未配置 mch_key小程序/会员/押金/点单等微信支付不会使用 api_v3_key'
'未填时将回落服务器 app_secrets 旧密钥,易与当前商户号不匹配导致签名错误。'
)
if mch_key and api_v3 and mch_key == api_v3:
warnings.append(
'mch_key 与 api_v3_key 内容相同通常不正确V2小程序支付与 V3转账是两套不同密钥。'
)
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 {
'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': '仅用于微信 V3 接口(如转账到零钱),不用于小程序统一下单',
'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,
'goeasy_appkey': club.goeasy_appkey,
'template_id': club.template_id,
'sort_order': club.sort_order,
}
if full:
cfg_json = club.config_json or {}
mch_key = (cfg_json.get('mch_key') or cfg_json.get('shanghumiyao') or '').strip()
base.update({
'wx_secret': club.wx_secret,
'api_v3_key': club.api_v3_key,
# 小程序 JSAPI 支付实际使用的 V2 密钥(与 api_v3_key 不是同一个)
'mch_key': mch_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:
setattr(club, field, request.data[field])
update_fields.append(field)
if 'mch_key' in request.data:
cfg = dict(club.config_json or {})
cfg['mch_key'] = (request.data.get('mch_key') or '').strip()
club.config_json = cfg
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 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)