chore: 拉取远程前保留本地改动
含 kefu_base 修改及 club_payment_channel 相关文件。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,352 +1,352 @@
|
||||
"""users.views.kefu_base - auto-generated by split script."""
|
||||
"""users.views.kefu - auto-generated by split script."""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
import requests
|
||||
import hashlib
|
||||
import traceback
|
||||
import decimal
|
||||
import jwt
|
||||
import ipaddress
|
||||
import xmltodict
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models, transaction, IntegrityError, DatabaseError, connection
|
||||
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
|
||||
from django.core.cache import cache
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.http import HttpResponse
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from utils.money import yuan_to_fen
|
||||
from utils.fadan_utils import check_fadan_qiangdan_eligible
|
||||
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||||
|
||||
from orders.utils import (
|
||||
update_daily_payout,
|
||||
settle_shangjia_order_guanshi_fenhong
|
||||
)
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action, update_guanshi_daily_by_action,
|
||||
update_shangjia_daily, update_zuzhang_daily_by_action,
|
||||
)
|
||||
from jituan.services.admin_context import is_kefu_backend_account
|
||||
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
|
||||
|
||||
from ..models import (
|
||||
UserBoss, UserDashou, UserShangjia, UserGuanshi,
|
||||
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
|
||||
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
|
||||
RankingRecord
|
||||
)
|
||||
from users.business_models import User
|
||||
from ..tixian_shenhe_services import (
|
||||
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
|
||||
mark_transfer_success, release_collect_quota_for_record,
|
||||
close_audit_wechat_failed, query_wechat_transfer_bill,
|
||||
check_shijidaozhang_within_limit,
|
||||
check_dashou_trial_blocks_commission_withdraw,
|
||||
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
|
||||
)
|
||||
from ..tixian_shenhe_views import process_audit_collect
|
||||
|
||||
from orders.models import (
|
||||
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
|
||||
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
|
||||
Penalty, PenaltyAppealImage
|
||||
)
|
||||
from products.models import (
|
||||
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
|
||||
)
|
||||
from config.models import Qunpeizhi
|
||||
from backend.models import MerchantDailyStats
|
||||
from rank.models import (
|
||||
KaohePayTemp, Chenghao, ShenheJilu,
|
||||
KaoheCishuFeiyong, YonghuChenghao
|
||||
)
|
||||
from users.models import UserShenheguan
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from rank.utils import create_shenhe_jilu_from_temp
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KefuLoginView(APIView):
|
||||
"""
|
||||
客服/管理员登录接口(支持手机号重复)
|
||||
请求:POST /yonghu/kefujinru
|
||||
请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
|
||||
返回:
|
||||
code=0 成功,data包含token,nicheng,yonghuid
|
||||
code=1 失败(msg区分:用户不存在/密码错误/二级密码错误)
|
||||
code=4 账号被封禁
|
||||
|
||||
管理员(IsSuperuser 或 UserType='admin')无需 erjimima 和 KefuProfile。
|
||||
"""
|
||||
throttle_classes = [AnonRateThrottle] # 限制匿名请求频率
|
||||
permission_classes = [AllowAny] # 任何人都可访问
|
||||
|
||||
def post(self, request):
|
||||
# 1. 获取参数
|
||||
phone = request.data.get('phone', '').strip()
|
||||
password = request.data.get('password', '')
|
||||
erjimima = request.data.get('erjimima', '')
|
||||
|
||||
if not phone or not password:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '请填写手机号和密码',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 获取客户端真实IP
|
||||
client_ip = self.get_client_ip(request)
|
||||
|
||||
# 3. 查询该手机号下所有账号(客服 + 管理员)
|
||||
# 先查客服(有 KefuProfile),再查管理员(IsSuperuser 或 UserType='admin')
|
||||
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':
|
||||
user_mains = [admin_user]
|
||||
break
|
||||
|
||||
if not user_mains:
|
||||
logger.warning(f"登录失败,用户不存在: {phone}, IP: {client_ip}")
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '用户不存在',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 遍历所有匹配的账号,验证密码和二级密码
|
||||
target_user = None
|
||||
target_kefu = None
|
||||
password_matched = False
|
||||
|
||||
for user in user_mains:
|
||||
# 验证主密码(bcrypt 哈希验证)
|
||||
if not user.CheckPassword(password):
|
||||
continue
|
||||
password_matched = True
|
||||
|
||||
# 管理员:跳过二级密码和客服扩展表检查
|
||||
is_admin = user.IsSuperuser or user.UserType == 'admin'
|
||||
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:
|
||||
# 只有被禁用的账号匹配,返回封禁提示
|
||||
logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}")
|
||||
return Response({
|
||||
'code': 4,
|
||||
'msg': '账号已被封禁',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
elif password_matched:
|
||||
# 密码正确但二级密码错误
|
||||
logger.warning(f"登录失败,二级密码错误: {phone}, IP: {client_ip}")
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '二级密码错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
# 密码错误
|
||||
logger.warning(f"登录失败,密码错误: {phone}, IP: {client_ip}")
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '密码错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 5. 更新IP和最后登录时间
|
||||
target_user.IP = client_ip
|
||||
target_user.UserLastLoginDate = timezone.now()
|
||||
target_user.save(update_fields=['IP', 'UserLastLoginDate'])
|
||||
|
||||
# 6. 生成JWT token
|
||||
refresh = RefreshToken.for_user(target_user)
|
||||
token = str(refresh.access_token)
|
||||
|
||||
# 7. 返回成功数据(管理员无 KefuProfile,nicheng 用 UserName 或 Phone 兜底)
|
||||
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '登录成功',
|
||||
'data': {
|
||||
'token': token,
|
||||
'nicheng': nicheng,
|
||||
'yonghuid': target_user.UserUID,
|
||||
}
|
||||
})
|
||||
|
||||
def get_client_ip(self, request):
|
||||
"""
|
||||
获取客户端真实IP
|
||||
"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0].strip()
|
||||
if ip:
|
||||
return ip
|
||||
return request.META.get('REMOTE_ADDR', '0.0.0.0')
|
||||
|
||||
|
||||
class KefuStatsView(APIView):
|
||||
"""
|
||||
客服统计数据接口
|
||||
请求:POST /yonghu/kfjrhqzl
|
||||
请求体:{"phone": "13800138000"}
|
||||
认证:必须携带有效的 JWT token(放在 Authorization 头)
|
||||
返回:
|
||||
code=0 成功,data 包含 jinrichuli, jinyuechuli, zongchuli
|
||||
验证失败统一返回 401,不透露具体原因
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated] # 必须登录
|
||||
|
||||
|
||||
def post(self, request):
|
||||
# 1. 获取请求中的 phone
|
||||
phone = request.data.get('phone', '').strip()
|
||||
if not phone:
|
||||
# 缺少参数,但按约定返回 401(可以改为 400,但为了统一,用401)
|
||||
logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.UserUID}")
|
||||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 2. 获取当前登录用户(从 JWT 中解析)
|
||||
current_user = request.user
|
||||
|
||||
# 3. 验证前端传来的 phone 是否与当前用户的 phone 一致
|
||||
if current_user.Phone != phone:
|
||||
# 不一致,可能 token 被冒用或参数错误
|
||||
logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
|
||||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 4. 验证是否为后台客服账号
|
||||
from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
|
||||
if not is_kefu_backend_account(current_user):
|
||||
logger.warning(f"统计接口用户类型非客服: {current_user.UserUID}")
|
||||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 5. 尝试获取客服扩展表数据(管理员跳过)
|
||||
is_admin = is_system_super_admin(current_user)
|
||||
if is_admin:
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'jinrichuli': 0,
|
||||
'jinyuechuli': 0,
|
||||
'zongchuli': 0,
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
try:
|
||||
kefu_profile = current_user.KefuProfile # OneToOneField 反向关系
|
||||
except UserKefu.DoesNotExist:
|
||||
logger.warning(f"统计接口客服扩展表不存在: {current_user.UserUID}")
|
||||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 6. 返回统计数据(字段名与前端约定一致)
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'jinrichuli': kefu_profile.jinrichuli,
|
||||
'jinyuechuli': kefu_profile.jinyuechuli,
|
||||
'zongchuli': kefu_profile.zongchuli,
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class KefuGetOrderTypesView(APIView):
|
||||
"""
|
||||
客服获取订单类型列表(平台/商家/跨平台订单页共用)
|
||||
请求:POST /yonghu/kfhqptddlx
|
||||
认证:JWT + 与 menu-access 相同的 is_kefu_backend_account
|
||||
返回:code=0 + 类型列表
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
if not is_kefu_backend_account(request.user):
|
||||
return Response({'code': 403, 'msg': '非后台账号'}, status=403)
|
||||
|
||||
try:
|
||||
types_qs = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
|
||||
type_list = [
|
||||
{
|
||||
'id': t.id,
|
||||
'biaoti': t.jieshao or '',
|
||||
'tupian_url': t.tupian_url or ''
|
||||
}
|
||||
for t in types_qs
|
||||
]
|
||||
return Response({'code': 0, 'data': type_list})
|
||||
except Exception as e:
|
||||
logger.error(f"查询商品类型表失败: {str(e)}")
|
||||
return Response({'code': 0, 'data': []})
|
||||
"""users.views.kefu_base - auto-generated by split script."""
|
||||
"""users.views.kefu - auto-generated by split script."""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
import requests
|
||||
import hashlib
|
||||
import traceback
|
||||
import decimal
|
||||
import jwt
|
||||
import ipaddress
|
||||
import xmltodict
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models, transaction, IntegrityError, DatabaseError, connection
|
||||
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
|
||||
from django.core.cache import cache
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.http import HttpResponse
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
|
||||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||
from utils.money import yuan_to_fen
|
||||
from utils.fadan_utils import check_fadan_qiangdan_eligible
|
||||
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||||
|
||||
from orders.utils import (
|
||||
update_daily_payout,
|
||||
settle_shangjia_order_guanshi_fenhong
|
||||
)
|
||||
from backend.utils import (
|
||||
update_dashou_daily_by_action, update_guanshi_daily_by_action,
|
||||
update_shangjia_daily, update_zuzhang_daily_by_action,
|
||||
)
|
||||
from jituan.services.admin_context import is_kefu_backend_account
|
||||
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
|
||||
|
||||
from ..models import (
|
||||
UserBoss, UserDashou, UserShangjia, UserGuanshi,
|
||||
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
|
||||
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
|
||||
RankingRecord
|
||||
)
|
||||
from users.business_models import User
|
||||
from ..tixian_shenhe_services import (
|
||||
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
|
||||
mark_transfer_success, release_collect_quota_for_record,
|
||||
close_audit_wechat_failed, query_wechat_transfer_bill,
|
||||
check_shijidaozhang_within_limit,
|
||||
check_dashou_trial_blocks_commission_withdraw,
|
||||
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
|
||||
)
|
||||
from ..tixian_shenhe_views import process_audit_collect
|
||||
|
||||
from orders.models import (
|
||||
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
|
||||
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
|
||||
Penalty, PenaltyAppealImage
|
||||
)
|
||||
from products.models import (
|
||||
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
|
||||
)
|
||||
from config.models import Qunpeizhi
|
||||
from backend.models import MerchantDailyStats
|
||||
from rank.models import (
|
||||
KaohePayTemp, Chenghao, ShenheJilu,
|
||||
KaoheCishuFeiyong, YonghuChenghao
|
||||
)
|
||||
from users.models import UserShenheguan
|
||||
from products.utils import update_shangpin_daily_stat
|
||||
from shop.utils import update_dianpu_daily_stat
|
||||
from rank.utils import create_shenhe_jilu_from_temp
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KefuLoginView(APIView):
|
||||
"""
|
||||
客服/管理员登录接口(支持手机号重复)
|
||||
请求:POST /yonghu/kefujinru
|
||||
请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
|
||||
返回:
|
||||
code=0 成功,data包含token,nicheng,yonghuid
|
||||
code=1 失败(msg区分:用户不存在/密码错误/二级密码错误)
|
||||
code=4 账号被封禁
|
||||
|
||||
管理员(IsSuperuser 或 UserType='admin')无需 erjimima 和 KefuProfile。
|
||||
"""
|
||||
throttle_classes = [AnonRateThrottle] # 限制匿名请求频率
|
||||
permission_classes = [AllowAny] # 任何人都可访问
|
||||
|
||||
def post(self, request):
|
||||
# 1. 获取参数
|
||||
phone = request.data.get('phone', '').strip()
|
||||
password = request.data.get('password', '')
|
||||
erjimima = request.data.get('erjimima', '')
|
||||
|
||||
if not phone or not password:
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '请填写手机号和密码',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2. 获取客户端真实IP
|
||||
client_ip = self.get_client_ip(request)
|
||||
|
||||
# 3. 查询该手机号下所有账号(客服 + 管理员)
|
||||
# 先查客服(有 KefuProfile),再查管理员(IsSuperuser 或 UserType='admin')
|
||||
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':
|
||||
user_mains = [admin_user]
|
||||
break
|
||||
|
||||
if not user_mains:
|
||||
logger.warning(f"登录失败,用户不存在: {phone}, IP: {client_ip}")
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '用户不存在',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4. 遍历所有匹配的账号,验证密码和二级密码
|
||||
target_user = None
|
||||
target_kefu = None
|
||||
password_matched = False
|
||||
|
||||
for user in user_mains:
|
||||
# 验证主密码(bcrypt 哈希验证)
|
||||
if not user.CheckPassword(password):
|
||||
continue
|
||||
password_matched = True
|
||||
|
||||
# 管理员:跳过二级密码和客服扩展表检查
|
||||
is_admin = user.IsSuperuser or user.UserType == 'admin'
|
||||
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:
|
||||
# 只有被禁用的账号匹配,返回封禁提示
|
||||
logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}")
|
||||
return Response({
|
||||
'code': 4,
|
||||
'msg': '账号已被封禁',
|
||||
'data': None
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
elif password_matched:
|
||||
# 密码正确但二级密码错误
|
||||
logger.warning(f"登录失败,二级密码错误: {phone}, IP: {client_ip}")
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '二级密码错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
# 密码错误
|
||||
logger.warning(f"登录失败,密码错误: {phone}, IP: {client_ip}")
|
||||
return Response({
|
||||
'code': 1,
|
||||
'msg': '密码错误',
|
||||
'data': None
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 5. 更新IP和最后登录时间
|
||||
target_user.IP = client_ip
|
||||
target_user.UserLastLoginDate = timezone.now()
|
||||
target_user.save(update_fields=['IP', 'UserLastLoginDate'])
|
||||
|
||||
# 6. 生成JWT token
|
||||
refresh = RefreshToken.for_user(target_user)
|
||||
token = str(refresh.access_token)
|
||||
|
||||
# 7. 返回成功数据(管理员无 KefuProfile,nicheng 用 UserName 或 Phone 兜底)
|
||||
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': '登录成功',
|
||||
'data': {
|
||||
'token': token,
|
||||
'nicheng': nicheng,
|
||||
'yonghuid': target_user.UserUID,
|
||||
}
|
||||
})
|
||||
|
||||
def get_client_ip(self, request):
|
||||
"""
|
||||
获取客户端真实IP
|
||||
"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0].strip()
|
||||
if ip:
|
||||
return ip
|
||||
return request.META.get('REMOTE_ADDR', '0.0.0.0')
|
||||
|
||||
|
||||
class KefuStatsView(APIView):
|
||||
"""
|
||||
客服统计数据接口
|
||||
请求:POST /yonghu/kfjrhqzl
|
||||
请求体:{"phone": "13800138000"}
|
||||
认证:必须携带有效的 JWT token(放在 Authorization 头)
|
||||
返回:
|
||||
code=0 成功,data 包含 jinrichuli, jinyuechuli, zongchuli
|
||||
验证失败统一返回 401,不透露具体原因
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated] # 必须登录
|
||||
|
||||
|
||||
def post(self, request):
|
||||
# 1. 获取请求中的 phone
|
||||
phone = request.data.get('phone', '').strip()
|
||||
if not phone:
|
||||
# 缺少参数,但按约定返回 401(可以改为 400,但为了统一,用401)
|
||||
logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.UserUID}")
|
||||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 2. 获取当前登录用户(从 JWT 中解析)
|
||||
current_user = request.user
|
||||
|
||||
# 3. 验证前端传来的 phone 是否与当前用户的 phone 一致
|
||||
if current_user.Phone != phone:
|
||||
# 不一致,可能 token 被冒用或参数错误
|
||||
logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
|
||||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 4. 验证是否为后台客服账号
|
||||
from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
|
||||
if not is_kefu_backend_account(current_user):
|
||||
logger.warning(f"统计接口用户类型非客服: {current_user.UserUID}")
|
||||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 5. 尝试获取客服扩展表数据(管理员跳过)
|
||||
is_admin = is_system_super_admin(current_user)
|
||||
if is_admin:
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'jinrichuli': 0,
|
||||
'jinyuechuli': 0,
|
||||
'zongchuli': 0,
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
try:
|
||||
kefu_profile = current_user.KefuProfile # OneToOneField 反向关系
|
||||
except UserKefu.DoesNotExist:
|
||||
logger.warning(f"统计接口客服扩展表不存在: {current_user.UserUID}")
|
||||
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# 6. 返回统计数据(字段名与前端约定一致)
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'jinrichuli': kefu_profile.jinrichuli,
|
||||
'jinyuechuli': kefu_profile.jinyuechuli,
|
||||
'zongchuli': kefu_profile.zongchuli,
|
||||
}
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class KefuGetOrderTypesView(APIView):
|
||||
"""
|
||||
客服获取订单类型列表(平台/商家/跨平台订单页共用)
|
||||
请求:POST /yonghu/kfhqptddlx
|
||||
认证:JWT + 与 menu-access 相同的 is_kefu_backend_account
|
||||
返回:code=0 + 类型列表
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def post(self, request):
|
||||
if not is_kefu_backend_account(request.user):
|
||||
return Response({'code': 403, 'msg': '非后台账号'}, status=403)
|
||||
|
||||
try:
|
||||
types_qs = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
|
||||
type_list = [
|
||||
{
|
||||
'id': t.id,
|
||||
'biaoti': t.jieshao or '',
|
||||
'tupian_url': t.tupian_url or ''
|
||||
}
|
||||
for t in types_qs
|
||||
]
|
||||
return Response({'code': 0, 'data': type_list})
|
||||
except Exception as e:
|
||||
logger.error(f"查询商品类型表失败: {str(e)}")
|
||||
return Response({'code': 0, 'data': []})
|
||||
|
||||
Reference in New Issue
Block a user